Skip to content

Instantly share code, notes, and snippets.

@amiel
Last active August 29, 2015 14:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amiel/9b6d273d7cb3e6e1456b to your computer and use it in GitHub Desktop.
Save amiel/9b6d273d7cb3e6e1456b to your computer and use it in GitHub Desktop.

urlTemplate Proposal

Summary

urlTemplate 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 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.

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:

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

Resolving template segments

Each segment (starting with :), will be resolved by trying a number of strategies:

  1. Special case :id to use the id argument passed to buildURL.
  2. Get the attribute from the record argument passed to buildURL (record.get(segment)).
  3. Get the attribute from the adapter (this.get(segment)).
  4. 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'));
  }
});

Psuedo-code implementation

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('/');
  }
});

Resolving template segments (alternative solution)

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');
  };
});

Psuedo-code implementation

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('/');
  }
});

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? I'm starting with just pathTemplate to start with just the simplest case, but maybe there should be a urlTemplate: "http://:host/:namespace/:path", and there could also be templates for different operations such as findAll, findQuery, findHasMany, findBelongsTo, etc.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment