1 /** 2 * @fileOverview Resource class definition 3 */ 4 5 var _ = require('underscore') 6 , ResponseError = require('./response_error') 7 , EventEmitter = require('events').EventEmitter 8 ; 9 10 /** 11 * Base class for objects repreenting resources in the spire api. It is meant 12 * to be extended by other classes. 13 * 14 * <p>Resources have methods for making requests to the spire api. These methods 15 * are defined with `Request.defineRequest`. Note that this is a method on the 16 * Request object itself, not the prototype. It is used to create methods on 17 * the prototype of Resource classes.</p> 18 * 19 * <p>The `Resource` class provides default requests for the `get`, `put`, 20 * `delete`, and `post` methods. These methods can be overwritten by 21 * subclasses.</p> 22 * 23 * <p>Once a request method has been defined, it can be run with 24 * <pre><code> 25 * resource.request(<request name>); 26 * </code></pre> 27 * </p> 28 * 29 * <p>Such request methods have no side effects, and return JSON objects direct 30 * from the spire api.</p> 31 * 32 * @class Base class for Spire API Resources 33 * 34 * @constructor 35 * @extends EventEmitter 36 * @param {object} spire Spire object 37 * @param {object} data Resource data from the spire api 38 */ 39 function Resource(spire, data) { 40 /** 41 * Reference to spire object. 42 */ 43 this.spire = spire; 44 45 /** 46 * Actual data from the spire.io api. 47 */ 48 this.data = data; 49 } 50 51 Resource.prototype = new EventEmitter(); 52 53 module.exports = Resource; 54 55 /** 56 * Creates a request method on given object. 57 * 58 * `name` is the name of the request. This is what gets passed to the `request` 59 * method when actually calling the request. 60 * 61 * For instance, a request defined with 62 * <pre><code> 63 * defineRequest(somePrototype, 'get', createGetReq); 64 * </code></pre> 65 * 66 * is run by calling 67 * <pre><code> 68 * resource.request('get', callback); 69 * </code></pre> 70 * 71 * `fn` is a function that takes any number of arguments and returns a hash 72 * describing the http request we are about to send. Any arguments to this 73 * function can be passed to the `request` method. 74 * 75 * For example, suppose `createGetReq` from above is the following function, 76 * which takes an id number as argument as puts it in the query params: 77 * 78 * <pre><code> 79 * function createGetReq (id) { 80 * return { 81 * method: 'get', 82 * url: this.url(), 83 * query: { id: id }, 84 * }; 85 * }; 86 * </code></pre> 87 * 88 * You would call that request like this: 89 * <code>resource.request('get', id, callback);</code> 90 * 91 * @param {object} obj Object to add the request method to. 92 * @param {string} name Name of the request method 93 * @param {function (*args)} fn Function taking any number of arguments, returning an object describing the HTTP request. 94 */ 95 Resource.defineRequest = function (obj, name, fn) { 96 obj['_req_' + name] = function () { 97 var shred = this.spire.shred; 98 99 var args = Array.prototype.slice.call(arguments); 100 var callback = args.pop(); 101 102 var req = fn.apply(this, args); 103 104 return shred.request(req) 105 .on('error', function (res) { 106 var error = new ResponseError(res, req); 107 callback(error); 108 }) 109 .on('success', function (res) { 110 callback(null, res.body.data); 111 }); 112 }; 113 }; 114 115 /** 116 * <p>Makes the request with the given name. 117 * 118 * <p>Arguments are the request name, followed by any number of arguments that will 119 * be passed to the function which creates the request description, and a 120 * callback. 121 */ 122 Resource.prototype.request = function () { 123 var args = Array.prototype.slice.call(arguments); 124 var reqName = args.shift(); 125 var cb = args[args.length - 1]; 126 if (typeof this['_req_' + reqName] !== 'function') { 127 return cb(new Error("No request defined for " + reqName)); 128 } 129 return this['_req_' + reqName].apply(this, args); 130 }; 131 132 /** 133 * <p>Gets the resource. 134 * 135 * <p>Default method that may be overwritten by subclasses. 136 * 137 * @param {function (err, resource)} cb Callback 138 */ 139 Resource.prototype.get = function (cb) { 140 var resource = this; 141 this.request('get', function (err, data) { 142 if (err) return cb(err); 143 resource.data = data; 144 cb(null, resource); 145 }); 146 }; 147 148 /** 149 * <p>Gets the resource if we have the get capability. Otherwise just return the resource. 150 * 151 * <p>Default method that may be overwritten by subclasses. 152 * 153 * @param {function (err, resource)} cb Callback 154 */ 155 Resource.prototype.getIfCapable = function(cb){ 156 if(this.capability('get')){ 157 this.get(cb); 158 } else { 159 cb(null, this); 160 } 161 } 162 163 /** 164 * <p>Updates (puts to) the resource. 165 * 166 * <p>Default method that may be overwritten by subclasses. 167 * 168 * @param {object} data Resource data 169 * @param {function (err, resource)} cb Callback 170 */ 171 Resource.prototype.update = function (data, cb) { 172 var resource = this; 173 this.request('update', data, function (err, data) { 174 if (err) return cb(err); 175 resource.data = data; 176 cb(null, resource); 177 }); 178 }; 179 180 /** 181 * <p>Delete the resource. 182 * 183 * <p>Default method that may be overwritten by subclasses. 184 * 185 * @param {function (err, resource)} cb Callback 186 */ 187 Resource.prototype['delete'] = function (cb) { 188 cb = cb || function (err) { if (err) { throw err; } }; 189 var resource = this; 190 this.request('delete', function (err, data) { 191 if (err) return cb(err); 192 delete resource.data; 193 cb(null); 194 }); 195 }; 196 197 /** 198 * Returns the url for the resource. 199 * 200 * @returns {string} url 201 */ 202 Resource.prototype.url = function () { 203 return this.data.url; 204 }; 205 206 /** 207 * Returns the capabilities for the resource. 208 * 209 * @returns {object} Capabilities 210 */ 211 Resource.prototype.capabilities = function () { 212 return this.data.capabilities; 213 }; 214 215 /** 216 * Returns the capability for the resource and method 217 * 218 * @returns {string} Capability 219 */ 220 Resource.prototype.capability = function (method) { 221 return this.capabilities()[method]; 222 }; 223 224 /** 225 * Returns the Authorization header for this resource, or for another capability 226 * if one is passed in. 227 * 228 * @param {Resource} [resource] Optional resource 229 * @param {string} method Method 230 * @returns {string} Authorization header 231 */ 232 Resource.prototype.authorization = function (method, resource) { 233 var cap; 234 if (resource) { 235 if (typeof resource.capability === 'function') { 236 cap = resource.capability(method); 237 } else { 238 cap = resource.capabilities[method]; 239 } 240 } else { 241 cap = this.capability(method); 242 } 243 return "Capability " + cap; 244 }; 245 246 /** 247 * Returns the resource schema for this resource the resource of a given name. 248 * 249 * @param {string} [name] Optional name of the resource schema to return 250 * @returns {string} Schema 251 */ 252 Resource.prototype.schema = function (name) { 253 name = name || this.resourceName; 254 255 if (!this.spire.api.schema) { 256 throw "No description object. Run `spire.api.discover` first."; 257 } 258 259 if (!this.spire.api.schema[name]) { 260 throw "No schema for resource " + name; 261 } 262 263 return this.spire.api.schema[name]; 264 }; 265 266 /** 267 * Returns the MIME type for this resource or the resource of a given name. 268 * 269 * @param {string} [name] Optional name of the resource MIME type to return 270 * @returns {string} MIME type 271 */ 272 Resource.prototype.mediaType = function (name) { 273 return this.schema(name).mediaType; 274 }; 275 276 /** 277 * Requests 278 * These define API calls and have no side effects. They can be run by calling 279 * this.request(<request name>); 280 * 281 * The requests defined on the Resource class are defaults. They can be 282 * overwritten by subclasses. 283 */ 284 285 /** 286 * Gets the resource. 287 * @name get 288 * @ignore 289 */ 290 Resource.defineRequest(Resource.prototype, 'get', function () { 291 return { 292 method: 'get', 293 url: this.url(), 294 headers: { 295 'Authorization': this.authorization('get'), 296 'Accept': this.mediaType(), 297 'Content-Type': this.mediaType() 298 } 299 }; 300 }); 301 302 /** 303 * Updates (puts) to the resouce. 304 * @name update 305 * @ignore 306 */ 307 Resource.defineRequest(Resource.prototype, 'update', function (data) { 308 return { 309 method: 'put', 310 url: this.url(), 311 content: data, 312 headers: { 313 'Authorization': this.authorization('update'), 314 'Accept': this.mediaType(), 315 'Content-Type': this.mediaType() 316 } 317 }; 318 }); 319 320 /** 321 * Deletes a resource. 322 * @name delete 323 * @ignore 324 */ 325 Resource.defineRequest(Resource.prototype, 'delete', function () { 326 return { 327 method: 'delete', 328 url: this.url(), 329 headers: { 330 'Authorization': this.authorization('delete'), 331 'Accept': this.mediaType(), 332 'Content-Type': this.mediaType() 333 } 334 }; 335 }); 336