Skip to content

Instantly share code, notes, and snippets.

@eykd
Created September 26, 2012 16:27
Show Gist options
  • Save eykd/3789008 to your computer and use it in GitHub Desktop.
Save eykd/3789008 to your computer and use it in GitHub Desktop.
API Client code for getting Backbone to talk with Tastypie. Over-engineering at it's finest!
/*global Backbone:false, $:false, _:false, extend:false */
// Copyright (c) 2012, David Eyk for Good News Publishers
// All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the
// distribution.
// Neither the name of the <ORGANIZATION> nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.
(function () {
var Resource, APIClient, syncAPI, syncParseList,
_updateResponse,
endpoint_tmpl;
endpoint_tmpl = _.template("<%= (typeof base_url !== 'undefined') ? base_url : '/' %><%= resource %>/<%= (typeof pk !== 'undefined') ? pk + '/' : '' %>");
_updateResponse = function (cumulative, response) {
var cmeta = cumulative.meta,
cobj = cumulative.objects,
rmeta = response.meta,
robj = response.objects;
cmeta.limit += rmeta.limit;
cmeta.total_count += robj.length;
Array.prototype.push.apply(cobj, robj);
};
// A remote resource of the API.
Resource = function (client, resource_name) {
this.client = client;
this.resource = encodeURIComponent(resource_name);
};
_.extend(Resource.prototype, {
_dispatch_detail: function (method, pk, options) {
options = options || {};
if (_.isArray(pk)) {
pk = _(pk).map(function (pk) {
return encodeURIComponent(pk).replace(/%2B|%20/, '+');
}).join('/');
}
else {
pk = encodeURIComponent(pk).replace(/%2B|%20/, '+');
}
return this.client.send(
method,
{resource: this.resource, pk: pk},
options);
},
_dispatch_list: function (method, options) {
options = options || {};
return this.client.send(
method,
{resource: this.resource, pk: undefined},
options);
},
// Retrieve a remote resource query.
// Returns a $.Deferred.promise() object.
get_list: function (options) {
return this._dispatch_list('GET', options);
},
// Retrieve a remote resource, by primary key, as a JSON object.
// Returns a $.Deferred.promise() object.
get_detail: function (pk, options) {
return this._dispatch_detail('GET', pk, options);
},
// Post to a remote resource.
// Returns a $.Deferred.promise() object.
post_list: function (options) {
return this._dispatch_list('POST', options);
},
// Post to a remote resource.
// Returns a $.Deferred.promise() object.
post_detail: function (pk, options) {
return this._dispatch_detail('POST', pk, options);
},
// Put to a remote resource.
// Returns a $.Deferred.promise() object.
put_detail: function (pk, options) {
return this._dispatch_detail('PUT', pk, options);
},
// Put to a remote resource.
// Returns a $.Deferred.promise() object.
put_list: function (options) {
return this._dispatch_list('PUT', options);
},
// Patch a remote resource.
// Returns a $.Deferred.promise() object.
patch_detail: function (pk, options) {
return this._dispatch_detail('PATCH', pk, options);
},
// Patch a remote resource.
// Returns a $.Deferred.promise() object.
patch_list: function (options) {
return this._dispatch_list('PATCH', options);
},
// Delete a remote resource.
// Returns a $.Deferred.promise() object.
delete_detail: function (pk, options) {
return this._dispatch_detail('DELETE', pk, options);
},
// Delete a remote resource.
// Returns a $.Deferred.promise() object.
delete_list: function (options) {
return this._dispatch_list('DELETE', options);
}
}),
// A simple client for interacting with the API.
APIClient = function (options) {
options = options || {};
this.api_key = options.api_key;
this.base_url = options.base_url;
this.user_auth = options.user_auth;
this._resources = {};
};
_.extend(APIClient.prototype, {
endpoint_tmpl: endpoint_tmpl,
// Return the headers necessary to make an API call.
headers: function () {
var h = {
'Accept': 'application/json'
};
if (this.api_key) {
h['X-APIKey'] = this.api_key;
}
if (this.user_auth) {
h['X-UserAuth'] = this.user_auth;
}
return h;
},
// Return a Resource object for interacting with the remote API resource.
resource: function (resource_name) {
if (_.has(this._resources, resource_name)) {
return this._resources[resource_name];
} else {
var resource = this._resources[resource_name] = new Resource(this, resource_name);
return resource;
}
},
// Sends a request to the remote endpoint with the specified HTTP method.
// Returns a $.Deferred.promise() object.
send: function (method, endpoint_ctx, options) {
endpoint_ctx = endpoint_ctx || {};
endpoint_ctx.base_url = endpoint_ctx.base_url || this.base_url || '/';
options = options || {};
options.headers = _.extend({}, this.headers(), options.headers || {});
options.cache = (options.cache === undefined) ? true : options.cache;
options.url = options.url || this.endpoint_tmpl(endpoint_ctx);
options.type = method;
var that = this,
success = new $.Deferred(),
objects = [],
final_result = {
meta: {limit: 0, next: null, offset: 0, previous: null, total_count: 0},
objects: objects
};
function resolve (resp, status, xhr) {
success.resolveWith(options.context || options, [resp, status, xhr]);
}
function reject (xhr, status, error) {
success.rejectWith(options.context || options, [xhr, status, error]);
}
$.ajax(options)
.done(function (resp, status, xhr) {
if (!_.isEmpty(resp) && resp.meta && options.auto_page) {
// Handle auto-paged content by recursively getting the next page, and
// accumulating the results in final_result.
_updateResponse(final_result, resp);
if (resp.meta.next) {
// Get the next page.
var next_options = _.extend({}, options);
next_options.data = _.extend({}, options.data, {offset: resp.meta.offset + resp.meta.limit});
that.send(method, endpoint_ctx, next_options)
.done(function (resp, status, xhr) {
_updateResponse(final_result, resp);
resolve(final_result, status, xhr);
});
}
else {
// Resolve with accumulated objects.
resolve(final_result, status, xhr);
}
} else if (_.isEmpty(resp) && options.do_get_on_empty_response &&
xhr.getResponseHeader('Location') &&
(xhr.status === 201 || xhr.status === 202 || xhr.status === 204)) {
// 201 CREATED, 202 ACCEPTED or 204 NO CONTENT; response null or empty.
var get_options = _.extend({}, options);
get_options.url = xhr.getResponseHeader('Location');
that.send('GET', null, get_options)
.done(function (resp, status, xhr) {
resolve(resp, status, xhr);
})
.fail(reject);
} else {
// Handle everything else by simply resolving.
resolve(resp, status, xhr);
}
})
.fail(reject);
return success;
}
});
// Backbone.sync replacement for working with the API.
syncAPI = function (method, model, options) {
var client, resource, query_type, api_resource, id_attr, pk, success, result;
// Retrieve an APIClient instance.
client = (
options.client ||
model.client ||
(model.collection && model.collection.client) ||
undefined);
if (client === undefined) {
throw new Error('An API client must be set on the model, collection, or options.');
}
if (_.isFunction(client)) {
client = client.apply(model);
}
// Retrieve the resource we're concerned with in this operation.
resource = (
options.resource ||
model.resource ||
(model.collection && model.collection.resource) ||
undefined);
if (resource === undefined) {
throw new Error('An API resource must be set on the model, collection, or options.');
}
if (_.isFunction(resource)) {
resource = resource.apply(model);
}
api_resource = client.resource(resource);
id_attr = (
options.id_attribute ||
model.id_attribute ||
(model.collection && model.collection.id_attribute) ||
undefined
);
if (_.isFunction(id_attr)) {
pk = id_attr.apply(model);
}
else {
pk = model.get(id_attr);
}
query_type = (typeof pk === 'undefined') ? 'list' : 'detail';
success = options.success;
options.success = undefined;
// Ensure that we have the appropriate request data.
if (!options.data && model && (method === 'create' || method === 'update')) {
options.contentType = 'application/json';
options.data = JSON.stringify(model.toJSON());
}
if (options.do_get_on_empty_response === undefined) {
options.do_get_on_empty_response = true;
}
if (query_type === 'list') {
// List request
switch (method) {
case "create":
result = api_resource.post_list(options);
break;
case "update":
result = api_resource.put_list(options);
break;
case "delete":
result = api_resource.delete_list(options);
break;
default: // "read":
result = api_resource.get_list(options);
break;
}
}
else {
// Detail request
switch (method) {
case "create":
result = api_resource.post_detail(pk, options);
break;
case "update":
result = api_resource.put_detail(pk, options);
break;
case "delete":
result = api_resource.delete_detail(pk, options);
break;
default: // "read":
result = api_resource.get_detail(pk, options);
break;
}
}
return $.when(result).done(success).promise();
};
// Backbone.Collection.parse replacement for working with data returned from API.
syncParseList = function (data) {
// Save a reference to the list metadata
if (data && data.meta) {
this.meta = data.meta;
}
return data && data.objects;
};
// Populate custom Backbone extensions.
if (typeof Backbone !== undefined) {
Backbone.APICollection = Backbone.Collection.extend({
sync: syncAPI,
parse: syncParseList,
resource: null
});
Backbone.APIModel = Backbone.Model.extend({
sync: syncAPI,
resource: null,
// Pluck the pk out of the resource_uri, if we have one.
id_attribute: function () {
var resource_uri = this.get('resource_uri');
return (resource_uri === undefined) ?
resource_uri
:
_(this.get('resource_uri').split( '/' )).chain().compact().last().value();
}
});
}
// Populate the api object.
extend('api', {
APIClient: APIClient,
syncParseList: syncParseList,
syncAPI: syncAPI
});
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment