weekly release 2.4dev
[moodle.git] / lib / yuilib / 3.7.1 / build / model-sync-rest / model-sync-rest-debug.js
blobab34dbca8194a2b8ccd7efae74d496a81f6fb09c
1 /*
2 YUI 3.7.1 (build 5627)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('model-sync-rest', function (Y, NAME) {
9 /**
10 An extension which provides a RESTful XHR sync implementation that can be mixed
11 into a Model or ModelList subclass.
13 @module app
14 @submodule model-sync-rest
15 @since 3.6.0
16 **/
18 var Lang = Y.Lang;
20 /**
21 An extension which provides a RESTful XHR sync implementation that can be mixed
22 into a Model or ModelList subclass.
24 This makes it trivial for your Model or ModelList subclasses communicate and
25 transmit their data via RESTful XHRs. In most cases you'll only need to provide
26 a value for `root` when sub-classing `Y.Model`.
28     Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
29         root: '/users'
30     });
32     Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
33         // By convention `Y.User`'s `root` will be used for the lists' URL.
34         model: Y.User
35     });
37     var users = new Y.Users();
39     // GET users list from: "/users"
40     users.load(function () {
41         var firstUser = users.item(0);
43         firstUser.get('id'); // => "1"
45         // PUT updated user data at: "/users/1"
46         firstUser.set('name', 'Eric').save();
47     });
49 @class ModelSync.REST
50 @extensionfor Model
51 @extensionfor ModelList
52 @since 3.6.0
53 **/
54 function RESTSync() {}
56 /**
57 A request authenticity token to validate HTTP requests made by this extension
58 with the server when the request results in changing persistent state. This
59 allows you to protect your server from Cross-Site Request Forgery attacks.
61 A CSRF token provided by the server can be embedded in the HTML document and
62 assigned to `YUI.Env.CSRF_TOKEN` like this:
64     <script>
65         YUI.Env.CSRF_TOKEN = {{session.authenticityToken}};
66     </script>
68 The above should come after YUI seed file so that `YUI.Env` will be defined.
70 **Note:** This can be overridden on a per-request basis. See `sync()` method.
72 When a value for the CSRF token is provided, either statically or via `options`
73 passed to the `save()` and `destroy()` methods, the applicable HTTP requests
74 will have a `X-CSRF-Token` header added with the token value.
76 @property CSRF_TOKEN
77 @type String
78 @default YUI.Env.CSRF_TOKEN
79 @static
80 @since 3.6.0
81 **/
82 RESTSync.CSRF_TOKEN = YUI.Env.CSRF_TOKEN;
84 /**
85 Static flag to use the HTTP POST method instead of PUT or DELETE.
87 If the server-side HTTP framework isn't RESTful, setting this flag to `true`
88 will cause all PUT and DELETE requests to instead use the POST HTTP method, and
89 add a `X-HTTP-Method-Override` HTTP header with the value of the method type
90 which was overridden.
92 @property EMULATE_HTTP
93 @type Boolean
94 @default false
95 @static
96 @since 3.6.0
97 **/
98 RESTSync.EMULATE_HTTP = false;
101 Default headers used with all XHRs.
103 By default the `Accept` and `Content-Type` headers are set to
104 "application/json", this signals to the HTTP server to process the request
105 bodies as JSON and send JSON responses. If you're sending and receiving content
106 other than JSON, you can override these headers and the `parse()` and
107 `serialize()` methods.
109 **Note:** These headers will be merged with any request-specific headers, and
110 the request-specific headers will take precedence.
112 @property HTTP_HEADERS
113 @type Object
114 @default
115     {
116         "Accept"      : "application/json",
117         "Content-Type": "application/json"
118     }
119 @static
120 @since 3.6.0
122 RESTSync.HTTP_HEADERS = {
123     'Accept'      : 'application/json',
124     'Content-Type': 'application/json'
128 Static mapping of RESTful HTTP methods corresponding to CRUD actions.
130 @property HTTP_METHODS
131 @type Object
132 @default
133     {
134         "create": "POST",
135         "read"  : "GET",
136         "update": "PUT",
137         "delete": "DELETE"
138     }
139 @static
140 @since 3.6.0
142 RESTSync.HTTP_METHODS = {
143     'create': 'POST',
144     'read'  : 'GET',
145     'update': 'PUT',
146     'delete': 'DELETE'
150 The number of milliseconds before the XHRs will timeout/abort. This defaults to
151 30 seconds.
153 **Note:** This can be overridden on a per-request basis. See `sync()` method.
155 @property HTTP_TIMEOUT
156 @type Number
157 @default 30000
158 @static
159 @since 3.6.0
161 RESTSync.HTTP_TIMEOUT = 30000;
164 Properties that shouldn't be turned into ad-hoc attributes when passed to a
165 Model or ModelList constructor.
167 @property _NON_ATTRS_CFG
168 @type Array
169 @default ["root", "url"]
170 @static
171 @protected
172 @since 3.6.0
174 RESTSync._NON_ATTRS_CFG = ['root', 'url'];
176 RESTSync.prototype = {
178     // -- Public Properties ----------------------------------------------------
180     /**
181     A string which represents the root or collection part of the URL which
182     relates to a Model or ModelList. Usually this value should be same for all
183     instances of a specific Model/ModelList subclass.
185     When sub-classing `Y.Model`, usually you'll only need to override this
186     property, which lets the URLs for the XHRs be generated by convention. If
187     the `root` string ends with a trailing-slash, XHR URLs will also end with a
188     "/", and if the `root` does not end with a slash, neither will the XHR URLs.
190     @example
191         Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
192             root: '/users'
193         });
195         var currentUser, newUser;
197         // GET the user data from: "/users/123"
198         currentUser = new Y.User({id: '123'}).load();
200         // POST the new user data to: "/users"
201         newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
203     When sub-classing `Y.ModelList`, usually you'll want to ignore configuring
204     the `root` and simply rely on the build-in convention of the list's
205     generated URLs defaulting to the `root` specified by the list's `model`.
207     @property root
208     @type String
209     @default ""
210     @since 3.6.0
211     **/
212     root: '',
214     /**
215     A string which specifies the URL to use when making XHRs, if not value is
216     provided, the URLs used to make XHRs will be generated by convention.
218     While a `url` can be provided for each Model/ModelList instance, usually
219     you'll want to either rely on the default convention or provide a tokenized
220     string on the prototype which can be used for all instances.
222     When sub-classing `Y.Model`, you will probably be able to rely on the
223     default convention of generating URLs in conjunction with the `root`
224     property and whether the model is new or not (i.e. has an `id`). If the
225     `root` property ends with a trailing-slash, the generated URL for the
226     specific model will also end with a trailing-slash.
228     @example
229         Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
230             root: '/users/'
231         });
233         var currentUser, newUser;
235         // GET the user data from: "/users/123/"
236         currentUser = new Y.User({id: '123'}).load();
238         // POST the new user data to: "/users/"
239         newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
241     If a `url` is specified, it will be processed by `Y.Lang.sub()`, which is
242     useful when the URLs for a Model/ModelList subclass match a specific pattern
243     and can use simple replacement tokens; e.g.:
245     @example
246         Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
247             root: '/users',
248             url : '/users/{username}'
249         });
251     **Note:** String subsitituion of the `url` only use string an number values
252     provided by this object's attribute and/or the `options` passed to the
253     `getURL()` method. Do not expect something fancy to happen with Object,
254     Array, or Boolean values, they will simply be ignored.
256     If your URLs have plural roots or collection URLs, while the specific item
257     resources are under a singular name, e.g. "/users" (plural) and "/user/123"
258     (singular), you'll probably want to configure the `root` and `url`
259     properties like this:
261     @example
262         Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
263             root: '/users',
264             url : '/user/{id}'
265         });
267         var currentUser, newUser;
269         // GET the user data from: "/user/123"
270         currentUser = new Y.User({id: '123'}).load();
272         // POST the new user data to: "/users"
273         newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
275     When sub-classing `Y.ModelList`, usually you'll be able to rely on the
276     associated `model` to supply its `root` to be used as the model list's URL.
277     If this needs to be customized, you can provide a simple string for the
278     `url` property.
280     @example
281         Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
282             // Leverages `Y.User`'s `root`, which is "/users".
283             model: Y.User
284         });
286         // Or specified explicitly...
288         Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
289             model: Y.User,
290             url  : '/users'
291         });
293     @property url
294     @type String
295     @default ""
296     @since 3.6.0
297     **/
298     url: '',
300     // -- Lifecycle Methods ----------------------------------------------------
302     initializer: function (config) {
303         config || (config = {});
305         // Overrides `root` at the instance level.
306         if ('root' in config) {
307             this.root = config.root || '';
308         }
310         // Overrides `url` at the instance level.
311         if ('url' in config) {
312             this.url = config.url || '';
313         }
314     },
316     // -- Public Methods -------------------------------------------------------
318     /**
319     Returns the URL for this model or model list for the given `action` and
320     `options`, if specified.
322     This method correctly handles the variations of `root` and `url` values and
323     is called by the `sync()` method to get the URLs used to make the XHRs.
325     You can override this method if you need to provide a specific
326     implementation for how the URLs of your Model and ModelList subclasses need
327     to be generated.
329     @method getURL
330     @param {String} [action] Optional `sync()` action for which to generate the
331         URL.
332     @param {Object} [options] Optional options which may be used to help
333         generate the URL.
334     @return {String} this model's or model list's URL for the the given
335         `action` and `options`.
336     @since 3.6.0
337     **/
338     getURL: function (action, options) {
339         var root = this.root,
340             url  = this.url;
342         // If this is a model list, use its `url` and substitute placeholders,
343         // but default to the `root` of its `model`. By convention a model's
344         // `root` is the location to a collection resource.
345         if (this._isYUIModelList) {
346             if (!url) {
347                 return this.model.prototype.root;
348             }
350             return this._substituteURL(url, Y.merge(this.getAttrs(), options));
351         }
353         // Assume `this` is a model.
355         // When a model is new, i.e. has no `id`, the `root` should be used. By
356         // convention a model's `root` is the location to a collection resource.
357         // The model's `url` will be used as a fallback if `root` isn't defined.
358         if (root && (action === 'create' || this.isNew())) {
359             return root;
360         }
362         // When a model's `url` is not provided, we'll generate a URL to use by
363         // convention. This will combine the model's `id` with its configured
364         // `root` and add a trailing-slash if the root ends with "/".
365         if (!url) {
366             return this._joinURL(this.getAsURL('id') || '');
367         }
369         // Substitute placeholders in the `url` with URL-encoded values from the
370         // model's attribute values or the specified `options`.
371         return this._substituteURL(url, Y.merge(this.getAttrs(), options));
372     },
374     /**
375     Called to parse the response object returned from `Y.io()`. This method
376     receives the full response object and is expected to "prep" a response which
377     is suitable to pass to the `parse()` method.
379     By default the response body is returned (`responseText`), because it
380     usually represents the entire entity of this model on the server.
382     If you need to parse data out of the response's headers you should do so by
383     overriding this method. If you'd like the entire response object from the
384     XHR to be passed to your `parse()` method, you can simply assign this
385     property to `false`.
387     @method parseIOResponse
388     @param {Object} response Response object from `Y.io()`.
389     @return {Any} The modified response to pass along to the `parse()` method.
390     @since 3.7.0
391     **/
392     parseIOResponse: function (response) {
393         return response.responseText;
394     },
396     /**
397     Serializes `this` model to be used as the HTTP request entity body.
399     By default this model will be serialized to a JSON string via its `toJSON()`
400     method.
402     You can override this method when the HTTP server expects a different
403     representation of this model's data that is different from the default JSON
404     serialization. If you're sending and receive content other than JSON, be
405     sure change the `Accept` and `Content-Type` `HTTP_HEADERS` as well.
407     **Note:** A model's `toJSON()` method can also be overridden. If you only
408     need to modify which attributes are serialized to JSON, that's a better
409     place to start.
411     @method serialize
412     @param {String} [action] Optional `sync()` action for which to generate the
413         the serialized representation of this model.
414     @return {String} serialized HTTP request entity body.
415     @since 3.6.0
416     **/
417     serialize: function (action) {
418         return Y.JSON.stringify(this);
419     },
421     /**
422     Communicates with a RESTful HTTP server by sending and receiving data via
423     XHRs. This method is called internally by load(), save(), and destroy().
425     The URL used for each XHR will be retrieved by calling the `getURL()` method
426     and passing it the specified `action` and `options`.
428     This method relies heavily on standard RESTful HTTP conventions
430     @method sync
431     @param {String} action Sync action to perform. May be one of the following:
433       * `create`: Store a newly-created model for the first time.
434       * `delete`: Delete an existing model.
435       * `read`  : Load an existing model.
436       * `update`: Update an existing model.
438     @param {Object} [options] Sync options:
439       @param {String} [options.csrfToken] The authenticity token used by the
440         server to verify the validity of this request and protected against CSRF
441         attacks. This overrides the default value provided by the static
442         `CSRF_TOKEN` property.
443       @param {Object} [options.headers] The HTTP headers to mix with the default
444         headers specified by the static `HTTP_HEADERS` property.
445       @param {Number} [options.timeout] The number of milliseconds before the
446         request will timeout and be aborted. This overrides the default provided
447         by the static `HTTP_TIMEOUT` property.
448     @param {Function} [callback] Called when the sync operation finishes.
449       @param {Error|null} callback.err If an error occurred, this parameter will
450         contain the error. If the sync operation succeeded, _err_ will be
451         falsy.
452       @param {Any} [callback.response] The server's response.
453     **/
454     sync: function (action, options, callback) {
455         options || (options = {});
457         var url       = this.getURL(action, options),
458             method    = RESTSync.HTTP_METHODS[action],
459             headers   = Y.merge(RESTSync.HTTP_HEADERS, options.headers),
460             timeout   = options.timeout || RESTSync.HTTP_TIMEOUT,
461             csrfToken = options.csrfToken || RESTSync.CSRF_TOKEN,
462             entity;
464         // Prepare the content if we are sending data to the server.
465         if (method === 'POST' || method === 'PUT') {
466             entity = this.serialize(action);
467         } else {
468             // Remove header, no content is being sent.
469             delete headers['Content-Type'];
470         }
472         // Setup HTTP emulation for older servers if we need it.
473         if (RESTSync.EMULATE_HTTP &&
474                 (method === 'PUT' || method === 'DELETE')) {
476             // Pass along original method type in the headers.
477             headers['X-HTTP-Method-Override'] = method;
479             // Fall-back to using POST method type.
480             method = 'POST';
481         }
483         // Add CSRF token to HTTP request headers if one is specified and the
484         // request will cause side effects on the server.
485         if (csrfToken &&
486                 (method === 'POST' || method === 'PUT' || method === 'DELETE')) {
488             headers['X-CSRF-Token'] = csrfToken;
489         }
491         this._sendSyncIORequest({
492             action  : action,
493             callback: callback,
494             entity  : entity,
495             headers : headers,
496             method  : method,
497             timeout : timeout,
498             url     : url
499         });
500     },
502     // -- Protected Methods ----------------------------------------------------
504     /**
505     Joins the `root` URL to the specified `url`, normalizing leading/trailing
506     "/" characters.
508     @example
509         model.root = '/foo'
510         model._joinURL('bar');  // => '/foo/bar'
511         model._joinURL('/bar'); // => '/foo/bar'
513         model.root = '/foo/'
514         model._joinURL('bar');  // => '/foo/bar/'
515         model._joinURL('/bar'); // => '/foo/bar/'
517     @method _joinURL
518     @param {String} url URL to append to the `root` URL.
519     @return {String} Joined URL.
520     @protected
521     @since 3.6.0
522     **/
523     _joinURL: function (url) {
524         var root = this.root;
526         if (!(root || url)) {
527             return '';
528         }
530         if (url.charAt(0) === '/') {
531             url = url.substring(1);
532         }
534         // Combines the `root` with the `url` and adds a trailing-slash if the
535         // `root` has a trailing-slash.
536         return root && root.charAt(root.length - 1) === '/' ?
537                 root + url + '/' :
538                 root + '/' + url;
539     },
542     /**
543     Calls both public, overrideable methods: `parseIOResponse()`, then `parse()`
544     and returns the result.
546     This will call into `parseIOResponse()`, if it's defined as a method,
547     passing it the full response object from the XHR and using its return value
548     to pass along to the `parse()`. This enables developers to easily parse data
549     out of the response headers which should be used by the `parse()` method.
551     @method _parse
552     @param {Object} response Response object from `Y.io()`.
553     @return {Object|Object[]} Attribute hash or Array of model attribute hashes.
554     @protected
555     @since 3.7.0
556     **/
557     _parse: function (response) {
558         // When `parseIOResponse` is defined as a method, it will be invoked and
559         // the result will become the new response object that the `parse()`
560         // will be invoked with.
561         if (typeof this.parseIOResponse === 'function') {
562             response = this.parseIOResponse(response);
563         }
565         return this.parse(response);
566     },
568     /**
569     Performs the XHR and returns the resulting `Y.io()` request object.
571     This method is called by `sync()`.
573     @method _sendSyncIORequest
574     @param {Object} config An object with the following properties:
575       @param {String} config.action The `sync()` action being performed.
576       @param {Function} [config.callback] Called when the sync operation
577         finishes.
578       @param {String} [config.entity] The HTTP request entity body.
579       @param {Object} config.headers The HTTP request headers.
580       @param {String} config.method The HTTP request method.
581       @param {Number} [config.timeout] Time until the HTTP request is aborted.
582       @param {String} config.url The URL of the HTTP resource.
583     @return {Object} The resulting `Y.io()` request object.
584     @protected
585     @since 3.6.0
586     **/
587     _sendSyncIORequest: function (config) {
588         return Y.io(config.url, {
589             'arguments': {
590                 action  : config.action,
591                 callback: config.callback,
592                 url     : config.url
593             },
595             context: this,
596             data   : config.entity,
597             headers: config.headers,
598             method : config.method,
599             timeout: config.timeout,
601             on: {
602                 start  : this._onSyncIOStart,
603                 failure: this._onSyncIOFailure,
604                 success: this._onSyncIOSuccess,
605                 end    : this._onSyncIOEnd
606             }
607         });
608     },
610     /**
611     Utility which takes a tokenized `url` string and substitutes its
612     placeholders using a specified `data` object.
614     This method will property URL-encode any values before substituting them.
615     Also, only expect it to work with String and Number values.
617     @example
618         var url = this._substituteURL('/users/{name}', {id: 'Eric F'});
619         // => "/users/Eric%20F"
621     @method _substituteURL
622     @param {String} url Tokenized URL string to substitute placeholder values.
623     @param {Object} data Set of data to fill in the `url`'s placeholders.
624     @return {String} Substituted URL.
625     @protected
626     @since 3.6.0
627     **/
628     _substituteURL: function (url, data) {
629         if (!url) {
630             return '';
631         }
633         var values = {};
635         // Creates a hash of the string and number values only to be used to
636         // replace any placeholders in a tokenized `url`.
637         Y.Object.each(data, function (v, k) {
638             if (Lang.isString(v) || Lang.isNumber(v)) {
639                 // URL-encode any string or number values.
640                 values[k] = encodeURIComponent(v);
641             }
642         });
644         return Lang.sub(url, values);
645     },
647     // -- Event Handlers -------------------------------------------------------
649     /**
650     Called when the `Y.io` request has finished, after "success" or "failure"
651     has been determined.
653     This is a no-op by default, but provides a hook for overriding.
655     @method _onSyncIOEnd
656     @param {String} txId The `Y.io` transaction id.
657     @param {Object} details Extra details carried through from `sync()`:
658       @param {String} details.action The sync action performed.
659       @param {Function} [details.callback] The function to call after syncing.
660       @param {String} details.url The URL of the requested resource.
661     @protected
662     @since 3.6.0
663     **/
664     _onSyncIOEnd: function (txId, details) {},
666     /**
667     Called when the `Y.io` request has finished unsuccessfully.
669     By default this calls the `details.callback` function passing it the HTTP
670     status code and message as an error object along with the response body.
672     @method _onSyncIOFailure
673     @param {String} txId The `Y.io` transaction id.
674     @param {Object} res The `Y.io` response object.
675     @param {Object} details Extra details carried through from `sync()`:
676       @param {String} details.action The sync action performed.
677       @param {Function} [details.callback] The function to call after syncing.
678       @param {String} details.url The URL of the requested resource.
679     @protected
680     @since 3.6.0
681     **/
682     _onSyncIOFailure: function (txId, res, details) {
683         var callback = details.callback;
685         if (callback) {
686             callback({
687                 code: res.status,
688                 msg : res.statusText
689             }, res);
690         }
691     },
693     /**
694     Called when the `Y.io` request has finished successfully.
696     By default this calls the `details.callback` function passing it the
697     response body.
699     @method _onSyncIOSuccess
700     @param {String} txId The `Y.io` transaction id.
701     @param {Object} res The `Y.io` response object.
702     @param {Object} details Extra details carried through from `sync()`:
703       @param {String} details.action The sync action performed.
704       @param {Function} [details.callback] The function to call after syncing.
705       @param {String} details.url The URL of the requested resource.
706     @protected
707     @since 3.6.0
708     **/
709     _onSyncIOSuccess: function (txId, res, details) {
710         var callback = details.callback;
712         if (callback) {
713             callback(null, res);
714         }
715     },
717     /**
718     Called when the `Y.io` request is made.
720     This is a no-op by default, but provides a hook for overriding.
722     @method _onSyncIOStart
723     @param {String} txId The `Y.io` transaction id.
724     @param {Object} details Extra details carried through from `sync()`:
725       @param {String} detials.action The sync action performed.
726       @param {Function} [details.callback] The function to call after syncing.
727       @param {String} details.url The URL of the requested resource.
728     @protected
729     @since 3.6.0
730     **/
731     _onSyncIOStart: function (txId, details) {}
734 // -- Namespace ----------------------------------------------------------------
736 Y.namespace('ModelSync').REST = RESTSync;
739 }, '3.7.1', {"requires": ["model", "io-base", "json-stringify"]});