Skip to content

Instantly share code, notes, and snippets.

@aaronksaunders
Created March 29, 2013 03:33
Show Gist options
  • Save aaronksaunders/5268598 to your computer and use it in GitHub Desktop.
Save aaronksaunders/5268598 to your computer and use it in GitHub Desktop.

TiAgent

Ti Agent is light-weight progressive ajax API crafted for flexibility, readability, and a low learning curve after being frustrated with many of the existing request APIs.

 request
   .post('/api/pet')
   .data({ name: 'Manny', species: 'cat' })
   .set('X-API-Key', 'foobar')
   .set('Accept', 'application/json')
   .end(function(res){
     if (res.ok) {
       alert('yay got ' + JSON.stringify(res.body));
     } else {
       alert('Oh no! error ' + res.text);
     }
   });

Request basics

A request can be initiated by invoking the appropriate method on the request object, then calling .end() to send the request. For example a simple GET request:

 request
   .get('/search')
   .end(function(res){
   
   });

The node client may also provide absolute urls:

 request
   .get('http://example.com/search')
   .end(function(res){
 
   });

DELETE, HEAD, POST, PUT and other HTTP verbs may also be used, simply change the method name:

request
  .head('/favicon.ico')
  .end(function(res){
  
  });

DELETE is a special-case, as it's a reserved word, so the method is named .del():

request
  .del('/user/1')
  .end(function(res){
    
  });

Crafting requests

TiAgent's flexible API gives you the granularity you need, when you need, yet more concise variations help reduce the amount of code necessary. For example the following GET request:

request
  .get('/search')
  .end(function(res){

  });

Could also be defined as the following, where a callback is given to the HTTP verb method:

request
  .get('/search', function(res){

  });

Taking this further the default HTTP verb is GET so the following works as well:

 request('/search', function(res){

 });

This applies to more complicated requests as well, for example the following GET request with a query-string can be written in the chaining manner:

 request
   .get('/search')
   .data({ query: 'tobi the ferret' })
   .end(function(res){
     
   });

Or one may pass the query-string object to .get():

 request
   .get('/search', { query: 'tobi the ferret' })
   .end(function(res){
   
   });

Taking this even further the callback may be passed as well:

 request
   .get('/search', { query: 'tobi the ferret' }, function(res){
 
   });

Setting header fields

Setting header fields is simple, invoke .set() with a field name and value:

 request
   .get('/search')
   .set('API-Key', 'foobar')
   .set('Accept', 'application/json')
   .end(callback);

GET requests

The .data() method accepts objects, which when used with the GET method will form a query-string. The following will produce the path /search?query=Manny&range=1..5&order=desc.

 request
   .get('/search')
   .data({ query: 'Manny' })
   .data({ range: '1..5' })
   .data({ order: 'desc' })
   .end(function(res){

   });

The .data() method accepts strings as well:

  request
    .get('/querystring')
    .data('search=Manny&range=1..5')
    .end(function(res){

    });

POST / PUT requests

