Created
September 26, 2012 16:27
-
-
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!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*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