@balderdash/sails-edge
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
402 lines (334 loc) • 11.9 kB
JavaScript
/**
* Module dependencies
*/
var util = require('util');
var Transform = require('stream').Transform;
var _ = require('lodash');
var MockRes = require('mock-res');
/**
* Ensure that response object has a minimum set of reasonable defaults
* Used primarily as a test fixture.
*
* @api private
* @idempotent
*/
module.exports = function buildResponse (req, _res) {
_res = _res||{};
req = req||{};
var res;
// If `_res` appears to be a stream (duck-typing), then don't try
// and turn it into a mock stream again.
if (typeof _res === 'object' && _res.end) {
res = _res;
}
else {
res = new MockRes();
delete res.statusCode;
}
// Ensure res.headers and res.locals exist.
res = _.extend(res, {locals: {}, headers: {}, _headers: {}});
res = _.extend(res, _res);
// Now that we're sure `res` is a Transform stream, we'll handle the two different
// approaches which a user of the virtual request interpreter might have taken:
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// (1) Providing a callback function (`_clientCallback`)
//
// If a `_clientCallback` function was provided, also pipe `res` into a
// fake clientRes stream where the response `body` will be buffered.
if (res._clientCallback) {
// If `res._clientRes` WAS NOT provided, then create one
if (!res._clientRes) {
res._clientRes = new MockClientResponse();
}
// Save session
// if (_.isObject(req.session) && _.isFunction(req.session.save)) {
// req.session.save(function (err){
// if (err) {
// err = _.isObject(err) ? err : new Error(err);
// err.code = 'E_SESSION_SAVE';
// if (req._sails && req._sails.log){
// req._sails.log.error('Session could not be persisted. Details:', err);
// }
// else {
// console.error(err);
// }
// }
// });
// }
// The stream should trigger the callback when it finishes or errors.
res._clientRes.on('finish', function() {
return res._clientCallback(res._clientRes);
});
res._clientRes.on('error', function(err) {
err = err || new Error('Error on response stream');
res._clientRes.statusCode = 500;
res._clientRes.body = err;
return res._clientCallback(res._clientRes);
});
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// (2) Providing a Writable stream (`_clientRes`)
//
// If a `_clientRes` response Transform stream was provided, pipe `res` directly to it.
if (res._clientRes) {
res.pipe(res._clientRes);
}
//
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Track whether headers have been written
// (TODO: pull all this into mock-res via a PR)
// res.writeHead() is wrapped in closure by the `on-header` module,
// but it still needs the underlying impl
res.writeHead = function ( /* statusCode, [reasonPhrase], headers */) {
// console.log('\n\n• res.writeHead(%s)', Array.prototype.slice.call(arguments));
var statusCode = +arguments[0];
//TODO: Analyze this code, reasonPhras not used
var reasonPhrase = (function(){
if (arguments[2] && _.isString(arguments[1])) {
return arguments[1];
}
return undefined;
})();
var newHeaders = (function (){
if (arguments[2] && _.isObject(arguments[2])) {
return arguments[2];
}
return arguments[1];
})();
if (!statusCode) {
throw new Error('`statusCode` must be passed to res.writeHead().');
}
// Set status code
res.statusCode = statusCode;
// Ensure `._headers` have been merged into `.headers`
_.extend(res.headers, res._headers);
if (newHeaders) {
if (!_.isObject(newHeaders)) {
throw new Error('`headers` must be passed to res.writeHead() as an object. Got: '+util.inspect(newHeaders, false, null));
}
// Set new headers
_.extend(res.headers, newHeaders);
}
// Set status code and headers on the `_clientRes` stream so they are accessible
// to the provider of that stream.
// (this has to happen in `send()` because the code/headers might have just changed)
if (res._clientRes) {
// console.log('Setting headers on clientRes- res.headers = ',res.headers);
res._clientRes.headers = res.headers;
res._clientRes.statusCode = res.statusCode;
}
};
// Wrap res.write() and res.end() to get them to call writeHead()
var prevWrite = res.write;
res.write = function (){
res.writeHead(res.statusCode, _.extend(res._headers,res.headers));
// console.log('res.write():: called writeHead with headers=',_.extend(res._headers,res.headers));
prevWrite.apply(res, Array.prototype.slice.call(arguments));
};
var prevEnd = res.end;
res.end = function (){
res.writeHead(res.statusCode, _.extend(res._headers,res.headers));
// console.log('our res.end() was triggered');
// console.log('res.end():: called writeHead with headers=',_.extend(res._headers,res.headers));
prevEnd.apply(res, Array.prototype.slice.call(arguments));
};
// we get `setHeader` from mock-res
// see http://nodejs.org/api/http.html#http_response_setheader_name_value
//
// Usage:
// response.setHeader("Set-Cookie", ["type=ninja", "language=javascript"]);
// If we ever need to wrap it...
//
// var prevSetHeader = res.setHeader;
// res.setHeader = function (){
// prevSetHeader.apply(res, Array.prototype.slice.call(arguments));
// };
// res.status()
res.status = res.status || function status_shim (statusCode) {
res.statusCode = statusCode;
return res;
};
// res.send()
res.send = res.send || function send_shim () {
var args = normalizeResArgs(arguments);
// Don't allow users to respond/redirect more than once per request
// TODO: prbly move this check to our `res.writeHead()` impl
try {
onlyAllowOneResponse(res);
}
catch (e) {
if (req._sails && req._sails.log && req._sails.log.error) {
req._sails.log.error(e);
return;
}
// TODO: use debug()
console.error(e);
return;
}
// Ensure charset is set
res.charset = res.charset || 'utf-8';
// Ensure headers are set
_.extend(res.headers, res._headers);
// Ensure statusCode is set
// (override `this.statusCode` if `statusCode` argument specified)
res.statusCode = args.statusCode || res.statusCode || 200;
// if a `_clientCallback` was specified, we'll skip the streaming stuff for res.send().
if (res._clientCallback) {
// Hard-code `res.body` rather than writing to the stream.
// (but don't include body if it is empty)
if (args.other) {
res.body = args.other;
// Then expose on res._clientRes.body
res._clientRes.body = res.body;
}
// End the `res` stream (which will in turn end the `res._clientRes` stream)
res.end();
return;
}
//
// Otherwise, the hook using the interpreter must have provided us with a `res._clientRes` stream,
// so we'll need to serialize everything to work w/ that stream.
//
// console.log('\n---\nwriting to clientRes stream...');
// console.log('res.headers =>',res.headers);
// console.log('res._headers =>',res._headers);
// Write body to `res` stream
if (args.other) {
var toWrite = args.other;
if (typeof toWrite === 'object') {
try {
toWrite = JSON.stringify(args.other);
// original way:
// toWrite = util.inspect(toWrite);
}
catch(e) {
//TODO: Defined failedStringify, but use as failedToStringify
var failedStringify = new Error(
'Failed to stringify specified JSON response body :: ' + util.inspect(args.other) +
'\nError:\n' + util.inspect(e)
);
// console.log('failed to stringify!');
if (req._sails && req._sails.log && req._sails.log.error) {
req._sails.log.error(failedToStringify);
}
else {
// todo: use debug()
console.error(failedToStringify);
}
toWrite = failedStringify.message;
res.statusCode = 500;
}
}
res.write(toWrite);
}
// End the `res` stream.
res.end();
};
// res.json()
res.json = res.json || function json_shim () {
var args = normalizeResArgs(arguments);
return res.send(args.other, args.statusCode || res.statusCode || 200);
};
// res.render()
res.render = res.render || function render_shim (relativeViewPath, locals, cb) {
if (_.isFunction(arguments[1])) {
cb = arguments[1];
locals = {};
}
if (!req._sails) {
return res.send(500, 'Cannot call res.render() - `req._sails` was not attached');
}
if (!req._sails.renderView) {
return res.send(500, 'Cannot call res.render() - `req._sails.renderView` was not attached (perhaps `views` hook is not enabled?)');
}
// TODO:
// Instead of this shim, turn `sails.renderView` into something like
// `sails.hooks.views.render()`, and then call it.
return res.send(501,'Not implemented in core yet');
};
// res.redirect()
res.redirect = res.redirect || function redirect_shim () {
var args = normalizeResArgs(arguments);
var address = args.other;
// Set location header
res.set('Location',address);
// address = this.get('Location');
return res.send(args.statusCode || res.statusCode || 302, 'Redirecting to '+encodeURI(address));
};
/**
* res.set( headerName, value )
*
* @param {[type]} headerName [description]
* @param {[type]} value [description]
*/
res.set = function (headerName, value) {
res.headers = res.headers || {};
res.headers[headerName] = value;
return this;
};
/**
* res.get( headerName )
*
* @param {[type]} headerName [description]
* @return {[type]} [description]
*/
res.get = function (headerName) {
return res.headers && res.headers[headerName];
};
return res;
};
/**
* As long as one of them is a number (i.e. a status code),
* allows a 2-nary method to be called with flip-flopped arguments:
* method( [statusCode|other], [statusCode|other] )
*
* This avoids confusing errors & provides Express 2.x backwards compat.
*
* E.g. usage in res.send():
* var args = normalizeResArgs.apply(this, arguments),
* body = args.other,
* statusCode = args.statusCode;
*
* @api private
*/
function normalizeResArgs( args ) {
// Traditional usage:
// `method( other [,statusCode] )`
var isNumeric = function (x) {
return (+x === x);
};
if (isNumeric(args[0])) {
return {
statusCode: args[0],
other: args[1]
};
}
else return {
statusCode: args[1],
other: args[0]
};
}
/**
* NOTE: ALL RESPONSES (INCLUDING REDIRECTS) ARE PREVENTED ONCE THE RESPONSE HAS BEEN SENT!!
* Even though this is not strictly required with sockets, since res.redirect()
* is an HTTP-oriented method from Express, it's important to maintain consistency.
*
* @api private
*/
function onlyAllowOneResponse (res) {
if (res._virtualResponseStarted) {
throw new Error('Cannot write to response more than once');
}
res._virtualResponseStarted = true;
}
// The constructor for clientRes stream
// (just a normal transform stream)
function MockClientResponse() {
Transform.call(this);
}
util.inherits(MockClientResponse, Transform);
MockClientResponse.prototype._transform = function(chunk, encoding, next) {
this.push(chunk);
next();
};