Skip to content

Commit

Permalink
Initial path templates proposal
Browse files Browse the repository at this point in the history
  • Loading branch information
Amiel Martin committed Aug 20, 2014
1 parent cf9b423 commit d7a0b11
Showing 1 changed file with 157 additions and 0 deletions.
157 changes: 157 additions & 0 deletions active/0000-path-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
- Start Date: 2014-08-20
- RFC PR:
- Ember Issue:

## Summary

`pathTemplate` improves the extesibility of API endpoint urls in `RESTAdapter`.

## Motivation

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](http://emberjs.com/guides/models/connecting-to-an-http-server/#toc_url-prefix)
or something [pathForType](http://emberjs.com/guides/models/customizing-adapters/#toc_path-customization)
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.

## Detailed Design

`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:

```javascript
export default DS.RESTAdapter.extend({
namespace: 'api/v1',
pathTemplate: '/:namespace/posts/:id'
});
```

### Resolving template segments

Each dynamic path segment will be resolved on a singleton object based on a `pathSegments`
object provided in the adapter. This `pathSegments` object will feel similar to defining
actions on routes and controllers.

```javascript
// adapter
export default DS.RESTAdapter.extend({
namespace: 'api/v1',
pathTemplate: '/:namespace/posts/:post_id/:category_name/:id',

pathSegments: {
category_name: function(record) {
return _pathForCategory(record.get('category'));
}

post_id: function(record) {
return record.get('post.id');
};
}
});
```

#### Psuedo implementation

```javascript
RESTAdapter = Adapter.extend({
buildURL: function(type, id, record) {
var urlResolver = _lookupURLResolver(type);
var template = this.get('urlTemplate')
var urlParts = _parseURLTemplate(template, function(name) {
var fn = urlResolver.get(name);
return fn(record);
});

return this.urlPrefix(urlParts.compact().join('/'));
}
});

function _lookupURLResolver(type) {
// a singleton object will be created using pathSegments that includes important
// adapter attributes (such as namespace and host) and delegates unknown
// properties to the record, with something like:
//
// unknownProperty: function(key) {
// return function(record) { return record.get(key); };
// }
};

// This is the simplest
function _parseURLTemplate(template, fn) {
var parts = template.split('/');
return parts.map(function(part) {
if (_isDynamicSegment(part)) { // if segment starts with a `:`
return fn(_segmentName(part)); // strip off the `:`
} else {
return part;
}
});
};
```

### Different URL templates per action

Configuring different urls for different actions becomes fairly trivial with this
system in place:

#### Usage

```javascript
// adapter
export default DS.RESTAdapter.extend({
namespace: 'api/v1',
pathTemplate: '/:namespace/posts/:post_id/:id',
createPathTemplate: '/:namespace/comments',
hasManyTemplate: '/:namespace/posts/:post_id/comments',

pathSegments: {
post_id: function(record) {
return record.get('post.id');
};
}
});
```

#### Psuedo implementation

```javascript
RESTAdapter = Adapter.extend({
createRecord: function(store, type, payload) {
// ...
var url = this.buildURL(type.typeKey, null, record, 'create');
return this.ajax(url, "POST", { data: data });
},

buildURL: function(type, id, record, action) {
var template = this.get(action + 'UrlTemplate') || this.get('urlTemplate);
// ...
}
});
```
## Drawbacks
* Building URLs in this way is likely to be less performant. If this proposal is
generally accepted, I will run benchmarks.
## Alternatives
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.
## Unresolved Questions
* How many templates are reasonable? There could be templates for different
operations such as `createRecord`, `updateRecord`, but also `findQuery`, etc.
* How do we handle generating urls for actions that do not have a single
record? This includes `findAll` and `findQuery`, which have no record, and
`findMany` and `findHasMany` which have a collection of records.

0 comments on commit d7a0b11

Please sign in to comment.