jan
Version:
Jan is a simple library for making HTTP requests.
303 lines (278 loc) • 9.11 kB
JavaScript
/** Jan is a simple library for making HTTP requests.
*
* Jan makes it easy to issue network calls without dealing with awkward legacy API signatures.
*
* If called as a function, `jan()` is an alias of {@link module:jan.request `jan.request()`}
* @module jan
*
* @example
* <caption>Basic Usage</caption>
* // grab the library:
* require(['jan'], function(jan) {
*
* // Log requests before they go out:
* jan.on('req', function(e) {
* console.log('Request: ', e.req);
* });
*
* // Log responses when they come in:
* jan.on('res', function(e) {
* console.log('Response: ', e.res);
* });
*
* // Make a basic GET request:
* jan('/api/todos', function(err, res, body) {
* if (err) throw err;
* var names = data.map(function(todo){ return todo.name; });
* alert('ToDos: ' + names.join(', '));
* });
* });
*/
(function(root, factory) {
if (typeof define==='function' && define.amd) {
define([], factory);
}
else if (typeof module==='object' && module.exports) {
module.exports = factory();
}
else {
root.jan = factory();
}
}(this, function() {
var events = { req:[], res:[] },
methods = 'GET POST PUT DELETE HEAD OPTIONS'.split(' '),
hop = {}.hasOwnProperty;
/** Issue an HTTP request.
* @memberOf module:jan
* @name request
* @function
* @param {Object|String} options Options for the request, or a `String` `"url"` to which a GET request should be issued.
* @param {String} [opt.method=GET] HTTP method
* @param {String} [opt.url=/] The URL to request
* @param {String|FormData|Blob|ArrayBufferView} [opt.body=none] Request body, for HTTP methods that allow it. Supported types: [XMLHttpRequest#send](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#send())
* @param {Object} [opt.headers={ }] A map of request headers
* @param {String} [opt.user=none] Authentication username, if basic auth is to be used
* @param {String} [opt.pass=none] Authentication password for basic auth
* @param {Function} callback A function to call when the request has completed (error or success). Gets passed `(err, httpResponse, responseBody)`.
*
* @example
* <caption>"Kitchen Sync"</caption>
* jan({
* method : 'PUT',
* url : 'http://foo.com/bar.json',
* headers : {
* 'Content-Type' : 'application/json'
* },
* user : 'bob',
* pass : 'firebird',
* body : JSON.stringify({ key : 'value' })
* }, function(err, res, body) {
* if (err) throw err;
* console.log(res.status===204, body);
* });
*/
function jan(opt, callback, a) {
if (a) {
opt = extend(callback || {}, { url:opt });
callback = a;
}
opt = typeof opt==='string' ? {url:opt} : opt || {};
if (opt.baseUrl) {
// TODO: proper support for URL concatenation
opt.url = opt.baseUrl + opt.url;
}
opt.headers = opt.headers || {};
var xhr = new XMLHttpRequest(),
e = { xhr:xhr, req:opt };
emit('req', e);
(xhr = e.xhr).open(opt.method || 'GET', opt.url || '/', true, opt.user, opt.pass);
for (var name in opt.headers) if (hop.call(opt.headers, name)) xhr.setRequestHeader(name, opt.headers[name]);
xhr.onreadystatechange = function() {
if (xhr.readyState!==4) return;
var res = {
status : xhr.status,
error : !xhr.status ? 'Connection Error' : xhr.status>399 ? xhr.statusText : null,
headers : {},
body : xhr.responseText,
xml : xhr.responseXML,
xhr : xhr
},
hreg = /^\s*([a-z0-9_-]+)\s*\:\s*(.*?)\s*$/gim,
h = xhr.getAllResponseHeaders(),
m;
try{ res.json = JSON.parse(res.body); }catch(o){}
res.data = res.json || res.xml || res.body;
while (m=hreg.exec(h)) res.headers[m[1].toLowerCase()] = m[2];
e.res = res;
emit('res', e);
(callback || opt.callback).call(e, res.error, res, res.data);
};
xhr.send(opt.body);
}
/**Get a namespaced copy of {@link module:jan jan} where all methods are relative to a base URL.
* @function
* @name module:jan.ns
* @param {String} baseUrl A URL to which all namespaced methods should be relative
* @returns A `baseUrl`-namespaced jan interface
* @example
* // Create a namespaced API client:
* var api = jan.ns('https://example.com/api');
*
* // GET /api/images:
* api.get('/images', function(err, res, images) {
* console.log(images);
* });
*
* // Log response headers for any requests to the base URL:
* api.on('res', function(e) {
* console.log( e.res.headers );
* });
*/
jan.ns = function(uri) {
var opt = { baseUrl:uri },
ns = mapVerbs(alias(opt), opt);
ns.on = function(type, handler, a) {
jan.on(type, uri + (handler.sub ? handler : ''), a || handler);
return ns;
};
return ns;
};
/**Register a handler function to be called in response to a given type of event.
*
* Valid event types are: `req` and `res`, fired on request and response respectively.
* @function
* @name module:jan.on
* @example
* jan.on('res', function(e) {
* // e.req
* // e.res
* // e.xhr
* });
* @param {String} type An event type to observe
* @param {String|RegExp} [urlFilter] A String prefix or RegExp to filter against each event's request url
* @param {Function} handler Handler function, gets passed an Event object
*/
jan.on = function(type, handler, a) {
events[type].push(a ? function(e) {
if (handler.exec ? e.req.url.match(handler) : e.req.url.indexOf(handler)===0) {
a.call(this, e);
}
} : handler);
return jan;
};
/**Alias of {@link module:jan.request request()} that presupplies the option `method:'GET'`
* @name module:jan.get
* @function
*
* @example
* <caption>Get popular YouTube videos</caption>
* var url = 'http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&alt=json';
* jan.get(url, function(err, data) {
* if (err) throw err;
* // display video links:
* document.body.innerHTML = data.feed.entry.map(function(vid) {
* return vid.title.$t.anchor(vid.content.src); // String#neverforget
* }).join('<br>');
* });
*/
/**Alias of {@link module:jan.request request()} that presupplies the option `method:'POST'`
* @name module:jan.post
* @function
*
* @example
* <caption>Submit a contact form</caption>
* jan.post({
* url : 'http://example.com/contact-form.php',
* headers : {
* 'Content-Type' : 'application/x-www-form-encoded'
* },
* body : new FormData(document.querySelector('form'))
* }, function(err, data) {
* if (err) throw err;
* alert('Submitted: ' + data.message);
* });
*/
/** Alias of {@link module:jan.request request()} that presupplies the option `method:'PUT'`
* @name module:jan.put
* @function
*
* @example
* <caption>Update a REST resource</caption>
* jan.put({
* url : 'http://foo.com/bar.json',
* headers : { 'Content-Type':'application/json' },
* body : '{"key":"val"}'
* }, function(err, data) {
* if (err) throw err;
* console.log(data);
* });
*/
/** Alias of {@link module:jan.request request()} that presupplies the option `method:'HEAD'`
* @name module:jan.head
* @function
*
* @example
* <caption>Get headers</caption>
* jan.head('/massive.json', function(err, data, res) {
* if (err) throw err;
* console.log(res.headers);
* });
*/
/** Alias of {@link module:jan.request request()} that presupplies the option `method:'OPTIONS'`
* @name module:jan.options
* @function
*
* @example
* <caption>Get WADL XML</caption>
* jan.options('/api/v1', function(err, data, res) {
* if (err) throw err;
* console.log(res.headers, res.body);
* });
*/
/** Alias of {@link module:jan.request request()} that presupplies the option `method:'DELETE'`
* @name module:jan.del
* @function
*
* @example
* <caption>Delete a REST resource</caption>
* jan.del({
* url : '/api/items/1a2b3c'
* }, function(err, data) {
* if (err) throw err;
* alert('Deleted');
* });
*/
/** Alias of {@link module:jan.del del()}.
* This alias is provided for completeness, but [should not be used](http://mothereff.in/js-properties#delete) because [it throws in ES3](http://mathiasbynens.be/notes/javascript-properties).
* @name module:jan.delete
* @function
* @deprecated Don't call <code>delete()</code> if you need to support ES3. <code>jan['delete']()</code> is okay.
*/
mapVerbs(jan);
function emit(type, args) {
args = Array.prototype.slice.call(arguments, 1);
for (var e=events[type], i=e.length; i--; ) e[i].apply(jan, args);
}
function alias(overrides) {
return function(opt, callback) {
return jan(extend({}, typeof opt==='string' ? {url:opt} : opt, overrides), callback);
};
}
function mapVerbs(onto, opts) {
for (var i=methods.length; i--; ) {
onto[methods[i].toLowerCase()] = alias(extend({}, opts || {}, {
method : methods[i]
}));
}
onto.del = onto['delete'];
return onto;
}
function extend(base, obj) {
for (var i=1, p, o; i<arguments.length; i++) {
o = arguments[i];
for (p in o) if (hop.call(o, p)) base[p] = o[p];
}
return base;
}
return (jan.jan = jan.request = jan);
}));