rendr
Version:
Render your Backbone.js apps on the client and the server.
171 lines (138 loc) • 4.32 kB
JavaScript
var DataAdapter = require('./index'),
utils = require('../utils'),
_ = require('underscore'),
url = require('url'),
request,
debug = require('debug')('rendr:RestAdapter'),
util = require('util'),
qs = require('qs2');
module.exports = RestAdapter;
function RestAdapter(options) {
DataAdapter.call(this, options);
/**
* Default options.
*/
_.defaults(this.options, {
userAgent: 'Rendr RestAdapter; Node.js'
});
request = this.options.request || require('request');
}
util.inherits(RestAdapter, DataAdapter);
/**
* `request`
*
* This is method that Rendr calls to ask for data. In this case, we override
* it to speak basic REST using HTTP & JSON. This is good for consuming an
* existing RESTful API that exists externally to your Node app.
*
* `req`: Actual request object from Express/Connect.
* `api`: Object describing API call; properties including 'path', 'query', etc.
* Passed to `url.format()`.
* `options`: (optional) Options.
* `callback`: Callback.
*/
RestAdapter.prototype.request = function(req, api, options, callback) {
/**
* Allow for either 3 or 4 arguments; `options` is optional.
*/
if (arguments.length === 3) {
callback = options;
options = {};
}
/**
* Do a shallow copy of options, and provide some default values.
*/
options = _.defaults({}, options, {
convertErrorCode: true,
allow4xx: false
});
/**
* Get defaults for the `api` object.
*/
api = this.apiDefaults(api, req);
/**
* Request timing.
*/
var start = new Date().getTime(),
end;
/**
* Make the request. The `api` object is passed into the `request` library.
*/
request(api, function(err, response, body) {
if (err) return callback(err);
end = new Date().getTime();
debug('%s %s %s %sms', api.method.toUpperCase(), api.url, response.statusCode, end - start);
/**
* If specified by options, convert an i.e. 5xx HTTP response to an error.
*/
if (options.convertErrorCode) {
err = this.getErrForResponse(response, {allow4xx: options.allow4xx});
}
/**
* Attempt to parse as JSON, if it looks like JSON.
*/
if (typeof body === 'string' && this.isJSONResponse(response)) {
try {
body = JSON.parse(body);
} catch (e) {
err = e;
}
}
callback(err, response, body);
}.bind(this));
};
RestAdapter.prototype.isJSONResponse = function(response) {
var contentType = response.headers['content-type'] || '';
return contentType.indexOf('application/json') !== -1;
};
RestAdapter.prototype.apiDefaults = function(api, req) {
var urlOpts, apiHost;
api = _.clone(api);
// If path contains a protocol, assume it's a URL.
if (api.path && ~api.path.indexOf('://')) {
api.url = url.format({ pathname: api.path });
delete api.path;
}
// Can specify a particular API to use, falling back to default.
apiHost = this.options[api.api] || this.options['default'] || this.options;
urlOpts = _.defaults(
_.pick(api, ['protocol', 'port']),
_.pick(apiHost, ['protocol', 'port', 'host', 'hostname'])
);
urlOpts.pathname = api.path || api.pathname;
_.defaults(api, {
method: 'GET',
url: url.format(urlOpts),
headers: {}
});
if (api.query && !_.isEmpty(api.query)) {
api.url += '?' + qs.stringify(api.query);
}
// Add a default UserAgent, so some servers don't reject our request.
if (api.headers['User-Agent'] == null) {
api.headers['User-Agent'] = this.options.userAgent;
}
// make it json, but only if content-type is empty or 'application/json'
if (api.body != null && (!api.headers['Content-Type'] || api.headers['Content-Type'] == 'application/json')) {
api.json = api.body;
}
// Remove entity body for GET requests if body is empty object
if (api.method === 'GET' && Object.keys(api.body).length === 0) {
delete api.json;
delete api.body;
}
return api;
};
/**
* Convert 4xx, 5xx responses to be errors.
*/
RestAdapter.prototype.getErrForResponse = function(res, options) {
var status = +res.statusCode,
err = null;
if (utils.isErrorStatus(status, options)) {
err = new Error(status + " status");
err.status = status;
err.body = res.body;
}
return err;
};