A typical JSON POST request might look a little like the following, where we set the Content-Type header field appropriately, and "write" some data, in this case just a JSON string.

  request.post('/user')
    .set('Content-Type', 'application/json')
    .data('{"name":"tj","pet":"tobi"})
    .end(callback)

Since JSON is undoubtably the most common, it's the default! The following example is equivalent to the previous.

  request.post('/user')
    .data({ name: 'tj', pet: 'tobi' })
    .end(callback)

Or using multiple .data() calls:

  request.post('/user')
    .data({ name: 'tj' })
    .data({ pet: 'tobi' })
    .end(callback)

TiAgent formats are extensible, however by default "json" and "form" are supported. To send the data as application/x-www-form-urlencoded simply invoke .type() with "form-data", where the default is "json". This request will POST the body "name=tj&pet=tobi".

  request.post('/user')
    .type('form')
    .data({ name: 'tj' })
    .data({ pet: 'tobi' })
    .end(callback)

Response properties

Many helpful flags and properties are set on the Response object, ranging from the response text, parsed response body, header fields, status flags and more.

Response text

The res.text property contains the unparsed response body string.

Response body

Much like TiAgent can auto-serialize request data, it can also automatically parse it. When a parser is defined for the Content-Type, it is parsed, which by default includes "application/json" and "application/x-www-form-urlencoded". The parsed object is then available via res.body.

Response header fields

The res.header contains an object of parsed header fields, lowercasing field names much like node does. For example res.header['content-length'].

Response Content-Type

The Content-Type response header is special-cased, providing res.contentType, which is void of the charset (if any). For example the Content-Type of "text/html; charset=utf8" will provide "text/html" as res.contentType, and the res.charset property would then contain "utf8".

Response status

The response status flags help determine if the request was a success, among other useful information, making TiAgent ideal for interacting with RESTful web services. These flags are currently defined as:

 var type = status / 100 | 0;

 // status / class
 res.status = status;
 res.statusType = type;

 // basics
 res.info = 1 == type;
 res.ok = 2 == type;
 res.clientError = 4 == type;
 res.serverError = 5 == type;
 res.error = 4 == type || 5 == type;

 // sugar
 res.accepted = 202 == status;
 res.noContent = 204 == status || 1223 == status;
 res.badRequest = 400 == status;
 res.unauthorized = 401 == status;
 res.notAcceptable = 406 == status;
 res.notFound = 404 == status;
/**
* Module exports.
*/
/**
* Check if `obj` is an array.
*/
function isArray(obj) {
return '[object Array]' == {}.toString.call(obj);
}
/**
* Event emitter constructor.
*
* @api public.
*/
function EventEmitter(){};
/**
* Adds a listener.
*
* @api public
*/
EventEmitter.prototype.on = function (name, fn) {
if (!this.$events) {
this.$events = {};
}
if (!this.$events[name]) {
this.$events[name] = fn;
} else if (isArray(this.$events[name])) {
this.$events[name].push(fn);
} else {
this.$events[name] = [this.$events[name], fn];
}
return this;
};
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
/**
* Adds a volatile listener.
*
* @api public
*/
EventEmitter.prototype.once = function (name, fn) {
var self = this;
function on () {
self.removeListener(name, on);
fn.apply(this, arguments);
};
on.listener = fn;
this.on(name, on);
return this;
};
/**
* Removes a listener.
*
* @api public
*/
EventEmitter.prototype.removeListener = function (name, fn) {
if (this.$events && this.$events[name]) {
var list = this.$events[name];
if (isArray(list)) {
var pos = -1;
for (var i = 0, l = list.length; i < l; i++) {
if (list[i] === fn || (list[i].listener && list[i].listener === fn)) {
pos = i;
break;
}
}
if (pos < 0) {
return this;
}
list.splice(pos, 1);
if (!list.length) {
delete this.$events[name];
}
} else if (list === fn || (list.listener && list.listener === fn)) {
delete this.$events[name];
}
}
return this;
};
/**
* Removes all listeners for an event.
*
* @api public
*/
EventEmitter.prototype.removeAllListeners = function (name) {
if (name === undefined) {
this.$events = {};
return this;
}
if (this.$events && this.$events[name]) {
this.$events[name] = null;
}
return this;
};
/**
* Gets all listeners for a certain event.
*
* @api publci
*/
EventEmitter.prototype.listeners = function (name) {
if (!this.$events) {
this.$events = {};
}
if (!this.$events[name]) {
this.$events[name] = [];
}
if (!isArray(this.$events[name])) {
this.$events[name] = [this.$events[name]];
}
return this.$events[name];
};
/**
* Emits an event.
*
* @api public
*/
EventEmitter.prototype.emit = function (name) {
if (!this.$events) {
return false;
}
var handler = this.$events[name];
if (!handler) {
return false;
}
var args = [].slice.call(arguments, 1);
if ('function' == typeof handler) {
handler.apply(this, args);
} else if (isArray(handler)) {
var listeners = handler.slice();
for (var i = 0, l = listeners.length; i < l; i++) {
listeners[i].apply(this, args);
}
} else {
return false;
}
return true;
};
/*!
* tiagent
* Copyright (c) 2012 Christian Sullivan <[email protected]>
* MIT Licensed
*
* Ported from superagent by TJ Holowaychuk <[email protected]>
*/
var tiagent = function(exports){
/**
* Expose the request function.
*/
exports = request;
/**
* Library version.
*/
exports.version = '0.2.0';
/*
* Check if mobile app
*/
exports.mobile = mobile = (typeof window !== 'object');
Ti.API.info(mobile);
/**
* Noop.
*/
var noop = function(){};
/**
* Determine XHR.
*/
function getXHR() {
if (mobile) {
try { return Titanium.Network.createHTTPClient(); } catch(e) {}
} else if (window.XMLHttpRequest
&& ('file:' != window.location.protocol || !window.ActiveXObject)) {
return new XMLHttpRequest;
} else {
try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch(e) {}
try { return new ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch(e) {}
try { return new ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch(e) {}
try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch(e) {}
}
return false;
}
/**
* Removes leading and trailing whitespace, added to support IE.
*
* @param {String} s
* @return {String}
* @api private
*/
var trim = ''.trim
? function(s) { return s.trim(); }
: function(s) { return s.replace(/(^\s*|\s*$)/g, ''); };
/**
* Check if `obj` is a function.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isFunction(obj) {
return 'function' == typeof obj;
}
/**
* Check if `obj` is an object.
*
* @param {Object} obj
* @return {Boolean}
* @api private
*/
function isObject(obj) {
return null != obj && 'object' == typeof obj;
}
/**
* Serialize the given `obj`.
*
* @param {Object} obj
* @return {String}
* @api private
*/
function serialize(obj) {
if (!isObject(obj)) return obj;
var pairs = [];
for (var key in obj) {
pairs.push(encodeURIComponent(key)
+ '=' + encodeURIComponent(obj[key]));
}
return pairs.join('&');
}
/**
* Expose serialization method.
*/
exports.serializeObject = serialize;
/**
* Parse the given x-www-form-urlencoded `str`.
*
* @param {String} str
* @return {Object}
* @api private
*/
function parseString(str) {
var obj = {}
, pairs = str.split('&')
, parts
, pair;
for (var i = 0, len = pairs.length; i < len; ++i) {
pair = pairs[i];
parts = pair.split('=');
obj[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
}
return obj;
}
/**
* Expose parser.
*/
exports.parseString = parseString;
/**
* Default MIME type map.
*
* tiagent.types.xml = 'application/xml';
*
*/
exports.types = {
html: 'text/html'
, json: 'application/json'
, urlencoded: 'application/x-www-form-urlencoded'
, 'form-data': 'application/x-www-form-urlencoded'
};
/**
* Default serialization map.
*
* tiagent.serialize['application/xml'] = function(obj){
* return 'generated xml here';
* };
*
*/
exports.serialize = {
'application/x-www-form-urlencoded': serialize
, 'application/json': JSON.stringify
};
/**
* Default parsers.
*
* tiagent.parse['application/xml'] = function(str){
* return { object parsed from str };
* };
*
*/
exports.parse = {
'application/x-www-form-urlencoded': parseString
, 'application/json': JSON.parse
};
/**
* Parse the given header `str` into
* an object containing the mapped fields.
*
* @param {String} str
* @return {Object}
* @api private
*/
function parseHeader(str) {
var lines = str.split(/\r?\n/)
, fields = {}
, index
, line
, field
, val;
lines.pop(); // trailing CRLF
for (var i = 0, len = lines.length; i < len; ++i) {
line = lines[i];
index = line.indexOf(':');
field = line.slice(0, index).toLowerCase();
val = trim(line.slice(index + 1));
fields[field] = val;
}
return fields;
}
/**
* Initialize a new `Response` with the given `xhr`.
*
* - set flags (.ok, .error, etc)
* - parse header
*
* Examples:
*
* Aliasing `tiagent` as `request` is nice:
*
* request = tiagent;
*
* We can use the promise-like API, or pass callbacks:
*
* request.get('/').end(function(res){});
* request.get('/', function(res){});
*
* Sending data can be chained:
*
* request
* .post('/user')
* .send({ name: 'tj' })
* .end(function(res){});
*
* Or passed to `.send()`:
*
* request
* .post('/user')
* .send({ name: 'tj' }, function(res){});
*
* Or passed to `.post()`:
*
* request
* .post('/user', { name: 'tj' })
* .end(function(res){});
*
* Or further reduced to a single call for simple cases:
*
* request
* .post('/user', { name: 'tj' }, function(res){});
*
* @param {XMLHTTPRequest} xhr
* @param {Object} options
* @api private
*/
function Response(xhr, options) {
options = options || {};
this.xhr = xhr;
this.text = xhr.responseText;
this.setStatusProperties(xhr.status);
this.header = (mobile) ? { 'content-type' : xhr.getResponseHeader('Content-Type') } : parseHeader(xhr.getAllResponseHeaders());
this.setHeaderProperties(this.header);
this.body = this.parseBody(this.text);
}
/**
* Set header related properties:
*
* - `.contentType` the content type without params
*
* A response of "Content-Type: text/plain; charset=utf-8"
* will provide you with a `.contentType` of "text/plain".
*
* @param {Object} header
* @api private
*/
Response.prototype.setHeaderProperties = function(header){
// TODO: moar!
var params = (this.header['content-type'] || '').split(/ *; */);
this.contentType = params.shift();
this.setParams(params);
};
/**
* Create properties from `params`.
*
* For example "Content-Type: text/plain; charset=utf-8"
* would provide `.charset` "utf-8".
*
* @param {Array} params
* @api private
*/
Response.prototype.setParams = function(params){
var param;
for (var i = 0, len = params.length; i < len; ++i) {
param = params[i].split(/ *= */);
this[param[0]] = param[1];
}
};
/**
* Parse the given body `str`.
*
* Used for auto-parsing of bodies. Parsers
* are defined on the `tiagent.parse` object.
*
* @param {String} str
* @return {Mixed}
* @api private
*/
Response.prototype.parseBody = function(str){
var parse = exports.parse[this.contentType];
return parse
? parse(str)
: null;
};
/**
* Set flags such as `.ok` based on `status`.
*
* For example a 2xx response will give you a `.ok` of __true__
* whereas 5xx will be __false__ and `.error` will be __true__. The
* `.clientError` and `.serverError` are also available to be more
* specific, and `.statusType` is the class of error ranging from 1..5
* sometimes useful for mapping respond colors etc.
*
* "sugar" properties are also defined for common cases. Currently providing:
*
* - .noContent
* - .badRequest
* - .unauthorized
* - .notAcceptable
* - .notFound
*
* @param {Number} status
* @api private
*/
Response.prototype.setStatusProperties = function(status){
var type = status / 100 | 0;
// status / class
this.status = status;
this.statusType = type;
// basics
this.info = 1 == type;
this.ok = 2 == type;
this.clientError = 4 == type;
this.serverError = 5 == type;
this.error = 4 == type || 5 == type;
// sugar
this.accepted = 202 == status;
this.noContent = 204 == status || 1223 == status;
this.badRequest = 400 == status;
this.unauthorized = 401 == status;
this.notAcceptable = 406 == status;
this.notFound = 404 == status;
};
/**
* Expose `Response`.
*/
exports.Response = Response;
/**
* Initialize a new `Request` with the given `method` and `url`.
*
* @param {String} method
* @param {String} url
* @api public
*/
function Request(method, url) {
var self = this;
EventEmitter.call(this);
this.method = method;
this.url = url;
this.header = {};
this.set('X-Requested-With', 'XMLHttpRequest');
this.on('end', function(){
self.callback(new Response(self.xhr));
});
}
/**
* Inherit from `EventEmitter.prototype`.
*/
Request.prototype = new EventEmitter;
Request.prototype.constructor = Request;
/**
* Set header `field` to `val`, or multiple fields with one object.
*
* Examples:
*
* req.get('/')
* .set('Accept', 'application/json')
* .set('X-API-Key', 'foobar')
* .end(callback);
*
* req.get('/')
* .set({ Accept: 'application/json', 'X-API-Key': 'foobar' })
* .end(callback);
*
* @param {String|Object} field
* @param {String} val
* @return {Request} for chaining
* @api public
*/
Request.prototype.set = function(field, val){
if (isObject(field)) {
for (var key in field) {
this.set(key, field[key]);
}
return this;
}
this.header[field.toLowerCase()] = val;
return this;
};
/**
* Set Content-Type to `type`, mapping values from `exports.types`.
*
* Examples:
*
* tiagent.types.xml = 'application/xml';
*
* request.post('/')
* .type('xml')
* .send(xmlstring)
* .end(callback);
*
* request.post('/')
* .type('application/xml')
* .send(xmlstring)
* .end(callback);
*
* @param {String} type
* @return {Request} for chaining
* @api public
*/
Request.prototype.type = function(type){
this.set('Content-Type', exports.types[type] || type);
return this;
};
/**
* Send `data`, defaulting the `.type()` to "json" when
* an object is given.
*
* Examples:
*
* // querystring
* request.get('/search')
* .send({ search: 'query' })
* .end(callback)
*
* // multiple data "writes"
* request.get('/search')
* .send({ search: 'query' })
* .send({ range: '1..5' })
* .send({ order: 'desc' })
* .end(callback)
*
* // manual json
* request.post('/user')
* .type('json')
* .send('{"name":"tj"})
* .end(callback)
*
* // auto json
* request.post('/user')
* .send({ name: 'tj' })
* .end(callback)
*
* // manual x-www-form-urlencoded
* request.post('/user')
* .type('form')
* .send('name=tj')
* .end(callback)
*
* // auto x-www-form-urlencoded
* request.post('/user')
* .type('form')
* .send({ name: 'tj' })
* .end(callback)
*
* @param {String|Object} data
* @return {Request} for chaining
* @api public
*/
Request.prototype.send = function(data){
var obj = isObject(data);
// merge
if (obj && isObject(this._data)) {
for (var key in data) {
this._data[key] = data[key];
}
} else {
this._data = data;
}
if ('GET' == this.method) return this;
if (!obj) return this;
if (this.header['content-type']) return this;
this.type('json');
return this;
};
/**
* Initiate request, invoking callback `fn(res)`
* with an instanceof `Response`.
*
* @param {Function} fn
* @return {Request} for chaining
* @api public
*/
Request.prototype.end = function(fn){
var self = this
, xhr = this.xhr = getXHR()
, data = this._data || null;
// store callback
this.callback = fn || noop;
// state change
xhr.onreadystatechange = function(){
if (4 == xhr.readyState) self.emit('end');
};
// querystring
if ('GET' == this.method && null != data) {
this.url += '?' + exports.serializeObject(data);
data = null;
}
// initiate request
xhr.open(this.method, this.url, true);
// body
if ('GET' != this.method && 'HEAD' != this.method) {
// serialize stuff
var serialize = exports.serialize[this.header['content-type']];
if (serialize) data = serialize(data);
}
// set header fields
for (var field in this.header) {
xhr.setRequestHeader(field, this.header[field]);
}
// send stuff
xhr.send(data);
return this;
};
/**
* Expose `Request`.
*/
exports.Request = Request;
/**
* Issue a request:
*
* Examples:
*
* request('GET', '/users').end(callback)
* request('/users').end(callback)
* request('/users', callback)
*
* @param {String} method
* @param {String|Function} url or callback
* @return {Request}
* @api public
*/
function request(method, url) {
// callback
if ('function' == typeof url) {
return new Request('GET', method).end(url);
}
// url first
if (1 == arguments.length) {
return new Request('GET', method);
}
return new Request(method, url);
}
/**
* GET `url` with optional callback `fn(res)`.
*
* @param {String} url
* @param {Mixed} data
* @param {Function} fn
* @return {Request}
* @api public
*/
request.get = function(url, data, fn){
var req = request('GET', url);
if (isFunction(data)) fn = data, data = null;
if (data) req.send(data);
if (fn) req.end(fn);
return req;
};
/**
* DELETE `url` with optional callback `fn(res)`.
*
* @param {String} url
* @param {Function} fn
* @return {Request}
* @api public
*/
request.del = function(url, fn){
var req = request('DELETE', url);
if (fn) req.end(fn);
return req;
};
/**
* POST `url` with optional `data` and callback `fn(res)`.
*
* @param {String} url
* @param {Mixed} data
* @param {Function} fn
* @return {Request}
* @api public
*/
request.post = function(url, data, fn){
var req = request('POST', url);
if (data) req.send(data);
if (fn) req.end(fn);
return req;
};
/**
* PUT `url` with optional `data` and callback `fn(res)`.
*
* @param {String} url
* @param {Mixed} data
* @param {Function} fn
* @return {Request}
* @api public
*/
request.put = function(url, data, fn){
var req = request('PUT', url);
if (data) req.send(data);
if (fn) req.end(fn);
return req;
};
return exports;
}({});
exports = tiagent;
@nuno
Copy link

nuno commented Nov 14, 2013

Nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment