Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Improve how ModelSync.REST handles URLs and remove ambiguity.

The `url` property is now _always_ a string, instead of being either a
string or a function. The `_getURL()` helper method has been promoted to
a public method, now named: `getURL()`. This method provides the guts of
figuring out what URL to generate/use based on convention.

An additional helper method, `_subtituteURL()`, has been created to make
it easier for developers to override `getURL()` while still being able
to use the URL substitution feature.
  • Loading branch information...
commit 41265dc389f7f109f52d94e813fdf4efc8c7fa9e 1 parent 48a7509
@ericf authored
View
242 src/app/js/model-extensions/model-sync-rest.js
@@ -147,12 +147,12 @@ Model or ModelList constructor.
@property _NON_ATTRS_CFG
@type Array
-@default ["url"]
+@default ["root", "url"]
@static
@protected
@since 3.6.0
**/
-RESTSync._NON_ATTRS_CFG = ['url'];
+RESTSync._NON_ATTRS_CFG = ['root', 'url'];
RESTSync.prototype = {
@@ -191,98 +191,152 @@ RESTSync.prototype = {
root: '',
/**
- A string which specifies the URL to use when making XHRs, or a function
- which will generate the URLs.
+ A string which specifies the URL to use when making XHRs, if not value is
+ provided, the URLs used to make XHRs will be generated by convention.
- While a `url` can be provided for each Model/ModelList instnace, usually
- you'll want to instead provide a function or string-pattern on the prototype
- which can be used for all instances.
+ While a `url` can be provided for each Model/ModelList instance, usually
+ you'll want to either rely on the default convention or provide a tokenized
+ string on the prototype which can be used for all instances.
- If the `url` property is a function, it should return the string that should
- be used as the URL. Function values will be called before each request and
- will be passed the sync `action` which is currently being performed.
+ When sub-classing `Y.Model`, you will probably be able to rely on the
+ default convention of generating URLs in conjunction with the `root`
+ property and whether the model is new or not (i.e. has an `id`). If the
+ `root` property ends with a trailing-slash, the generated URL for the
+ specific model will also end with a trailing-slash.
- If the `url` property is a string, it will be processed by `Y.Lang.sub()`,
- which is useful when the URLs for a Model/ModelList subclass match a
- specific pattern and can use simple replacement tokens; e.g.:
+ @example
+ Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
+ root: '/users/'
+ });
+
+ var currentUser, newUser;
+
+ // GET the user data from: "/users/123/"
+ currentUser = new Y.User({id: '123'}).load();
+
+ // POST the new user data to: "/users/"
+ newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
+
+ If a `url` is specified, it will be processed by `Y.Lang.sub()`, which is
+ useful when the URLs for a Model/ModelList subclass match a specific pattern
+ and can use simple replacement tokens; e.g.:
@example
- var User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
- url: '/users/{id}'
+ Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
+ root: '/users',
+ url : '/users/{username}'
});
**Note:** String substitution of the `url` property will only happen for
- `Y.Model` subclasses, and only string and number attribute values will be
+ Model subclasses, and only string and number attribute values will be
substituted. Do not expect something fancy to happen with Object, Array, or
Boolean values, they will simply be ignored.
- When sub-classing Y.Model, you will probably be able to rely on the default
- implementation of `url()` which works in conjunction with the `root`
- property and whether the Model instance is new or not (i.e. has an `id`). If
- the `root` property ends with a trailing-slash, the generated URL for the
- specific Model instance will also end with a trailing-slash.
-
If your URLs have plural roots or collection URLs, while the specific item
resources are under a singular name, e.g. "/users" (plural) and "/user/123"
(singular), you'll probably want to configure the `root` and `url`
properties like this:
@example
- var User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
+ Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
root: '/users',
url : '/user/{id}'
});
- var myUser = new User({id: '123'});
- myUser.load(); // Will GET the User data from: /user/123
+ var currentUser, newUser;
- var newUser = new User({name: 'Eric Ferraiuolo'});
- newUser.save(); // Will POST the User data to: /users
+ // GET the user data from: "/user/123"
+ currentUser = new Y.User({id: '123'}).load();
+
+ // POST the new user data to: "/users"
+ newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
+
+ When sub-classing `Y.ModelList`, usually you'll be able to rely on the
+ associated `model` to supply its `root` to be used as the model list's URL.
+ If this needs to be customized, you can provide a simple string for the
+ `url` property.
- When sub-classing `Y.ModelList`, usually you'll only need to specify a
- simple string for the `url` property and leave `root` to be the default
- value.
+ @example
+ Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
+ // Leverages `Y.User`'s `root`, which is "/users".
+ model: Y.User
+ });
+
+ // Or specified explicitly...
+
+ Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
+ model: Y.User,
+ url : '/users'
+ });
@property url
- @type Function|String
+ @type String
+ @default ""
+ @since 3.6.0
+ **/
+ url: '',
+
+ // -- Lifecycle Methods ----------------------------------------------------
+
+ initializer: function (config) {
+ config || (config = {});
+
+ // Use instance-level values passed into the constructor and default
+ // falsy values to empty strings.
+ ('root' in config) && (this.root = config.root || '');
+ ('url' in config) && (this.url = config.url || '');
+ },
+
+ // -- Public Methods -------------------------------------------------------
+
+ /**
+ Returns the URL for this model or model list for the given `action`, if
+ specified.
+
+ This method correctly handles the variations of `root` and `url` values and
+ is used by the `sync()` method to get the URLs used to make the XHRs.
+
+ You can override this method if you need to provide a specific
+ implementation for how the URLs of your Model and ModelList subclasses need
+ to be generated.
+
+ @method getURL
+ @param {String} [action] Optional `sync()` action for which to generate the
+ URL.
+ @return {String} this model's or model list's URL for the the given
+ `action`, if specified.
@since 3.6.0
**/
- url: function () {
+ getURL: function (action) {
var root = this.root,
- url;
+ url = this.url;
- // A model list's `url` should default to its `root` or its `model`'s
- // `root`. By convention the a model's `root` locates a collection/list.
+ // If this is a model list, use its `url`, but default to the `root`
+ // of its `model`. By convention a model's `root` is the location to a
+ // collection resource.
if (this._isYUIModelList) {
- return root || this.model.prototype.root;
+ return url || this.model.prototype.root;
}
- // By convention a model's `root` locates a collection/list and when
- // creating a new representation on the server no resource exits for it
- // yet, therefore the collection URL is used.
- if (this.isNew()) {
+ // When a model is new, i.e. has no `id`, the `root` should be used. By
+ // convention a model's `root` is the location to a collection resource.
+ // The model's `url` will be used as a fallback if `root` isn't defined.
+ if (root && (this.isNew() || action === 'create')) {
return root;
}
- url = this.getAsURL('id');
-
- if (root && root.charAt(root.length - 1) === '/') {
- // Add trailing-slash because root has a trailing-slash.
- url += '/';
+ // When a model's `url` is not defined, we'll generate a URL to use by
+ // convention. This will combine the model's `id` with its configured
+ // `root` and add a trailing-slash if the root ends with "/".
+ if (!url) {
+ return this._joinURL(this.getAsURL('id') || '');
}
- return this._joinURL(url);
- },
-
- // -- Lifecycle Methods ----------------------------------------------------
-
- initializer: function (config) {
- config || (config = {});
- Lang.isValue(config.url) && (this.url = config.url);
+ // Substitute placeholders in the `url` with the model's URL-encoded
+ // attribute values.
+ return this._substituteURL(url);
},
- // -- Public Methods -------------------------------------------------------
-
/**
Serializes `this` model to be used as the HTTP request entity body.
@@ -339,7 +393,7 @@ RESTSync.prototype = {
sync: function (action, options, callback) {
options || (options = {});
- var url = this._getURL(action),
+ var url = this.getURL(action),
method = RESTSync.HTTP_METHODS[action],
headers = Y.merge(RESTSync.HTTP_HEADERS, options.headers),
timeout = options.timeout || RESTSync.HTTP_TIMEOUT,
@@ -401,42 +455,6 @@ RESTSync.prototype = {
// -- Protected Methods ----------------------------------------------------
/**
- Helper method to return the URL to use when making the XHR to the server.
-
- This method correctly handles variations of the `url` property/method.
-
- @method _getURL
- @param {String} action Sync action to perform.
- @return {String} the URL for the XHR.
- @protected
- @since 3.6.0
- **/
- _getURL: function (action) {
- var url = this.url,
- data;
-
- if (Lang.isFunction(url)) {
- return this.url(action);
- }
-
- if (this._isYUIModel) {
- data = {};
-
- Y.Object.each(this.getAttrs(), function (v, k) {
- if (Lang.isString(v) || Lang.isNumber(v)) {
- // URL-encode any string or number values.
- data[k] = encodeURIComponent(v);
- }
- });
-
- // Substitute placeholders with the URL-encoded data values.
- url = Lang.sub(url, data);
- }
-
- return url || this.root;
- },
-
- /**
Joins the `root` URL to the specified `url`, normalizing leading/trailing
"/" characters.
@@ -446,8 +464,8 @@ RESTSync.prototype = {
model._joinURL('/bar'); // => '/foo/bar'
model.root = '/foo/'
- model._joinURL('bar'); // => '/foo/bar'
- model._joinURL('/bar'); // => '/foo/bar'
+ model._joinURL('bar'); // => '/foo/bar/'
+ model._joinURL('/bar'); // => '/foo/bar/'
@method _joinURL
@param {String} url URL to append to the `root` URL.
@@ -458,13 +476,47 @@ RESTSync.prototype = {
_joinURL: function (url) {
var root = this.root;
+ if (!(root || url)) {
+ return '';
+ }
+
if (url.charAt(0) === '/') {
url = url.substring(1);
}
+ // Combines the `root` with the `url` and adds a trailing-slash if the
+ // `root` has a trailing-slash.
return root && root.charAt(root.length - 1) === '/' ?
- root + url :
+ root + url + '/' :
root + '/' + url;
+ },
+
+ /**
+
+ @method _substituteURL
+ @param {String} url Tokenized URL string to substitute placeholder values.
+ @return {String} Substituted URL.
+ @protected
+ @since 3.6.0
+ **/
+ _substituteURL: function (url) {
+ if (!url) {
+ return '';
+ }
+
+ var data = {};
+
+ // Creates a hash of URL-encoded values for a model's string and number
+ // attributes. These values are then used to replace any placeholders in
+ // a tokenized `url`.
+ Y.Object.each(this.getAttrs(), function (v, k) {
+ if (Lang.isString(v) || Lang.isNumber(v)) {
+ // URL-encode any string or number values.
+ data[k] = encodeURIComponent(v);
+ }
+ });
+
+ return Lang.sub(url, data);
}
};
View
69 src/app/tests/model-sync-rest-test.js
@@ -54,7 +54,7 @@ modelSyncRESTSuite.add(new Y.Test.Case({
delete this.TestModelList;
},
- '`root` property should have a default value' : function () {
+ '`root` property should be an empty string by default' : function () {
var model = new this.TestModel();
Assert.areSame('', model.root);
@@ -62,12 +62,12 @@ modelSyncRESTSuite.add(new Y.Test.Case({
Assert.areSame('', modelList.root);
},
- '`url` should be a function by default' : function () {
+ '`url` property should be an empty string by default' : function () {
var model = new this.TestModel();
- Assert.isTrue(Y.Lang.isFunction(model.url));
+ Assert.areSame('', model.url);
var modelList = new this.TestModelList();
- Assert.isTrue(Y.Lang.isFunction(modelList.url));
+ Assert.areSame('', modelList.url);
}
}));
@@ -87,59 +87,59 @@ modelSyncRESTSuite.add(new Y.Test.Case({
delete this.TestModelList;
},
- '_getURL() should return a String' : function () {
+ 'getURL() should return a String' : function () {
var model = new this.TestModel();
- Assert.isString(model._getURL());
+ Assert.isString(model.getURL());
var modelList = new this.TestModelList();
- Assert.isString(modelList._getURL());
+ Assert.isString(modelList.getURL());
},
- '_getURL() should return locally set `url` property' : function () {
+ 'getURL() should return locally set `url` property' : function () {
var model = new this.TestModel({ url: '/model/123' });
- Assert.areSame('/model/123', model._getURL());
+ Assert.areSame('/model/123', model.getURL());
model.url = '/model/abc';
- Assert.areSame('/model/abc', model._getURL());
+ Assert.areSame('/model/abc', model.getURL());
var modelList = new this.TestModelList({ url: '/model' });
- Assert.areSame('/model', modelList._getURL());
+ Assert.areSame('/model', modelList.getURL());
modelList.url = '/models';
- Assert.areSame('/models', modelList._getURL());
+ Assert.areSame('/models', modelList.getURL());
},
- '_getURL() should substitute placeholder values of Models’ `url`' : function () {
+ 'getURL() should substitute placeholder values of Models’ `url`' : function () {
var model = new this.TestModel({
id : 123,
url: '/model/{id}/'
});
- Assert.areSame('/model/123/', model._getURL());
+ Assert.areSame('/model/123/', model.getURL());
model.addAttr('foo', { value: 'bar' });
model.url = '/{foo}/{id}';
- Assert.areSame('/bar/123', model._getURL());
+ Assert.areSame('/bar/123', model.getURL());
},
- '_getURL() should not substitute placeholder values of ModelLists’ `url`' : function () {
+ 'getURL() should not substitute placeholder values of ModelLists’ `url`' : function () {
var modelList = new this.TestModelList({ url: '/{foo}/' });
modelList.addAttr('foo', { value: 'bar' });
Assert.areSame('bar', modelList.get('foo'));
- Assert.areSame('/{foo}/', modelList._getURL());
+ Assert.areSame('/{foo}/', modelList.getURL());
},
- '_getURL() should URL-encode the substitutions of placeholder values of Models’ `url`' : function () {
+ 'getURL() should URL-encode the substitutions of placeholder values of Models’ `url`' : function () {
var model = new this.TestModel({
id : '123 456',
url: '/model/{id}'
});
- Assert.areSame('/model/123%20456', model._getURL());
+ Assert.areSame('/model/123%20456', model.getURL());
},
- '_getURL() should not substitute Arrays, Objects, or Boolean values of Models’ `url`' : function () {
+ 'getURL() should not substitute Arrays, Objects, or Boolean values of Models’ `url`' : function () {
var model = new this.TestModel({
id : 'asdf',
url: '/model/{foo}/{bar}/{baz}/{id}'
@@ -151,44 +151,43 @@ modelSyncRESTSuite.add(new Y.Test.Case({
baz : {value: true}
});
- Assert.areSame('/model/{foo}/{bar}/{baz}/asdf', model._getURL());
+ Assert.areSame('/model/{foo}/{bar}/{baz}/asdf', model.getURL());
},
- '_getURL() should return `root` if `url` is falsy' : function () {
+ 'getURL() should return `root` if `url` is falsy' : function () {
var model = new this.TestModel();
model.root = '/model/';
model.url = '';
- Assert.areSame('/model/', model._getURL());
+ Assert.areSame('/model/', model.getURL());
},
- 'url() should return `root` if ModelList or Model is new' : function () {
+ 'getURL() should return `root` if the Model is new' : function () {
var model = new this.TestModel();
model.root = '/model';
- Assert.areSame(model.root, model.url());
-
- var modelList = new this.TestModelList();
- modelList.root = '/model';
- Assert.areSame(modelList.root, modelList.url());
+ model.url = '/foo';
+ Assert.areSame(model.root, model.getURL());
},
- 'url() should return a URL that ends with a / only if Model’s `root` ends with a /' : function () {
+ 'getURL() should return a URL that ends with a / only if Model’s `root` ends with a /' : function () {
var model = new this.TestModel({id: 123});
model.root = '/model';
- Assert.areSame('/model/123', model.url());
+ Assert.areSame('/model/123', model.getURL());
model.root = '/model/';
- Assert.areSame('/model/123/', model.url());
+ Assert.areSame('/model/123/', model.getURL());
},
- 'url() should return a URL determined from the sync action' : function () {
+ 'getURL() should return a URL determined from the sync action' : function () {
var model = new this.TestModel({id: 123});
- model.url = function(action) { return '/model/' + action; };
+ model.getURL = function (action) {
+ return '/model/' + action;
+ };
- Assert.areSame('/model/read', model._getURL('read'));
+ Assert.areSame('/model/read', model.getURL('read'));
},
'serialize() can modify the data' : function () {
Please sign in to comment.
Something went wrong with that request. Please try again.