lib/contexts/http.js
/*!
* Expose `HttpContext`.
*/
module.exports = HttpContext;
/*!
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter;
var debug = require('debug')('poplar:http-context');
var _ = require('lodash');
var util = require('util');
var inherits = util.inherits;
var assert = require('assert');
var js2xmlparser = require('js2xmlparser');
var delegate = require('delegates');
var Helper = require('./../helper');
var Dynamic = require('./../dynamic');
var StateManager = require('../state_manager');
var DEFAULT_SUPPORTED_TYPES = [
'application/json', 'application/javascript', 'application/xml',
'text/javascript', 'text/xml',
'json', 'xml',
'*/*'
];
/**
* Create a new `HttpContext` with the given `options`.
*
* @param {Object} options
* @return {HttpContext}
* @class
*/
function HttpContext(req, res, method, options) {
this.req = this.request = req;
this.res = this.response = res;
this.method = method;
this.options = options || {};
this.args = this.buildArgs(method);
this.methodName = method.name;
this.supportedTypes = this.options.supportedTypes || DEFAULT_SUPPORTED_TYPES;
// apply delegates
this.applyDelegates();
// initialize state
this.state = StateManager.init();
if (this.supportedTypes === DEFAULT_SUPPORTED_TYPES && !this.options.xml) {
// Disable all XML-based types by default
this.supportedTypes = this.supportedTypes.filter(function(type) {
return !/\bxml\b/i.test(type);
});
}
req.context = this;
}
/**
* Inherit from `EventEmitter`.
*/
inherits(HttpContext, EventEmitter);
/**
* Apply delegates
*/
HttpContext.prototype.applyDelegates = function(method) {
var proto = this;
/**
* Response delegation.
*/
delegate(proto, 'response')
.getter('headersSent')
.getter('locals')
.method('append')
.method('attachment')
.method('cookie')
.method('clearCookie')
.method('download')
.method('end')
.method('format')
.method('get')
.method('json')
.method('jsonp')
.method('links')
.method('location')
.method('redirect')
.method('render')
.method('send')
.method('sendFile')
.method('sendStatus')
.method('set')
.method('status')
.method('type')
.method('vary');
/**
* Request delegation.
*/
delegate(proto, 'request')
.method('accepts')
.method('acceptsCharsets')
.method('acceptsEncodings')
.method('acceptsLanguages')
.method('get')
.method('is')
.method('param')
.access('socket')
.access('query')
.access('params')
.access('body')
.access('path')
.access('url')
.getter('origin')
.getter('baseUrl')
.getter('cookies')
.getter('fresh')
.getter('host')
.getter('hostname')
.getter('header')
.getter('headers')
.getter('ip')
.getter('ips')
.getter('originalUrl')
.getter('protocol')
.getter('route')
.getter('secure')
.getter('signedCookies')
.getter('stale')
.getter('subdomains');
// seperate `request.method` with `ctx.method` to avoid conflicts
proto.httpMethod = proto.request.method;
};
/**
* Build args object from the http context's `req` and `res`.
*/
HttpContext.prototype.buildArgs = function(method) {
var args = {};
var ctx = this;
var accepts = method.accepts;
// build arguments from req and method options
for (var i = 0, n = accepts.length; i < n; i++) {
var o = accepts[i];
var httpFormat = o.http;
var name = o.name || o.arg;
var val;
// Support array types, such as ['string']
var isArrayType = Array.isArray(o.type);
var otype = isArrayType ? o.type[0] : o.type;
otype = (typeof otype === 'string') && otype.toLowerCase();
var isAny = !otype || otype === 'any';
// This is an http method keyword, which requires special parsing.
if (httpFormat) {
switch (typeof httpFormat) {
case 'function':
// the options have defined a formatter
val = httpFormat(ctx);
break;
case 'object':
switch (httpFormat.source) {
case 'body':
val = ctx.req.body;
break;
case 'form':
// From the form (body)
val = ctx.req.body && ctx.req.body[name];
break;
case 'query':
// From the query string
val = ctx.req.query[name];
break;
case 'path':
// From the url path
val = ctx.req.params[name];
break;
case 'header':
val = ctx.req.get(name);
break;
case 'req':
// Direct access to http req
val = ctx.req;
break;
case 'res':
// Direct access to http res
val = ctx.res;
break;
case 'context':
// Direct access to http context
val = ctx;
break;
}
break;
}
} else {
val = ctx.getArgByName(name, o);
// Safe to coerce the contents of this
if (typeof val === 'object' && (!isArrayType || isAny)) {
val = coerceAll(val);
}
}
// If we expect an array type and we received a string, parse it with JSON.
// If that fails, parse it with the arrayItemDelimiters option.
if (val && typeof val === 'string' && isArrayType) {
var parsed = false;
if (val[0] === '[') {
try {
val = JSON.parse(val);
parsed = true;
} catch (e) {}
}
if (!parsed && ctx.options.arrayItemDelimiters) {
// Construct delimiter regex if input was an array. Overwrite option
// so this only needs to happen once.
var delims = this.options.arrayItemDelimiters;
if (Array.isArray(delims)) {
delims = new RegExp(_.map(delims, Helper.escapeRegex).join('|'), 'g');
this.options.arrayItemDelimiters = delims;
}
val = val.split(delims);
}
}
// Coerce dynamic args when input is a string.
if (isAny && typeof val === 'string') {
val = coerceAll(val);
}
// If the input is not an array, but we were expecting one, create
// an array. Create an empty array if input is empty.
if (!Array.isArray(val) && isArrayType) {
if (val !== undefined && val !== '') val = [val];
else val = [];
}
// For boolean and number types, convert certain strings to that type.
// The user can also define new dynamic types.
if (Dynamic.canConvert(otype)) {
val = dynamic(val, otype, ctx);
}
if (o.hasOwnProperty('default')) {
val = val || o.default;
}
// set the argument value
args[o.arg] = val;
}
return args;
};
/**
* Get an arg by name using the given options.
*
* @param {String} name
* @param {Object} options **optional**
*/
HttpContext.prototype.getArgByName = function(name, options) {
var req = this.req;
var args = req.params && req.params.args !== undefined ? req.params.args :
req.body && req.body.args !== undefined ? req.body.args :
req.query && req.query.args !== undefined ? req.query.args :
undefined;
if (args) {
args = JSON.parse(args);
}
if (typeof args !== 'object' || !args) {
args = {};
}
var arg = (args && args[name] !== undefined) ? args[name] :
this.req.params[name] !== undefined ? this.req.params[name] :
(this.req.body && this.req.body[name]) !== undefined ? this.req.body[name] :
this.req.query[name] !== undefined ? this.req.query[name] :
this.req.get(name);
// search these in order by name
// req.params
// req.body
// req.query
// req.header
return arg;
};
/*!
* Integer test regexp.
*/
var isint = /^[0-9]+$/;
/*!
* Float test regexp.
*/
var isfloat = /^([0-9]+)?\.[0-9]+$/;
// Use dynamic to coerce a value or array of values.
function dynamic(val, toType, ctx) {
if (Array.isArray(val)) {
return _.map(val, function(v) {
return dynamic(v, toType, ctx);
});
}
return (new Dynamic(val, ctx)).to(toType);
}
function coerce(str) {
if (typeof str !== 'string') return str;
if ('null' === str) return null;
if ('true' === str) return true;
if ('false' === str) return false;
if (isfloat.test(str)) return parseFloat(str, 10);
if (isint.test(str) && str.charAt(0) !== '0') return parseInt(str, 10);
return str;
}
// coerce every string in the given object / array
function coerceAll(obj) {
var type = Array.isArray(obj) ? 'array' : typeof obj;
var i;
var n;
switch (type) {
case 'string':
return coerce(obj);
case 'object':
if (obj) {
var props = Object.keys(obj);
for (i = 0, n = props.length; i < n; i++) {
var key = props[i];
obj[key] = coerceAll(obj[key]);
}
}
break;
case 'array':
for (i = 0, n = obj.length; i < n; i++) {
coerceAll(obj[i]);
}
break;
}
return obj;
}
function buildArgs(ctx, method, fn) {
try {
return ctx.buildArgs(method);
} catch (err) {
// JSON.parse() might throw
process.nextTick(function() {
fn(err);
});
return undefined;
}
}
function toJSON(input) {
if (!input) {
return input;
}
if (typeof input.toJSON === 'function') {
return input.toJSON();
} else if (Array.isArray(input)) {
return _.map(input, toJSON);
} else {
return input;
}
}
function toXML(input) {
var xml;
if (input && typeof input.toXML === 'function') {
xml = input.toXML();
} else {
if (input) {
// Trigger toJSON() conversions
input = toJSON(input);
}
if (Array.isArray(input)) {
input = { result: input };
}
xml = js2xmlparser('response', input, {
prettyPrinting: {
indentString: ' '
},
convertMap: {
'[object Date]': function(date) {
return date.toISOString();
}
}
});
}
return xml;
}
/**
* Finish the request and send the correct response.
*/
HttpContext.prototype.done = function() {
// if response is already returned, then do nothing
if (this._done) return;
// send the result back as
// the requested content type
var data = this.result;
var res = this.res;
var accepts = this.req.accepts(this.supportedTypes);
if (this.req.query._format) {
accepts = this.req.query._format.toLowerCase();
}
var dataExists = typeof data !== 'undefined';
if (dataExists) {
switch (accepts) {
case '*/*':
case 'application/json':
case 'json':
res.json(data);
break;
case 'application/javascript':
case 'text/javascript':
res.jsonp(data);
break;
case 'application/xml':
case 'text/xml':
case 'xml':
if (accepts === 'application/xml') {
res.header('Content-Type', 'application/xml');
} else {
res.header('Content-Type', 'text/xml');
}
if (data === null) {
res.header('Content-Length', '7');
res.end('<null/>');
} else {
try {
var xml = toXML(data);
res.send(xml);
} catch (e) {
res.send(500, e + '\n' + data);
}
}
break;
default:
// not acceptable
res.send(406);
break;
}
} else {
if (!res.get('Content-Type')) {
res.header('Content-Type', 'application/json');
}
if (res.statusCode === undefined || res.statusCode === 200) {
res.statusCode = 204;
}
res.end();
}
this._done = true;
};