bogart-edge
Version:
Fast JSGI web framework taking inspiration from Sinatra
291 lines (233 loc) • 7.39 kB
JavaScript
var
util = require('./util'),
EventEmitter = require('events').EventEmitter,
Q = require('./q'),
when = Q.when,
inherits = require('util').inherits,
slice = Array.prototype.slice,
_ = require('underscore'),
Injector = require('bogart-injector'),
XRegExp = require('xregexp'),
Request = require('./request');
var
httpMethod = {
GET: "get",
POST: "post",
PUT: "put",
DELETE: "delete"
},
restMethod = {
SHOW: httpMethod.GET,
CREATE: httpMethod.POST,
UPDATE: httpMethod.PUT,
DESTROY: httpMethod.DELETE
},
PATH_PARAMETER_REPLACEMENT = "([^\/\?]+)",
PATH_PARAMETERS = /:([\w\d]+)/g;
exports.bogartEvent = {
BEFORE_ADD_ROUTE: "beforeAddRoute",
AFTER_ADD_ROUTE: "afterAddRoute"
};
exports.Router = Router;
function Router() {
if (!this.respond) {
return new Router();
}
var settings = {}
var app = function (injector) {
if (!injector.has('req')) {
throw new Error('Bogart Router requires an injector ' +
'with a request dependency registered under key `req`');
}
injector.value('req', new Request(injector.resolve('req')));
return app.respond(injector.invoke.bind(injector));
};
app.routes = {};
app.beforeCallbacks = [];
app.afterCallbacks = [];
app.setting = function(name, val) {
if (val === undefined) {
return settings[name];
}
settings[name] = val;
return this;
};
EventEmitter.call(app);
_.extend(app, this.__proto__);
return app;
}
inherits(Router, EventEmitter);
/**
* Register a callback to happen before bogart handlers are invoked to
* handle a request. Multiple 'before' callbacks may be registered.
*
* @param {Function} cb Callback to happen before route handler is invoked.
*/
Router.prototype.before = function(cb) {
this.beforeCallbacks.push(cb);
};
/**
* Register a callback to happen after bogart handlers are invoked to
* handle a request. Multiple 'after' callbacks may be registered.
*
* @param {Function} cb Callback to happen after route handler is invoked.
*/
Router.prototype.after = function(cb) {
this.afterCallbacks.push(cb);
};
/**
* Register a route
* @param {String} method Http Verb e.g. 'GET', 'POST', 'PUT', 'DELETE'
* @param {String} path Path for the route
* @param {Function} handler Function to execute when the route is accessed
*/
Router.prototype.route = function(method, path /*, handlers... */) {
var paramNames, route, originalPath = path
, args = slice.call(arguments);
method = args.shift();
path = args.shift();
apps = args;
if (path.constructor === String) {
paramNames = path.match(PATH_PARAMETERS) || [];
paramNames = paramNames.map(function(x) { return x.substring(1); });
path = new RegExp("^"+path.replace(/\./, '\\.').replace(/\*/g, '(.+)').replace(PATH_PARAMETERS, PATH_PARAMETER_REPLACEMENT)+'$');
}
route = makeRoute({ path: path, paramNames: paramNames, apps: apps, originalPath: originalPath });
// this.emit not supported for some reason
// this.emit(exports.bogartEvent.BEFORE_ADD_ROUTE, this, route);
this.routes[method] = this.routes[method] || [];
this.routes[method].push(route);
// this.emit(exports.bogartEvent.AFTER_ADD_ROUTE, this, route);
return this;
};
Router.prototype.handler = function(verb, path) {
verb = verb.toLowerCase();
var route;
if (this.routes[verb]) {
for (var i=0;i<this.routes[verb].length;i++) {
route = this.routes[verb][i];
if (route.path.test(path) || route.path.test(decodeURIComponent(path))) {
return route;
}
}
}
if (path === '') {
return this.handler(verb, '/');
}
return null;
};
Router.prototype.respond = function (invoke) {
var self = this
, noRouteMatchesReason = 'No route matches request'
var route = invoke(function (req) {
var route = self.handler(req.method, req.pathInfo);
if (route) {
route.bindRouteParametersToRequest(req);
}
return route;
});
if (util.no(route)) {
return next(invoke);
}
var allBefores = Q.all(self.beforeCallbacks.map(invoke));
return allBefores
.then(function () {
var stackApps = stack(invoke, route.apps);
return stackApps();
})
.then(function(resp) {
var allAfters = Q.all(self.afterCallbacks.map(invoke));
return allAfters.then(thenResolve(resp));
})
.then(function(resp) {
if (util.no(resp)) {
return next(invoke);
}
return resp;
});
};
Router.prototype.show = Router.prototype.get = function(path /*, jsgiApps */) {
return this.route.apply(this, [ restMethod.SHOW ].concat(slice.call(arguments)));
};
Router.prototype.create = Router.prototype.post = function(path /*, jsgiApps */) {
return this.route.apply(this, [ restMethod.CREATE ].concat(slice.call(arguments)));
};
Router.prototype.update = Router.prototype.put = function(path /*, jsgiApps */) {
return this.route.apply(this, [ restMethod.UPDATE ].concat(slice.call(arguments)));
};
Router.prototype.destroy = Router.prototype.del = function(path /*, jsgiApps */) {
return this.route.apply(this, [ restMethod.DESTROY ].concat(slice.call(arguments)));
};
Router.prototype.isRouter = true;
/**
* Takes a variadic number of callbacks and chains them together.
*
* @param {Function} invoke function used to execute the callbacks
* @param {Array} callbacks array of callbacks
* @returns {Function} A function that takes a request and executes a sequence of callbacks.
*/
function stack(invoke, callbacks) {
if (callbacks.length === 0) {
return function () {
}
}
callbacks = callbacks.concat();
function next() {
var cb = callbacks.shift();
var locals = callbacks.length > 0 ? { next: next } : {};
return invoke(cb, null, locals);
}
return function () {
return next();
};
}
function getParameterNames(fn) {
var source = fn.toString();
var signature = source.slice(source.indexOf('(') + 1, source.indexOf(')'));
return signature.match(/([^\s,]+)/g);
}
exports.stack = stack;
/**
* Returns a function that returns @param val.
*
* @param val The value to return
* @returns {Function} A function that returns `val`.
*/
function thenResolve(val) {
return function() {
return val;
}
}
function next(invoke) {
return invoke(function (next) {
if (next !== null) {
return next();
}
});
}
function makeRoute(proto) {
return Object.create(proto, {
bindRouteParametersToRequest: {
value: function(req) {
var route = this
, routeParamValues = XRegExp.exec(req.pathInfo, route.path);
if (routeParamValues) {
routeParamValues.shift(); // Remove the initial match
for (var key of Object.keys(routeParamValues)){
req.routeParams[key] = routeParamValues[key]
}
routeParamValues
.map(decodeURIComponent)
.forEach(function(val, indx) {
if (route.paramNames && route.paramNames.length > indx) {
req.routeParams[route.paramNames[indx]] = val;
} else if (val !== undefined) {
req.routeParams.splat = req.routeParams.splat || [];
req.routeParams.splat.push(val);
}
});
}
}
}
});
}