Skip to content

Commit 9206dc0

Browse files
author
Amiel Martin
committed
Initial path templates proposal
1 parent cf9b423 commit 9206dc0

File tree

1 file changed

+156
-0
lines changed

1 file changed

+156
-0
lines changed

active/0000-path-templates.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# - Start Date: 2014-08-20
2+
# - RFC PR:
3+
# - Ember Issue:
4+
5+
## Summary
6+
7+
`pathTemplate` improves the extesibility of API endpoint urls in `RESTAdapter`.
8+
9+
## Motivation
10+
11+
I think that Ember Data has a reputation for being hard to configure. I've often
12+
heard it recommended to design the server API around what Ember Data expects.
13+
Considering a lot of thought has gone in to the default RESTAdapter API, this
14+
is sound advice. However, this is a false and damaging reputation. The adapter
15+
and serializer pattern that Ember Data uses makes it incredibly extensible. The
16+
barrier of entry is high though, and it's not obvious how to get the url you need
17+
unless it's [a namespace](http://emberjs.com/guides/models/connecting-to-an-http-server/#toc_url-prefix)
18+
or something [pathForType](http://emberjs.com/guides/models/customizing-adapters/#toc_path-customization)
19+
can handle. Otherwise it's "override `buildURL`". `RESTSerializer` was recently
20+
improved to make handling various JSON structures easier; it's time for url
21+
configuration to be easy too.
22+
23+
## Detailed Design
24+
25+
`buildURL` and associated methods and properties will be moved to a mixin design
26+
to handle url generation only. `buildURL` will use templates to generate a URL
27+
instead of manually assembling parts. Simple usage example:
28+
29+
```javascript
30+
export default DS.RESTAdapter.extend({
31+
namespace: 'api/v1',
32+
pathTemplate: '/:namespace/posts/:id'
33+
});
34+
```
35+
36+
### Resolving template segments
37+
38+
Each dynamic path segment will be resolved on a singleton object based on a `pathSegments`
39+
object provided in the adapter. This `pathSegments` object will feel similar to defining
40+
actions on routes and controllers.
41+
42+
```javascript
43+
// adapter
44+
export default DS.RESTAdapter.extend({
45+
namespace: 'api/v1',
46+
pathTemplate: '/:namespace/posts/:post_id/:category_name/:id',
47+
48+
pathSegments: {
49+
category_name: function(record) {
50+
return _pathForCategory(record.get('category'));
51+
}
52+
53+
post_id: function(record) {
54+
return record.get('post.id');
55+
};
56+
}
57+
});
58+
```
59+
60+
#### Psuedo implementation
61+
62+
```javascript
63+
RESTAdapter = Adapter.extend({
64+
buildURL: function(type, id, record) {
65+
var urlResolver = _lookupURLResolver(type);
66+
var template = this.get('urlTemplate')
67+
var urlParts = _parseURLTemplate(template, function(name) {
68+
var fn = urlResolver.get(name);
69+
return fn(record);
70+
});
71+
72+
return this.urlPrefix(urlParts.compact().join('/'));
73+
}
74+
});
75+
76+
function _lookupURLResolver(type) {
77+
// a singleton object will be created using pathSegments that includes important
78+
// adapter attributes (such as namespace and host) and delegates unknown
79+
// properties to the record, with something like:
80+
//
81+
// unknownProperty: function(key) {
82+
// return function(record) { return record.get(key); };
83+
// }
84+
};
85+
86+
// This is the simplest
87+
function _parseURLTemplate(template, fn) {
88+
var parts = template.split('/');
89+
return parts.map(function(part) {
90+
if (_isDynamicSegment(part)) { // if segment starts with a `:`
91+
return fn(_segmentName(part)); // strip off the `:`
92+
} else {
93+
return part;
94+
}
95+
});
96+
};
97+
```
98+
99+
### Different URL templates per action
100+
101+
Configuring different urls for different actions becomes fairly trivial with this
102+
system in place:
103+
104+
#### Usage
105+
106+
```javascript
107+
// adapter
108+
export default DS.RESTAdapter.extend({
109+
namespace: 'api/v1',
110+
pathTemplate: '/:namespace/posts/:post_id/:id',
111+
createPathTemplate: '/:namespace/posts/:post_id/comments',
112+
113+
pathSegments: {
114+
post_id: function(record) {
115+
return record.get('post.id');
116+
};
117+
}
118+
});
119+
```
120+
121+
#### Psuedo implementation
122+
123+
```javascript
124+
RESTAdapter = Adapter.extend({
125+
createRecord: function(store, type, payload) {
126+
// ...
127+
var url = this.buildURL(type.typeKey, null, record, 'create');
128+
return this.ajax(url, "POST", { data: data });
129+
},
130+
131+
buildURL: function(type, id, record, action) {
132+
var template = this.get(action + 'UrlTemplate') || this.get('urlTemplate);
133+
// ...
134+
}
135+
});
136+
```
137+
138+
## Drawbacks
139+
140+
* Building URLs in this way is likely to be less performant. If this proposal is
141+
generally accepted, I will run benchmarks.
142+
143+
## Alternatives
144+
145+
The main alternative that comes to mind, that would make it easier to configure
146+
urls in the adapter, would be to generally simplify `buildURL` and create more
147+
hooks.
148+
149+
## Unresolved Questions
150+
151+
* How many templates are reasonable? There could be templates for different
152+
operations such as `createRecord`, `updateRecord`, but also `findQuery`, etc.
153+
* How do we handle generating urls for actions that do not have a single
154+
record? This includes `findAll`, `findQuery`, which have no record, and
155+
`findMany`, which has a collection of records.
156+

0 commit comments

Comments
 (0)