urlTemplate
improves the extesibility of API endpoint urls in RESTAdapter
.
I think that Ember Data has a reputation for being hard to configure. I've often
heard it recommended to design the server API around what Ember Data expects.
Considering a lot of thought has gone in to the default RESTAdapter API, this
is sound advice. However, this is a false and damaging reputation. The adapter
and serializer pattern that Ember Data uses makes it incredibly extensible. The
barrier of entry is high though, and it's not obvious how to get the url you need
unless it's a namespace
or something pathForType
can handle. Otherwise it's "override buildURL
". RESTSerializer
was recently
improved to make handling various JSON structures easier; it's time for url
configuration to be easy too.
buildURL
and associated methods and properties will be moved to a mixin design
to handle url generation only. buildURL
will use templates to generate a URL
instead of manually assembling parts. Simple usage example:
export default DS.RESTAdapter.extend({
namespace: 'api/v1',
pathTemplate: ':namespace/:type/:id'
});
Each segment (starting with :
), will be resolved by trying a number of strategies:
- Special case
:id
to use theid
argument passed tobuildURL
. - Get the attribute from the
record
argument passed tobuildURL
(record.get(segment)
). - Get the attribute from the adapter (
this.get(segment)
). - If the current result is a function, call it with the arguments from
buildURL
(segmentValue(type, id, record)
).
Example:
export default DS.RESTAdapter.extend({
namespace: 'api/v1',
pathTemplate: ':namespace/:parent_id/:category/:id',
category: function(type, id, record) {
return _pathForCategory(record.get('category'));
}
});
function _parseURLTemplate(template, fn) {
var parts = template.split('/');
return parts.map(function(part) {
if (_isDynamic(part)) {
return fn(_dynamicName(part));
} else {
return part;
}
});
};
RESTAdapter = AbstractAdapter.extend({
buildURL: function(type, id, record) {
var urlParts = _parseURLTemplate(this.get('urlTemplate'), function(name) {
var value;
if (name === 'id') return id;
value = get(record, name);
if (!value) value = get(this, name);
if ($.isFunction(value)) {
value = value(type, id, record);
}
return value;
});
return urlParts.compact().join('/');
}
});
An alternative solution could be to introduce a new object to resolve the path segments. This feels heavy-handed, but the usage ends up being very elegant.
// adapter
export default DS.RESTAdapter.extend({
namespace: 'api/v1',
pathTemplate: ':namespace/:parent_id/:category/:id',
});
// url resolver ?
export default Ember.Object.extend({ // Sure, it could be DS.URLResolver.extend
category: function() {
return _pathForCategory(record.get('category'));
}.property('record.category'),
parent_id: function() {
return record.get('parent.id');
};
});
function _parseURLTemplate(template, fn) {
var parts = template.split('/');
return parts.map(function(part) {
if (_isDynamic(part)) {
return fn(_dynamicName(part));
} else {
return part;
}
});
};
RESTAdapter = AbstractAdapter.extend({
buildURL: function(type, id, record) {
var urlResolver = _lookupURLResolver(type).create({ type: type, id: id, record: record});
var urlParts = _parseURLTemplate(this.get('urlTemplate'), function(name) {
return urlResolver.get(name);
});
return urlParts.compact().join('/');
}
});
- Building URLs in this way is likely to be less performant. If this proposal is generally accepted, I will run benchmarks.
The main alternative that comes to mind, that would make it easier to configure
urls in the adapter, would be to generally simplify buildURL
and create more
hooks.
- How many templates are reasonable? I'm starting with just
pathTemplate
to start with just the simplest case, but maybe there should be aurlTemplate: "http://:host/:namespace/:path"
, and there could also be templates for different operations such asfindAll
,findQuery
,findHasMany
,findBelongsTo
, etc.