|
/** |
|
* 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; |
Nice!