rest-handler
Version:
A module for creating a request handler that does REST style routing
437 lines (353 loc) • 10.4 kB
JavaScript
'use strict';
var events = require('events');
var util = require('util');
var url = require('url');
var pathBasedRouter = require('path-based-router');
var REST = require('./REST');
function RESTHandler(options) {
events.EventEmitter.call(this);
this._routerByMethod = {};
this.ALL_ROUTES_METHOD = 'AllRoutes';
this.routeInfoByPath = {};
// array of functions that will be invoked prior to request being dispatched to final route function
this._before = [];
if (options) {
this._onRouteNotFound = options.onRouteNotFound;
if (options.routes) {
for (var i = 0; i < options.routes.length; i++) {
this.addRoute(options.routes[i]);
}
}
}
this.serializers = [];
this.errorHandlers = [];
}
util.inherits(RESTHandler, events.EventEmitter);
var Route_addBefore = function(handler) {
var route = this;
var before = route._before || (route._before = []);
before.push(handler);
};
RESTHandler.prototype._registerRoute = function(method, route) {
var path = route.path;
var routeInfo = this.routeInfoByPath[path] || (this.routeInfoByPath[path] = {
methods: []
});
routeInfo.methods.push(method);
};
RESTHandler.prototype._getMethodsForPath = function(path) {
var routeInfo = this.routeInfoByPath[path];
return (routeInfo) ? routeInfo.methods : null;
};
RESTHandler.prototype._addRoute = function(method, routeConfig) {
var route = this.createRoute(routeConfig);
this.getMethodRouter(method).addRoute(route);
this.getMethodRouter(this.ALL_ROUTES_METHOD).addRoute(route);
this._registerRoute(method, route);
route.restHandler = this;
// notify listeners that route was added
this.emit('route', {
method: method,
route: route
});
};
function _findRoute(restHandler, method, reqUrl) {
var router = restHandler._routerByMethod[method];
if (!router) {
return null;
}
return router.findRoute(reqUrl.pathname);
}
function _notFound(restHandler, reqUrlObj, req, res, socket) {
// Check if the route exists regardless of method
var match = _findRoute(restHandler, restHandler.ALL_ROUTES_METHOD, reqUrlObj);
if (match) {
var route = match.route;
restHandler.emit('methodNotAllowed', req, res, socket);
restHandler.methodNotAllowed('Method Not Allowed', req, res, socket, restHandler._getMethodsForPath(route.path));
} else {
restHandler.emit('routeNotFound', req, res, socket);
restHandler.notFound('Not found', req, res, socket);
}
}
function _rest(restHandler, req, res, socket, domain) {
var reqUrlObj = url.parse(req.url, true);
// find route by exact method match or by wildcard method
var match = _findRoute(restHandler, req.method, reqUrlObj) || _findRoute(restHandler, '*', reqUrlObj);
if (!match) {
_notFound(restHandler, reqUrlObj, req, res, socket);
return null;
}
// Instantiate a REST object that will encapsulate
// req and res and contain helper methods
var rest = new REST(restHandler, domain);
rest.req = req;
rest.res = res;
rest.socket = socket;
rest.params = match.params;
rest.route = match.route;
rest.url = reqUrlObj;
return rest;
}
RESTHandler.prototype.createRoute = function(routeConfig) {
var route = pathBasedRouter.createRoute(routeConfig);
if (route.before && (route._before === undefined)) {
route._before = Array.isArray(route.before) ? route.before : [route.before];
}
if (!route.addBefore) {
// add a mixin
route.addBefore = Route_addBefore;
}
return route;
};
RESTHandler.prototype.getAllRoutes = function() {
return this.getMethodRouter(this.ALL_ROUTES_METHOD).getRoutes();
};
RESTHandler.prototype.addRoute = function(routeConfig) {
if (!routeConfig.method) {
// No method so hook to any method
this._addRoute('*', routeConfig);
} else if (Array.isArray(routeConfig.method)) {
// Route will be bound to multiple methods
for (var m = 0; m < routeConfig.method.length; m++) {
var method = routeConfig.method[m];
this._addRoute(method.toUpperCase(), routeConfig);
}
} else {
// Route will be bound to a single method
this._addRoute(routeConfig.method.toUpperCase(), routeConfig);
}
};
RESTHandler.prototype.handle = function(req, res, domain) {
var rest = _rest(this, req, res, null, domain);
if (rest !== null) {
this.emit('beforeHandle', rest);
rest.handle();
return rest;
}
};
RESTHandler.prototype.forwardTo = function(rest, route) {
var forwardFrom = rest.forwardFrom || (rest.forwardFrom = []);
forwardFrom.push(rest.url);
rest.req.url = route;
rest.url = url.parse(route, true);
// find route by exact method match or by wildcard method
var match = _findRoute(this, rest.req.method, rest.url) || _findRoute(this, '*', rest.url);
if (!match) {
_notFound(this, rest.url, rest.req, rest.res, rest.socket);
return null;
}
rest.params = match.params;
rest.route = match.route;
this.emit('beforeHandle', rest);
rest.handle();
return rest;
};
RESTHandler.prototype.handleUpgrade = function(req, socket, head, domain) {
var rest = _rest(this, req, null, socket, domain);
if (rest !== null) {
// store the socket and head in the REST object and also set "upgrade" to true
rest.upgrade = true;
rest.head = head;
// notify listeners that route was added
this.emit('beforeHandle', rest);
rest.handle();
return rest;
}
};
RESTHandler.prototype.notFound = function(message, req, res, socket) {
if (this._onRouteNotFound) {
this._onRouteNotFound(message. req, res, socket);
return;
}
if (arguments.length === 2) {
req = arguments[0];
res = arguments[1];
message = 'Not Found';
}
if (res) {
res.setHeader('Content-Type', 'text/plain');
res.statusCode = 404;
res.end(message);
} else if (socket) {
socket.end();
}
};
RESTHandler.prototype.methodNotAllowed = function(message, req, res, socket, methods) {
if (arguments.length === 2) {
req = arguments[0];
res = arguments[1];
message = 'Method Not Allowed';
}
if (res) {
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Allow', methods.join());
res.statusCode = 405;
res.end(message);
} else if (socket) {
socket.end();
}
};
RESTHandler.prototype.middleware = RESTHandler.prototype.before = function(middleware) {
var handler = [];
if (middleware.constructor === Function) {
handler = middleware;
} else {
handler = middleware.handler;
if (middleware.init && (middleware.__restHandler !== this)) {
// record that we initialized it
middleware.__restHandler = this;
// middleware has an init function and we haven't initialized it yet
middleware.init(this);
}
}
if (handler) {
this._before.push(handler);
}
return this;
};
RESTHandler.prototype.getBefore = function() {
return this._before;
};
RESTHandler.prototype.getMethodRouter = function(method) {
var router = this._routerByMethod[method];
if (!router) {
this._routerByMethod[method] = router = pathBasedRouter.create();
}
return router;
};
RESTHandler.prototype.defaultSerializer = function(rest, message) {
var res = rest.res;
res.statusCode = res.statusCode || 200;
if (!message) {
rest.res.end();
return;
}
var contentType;
if (message.constructor === String) {
contentType = res.getHeader('Content-Type') || 'text/plain';
} else {
try {
message = JSON.stringify(message, null, ' ');
contentType = 'application/json';
} catch (e) {
message = message.toString();
contentType = 'text/plain';
}
}
res.setHeader('Content-Type', contentType);
res.write(message);
res.end();
};
RESTHandler.prototype.defaultErrorHandler = function(rest, message) {
var res = rest.res;
// remove statusCode if it is not an error code
if ((res.statusCode < 400) || (res.statusCode >= 600)) {
res.statusCode = null;
}
if (!message) {
res.statusCode = res.statusCode || 500;
rest.res.end();
return;
}
if (rest.res.headersSent) {
if (message instanceof Error) {
message = message.toString();
} else if (message) {
try {
message = JSON.stringify(message);
} catch(e) {
message = message.toString();
}
}
console.error('Error while handling request.', message);
rest.res.end();
return;
}
var contentType;
if (message instanceof Error) {
message = message.toString();
contentType = 'text/plain';
} else if (message.constructor === String) {
contentType = res.getHeader('Content-Type') || 'text/plain';
} else {
try {
message = JSON.stringify(message, null, ' ');
contentType = 'application/json';
} catch (e) {
message = message.toString();
contentType = 'text/plain';
}
}
if (res.statusCode == null) {
res.statusCode = 500;
}
res.setHeader('Content-Type', contentType);
if (message != null) {
res.write(message);
}
res.end();
};
RESTHandler.prototype.addSerializer = function(serializer) {
this.serializers.push(serializer);
return this;
};
RESTHandler.prototype.errorHandler = RESTHandler.prototype.addErrorSerializer = function(handler) {
this.errorHandlers.push(handler);
return this;
};
RESTHandler.prototype.send = function(rest, message) {
this.emit('beforeSend', rest, message);
for (var i = 0; i < this.serializers.length; i++) {
var serializer = this.serializers[i];
if (serializer(rest, message) || rest.isClosed()) {
return;
}
}
this.defaultSerializer(rest, message);
};
RESTHandler.prototype.error = function(rest, message) {
this.emit('requestError', rest, message);
if (rest.isClosed()) {
return;
}
for (var i = 0; i < this.errorHandlers.length; i++) {
var serializer = this.errorHandlers[i];
serializer(rest, message);
if (rest.isClosed()) {
return;
}
}
this.defaultErrorHandler(rest, message);
};
/**
* The default route invoker
*/
RESTHandler.prototype.invokeRoute = function(rest) {
// invoke the route handler function
var result = rest.route.handler(rest);
if (result !== undefined) {
if (result !== null) {
if (result.then) {
// message is a Promise
return result.then(
function(result) {
rest.send(result);
},
function(err) {
rest.error(err);
});
}
}
rest.send(result);
}
};
/**
* Replace the default route invoker with the given function.
* The given function will be given a single argument of type
* REST.
*/
RESTHandler.prototype.setRouteInvoker = function (func) {
this.invokeRoute = func;
};
module.exports = RESTHandler;