UNPKG

named-routes

Version:

Named routes for node.js. Works with express and independently

337 lines (300 loc) 10.1 kB
var Route = require('./route'); exports = module.exports = Router; /** * Initialize a new `Router` with the given `options`. * * @param {Object} options * @api private */ function Router(options) { var self = this; options = options || {}; this.expressMode = false; this.routesByMethod = {}; this.routesByMethodAndPath = {}; this.routesByNameAndMethod = {}; this.callbacksByPathAndMethod = {}; this.parameterCallbackWrappers = []; this.callbacksPerParameter = {}; // Alias for `dispatch()` to match the express.js concept this.middleware = function router(req, res, next) { self.dispatch(req, res, next); }; this.caseSensitive = options.caseSensitive == undefined ? false : options.caseSensitive; } /** * Find matching route * * @param req * @return {boolean|Object} */ Router.prototype.match = function (req) { var method = req.method.toLowerCase(); var routes = this.routesByMethod[method]; if (!routes) return false; var offset = req.route == undefined ? 0 : routes.indexOf(req.route) + 1; // Performance: Lazy matching. Only match 1 route at a time. Most often the first route will generate the server response for (var i = offset; i < routes.length; i++) { var route = routes[i]; var outcome = route.match(req.path); if (outcome === false) continue; // guarantee array of parameters outcome = outcome === true ? [] : outcome; return { route: route, callbacks:this.callbacksByPathAndMethod[route.path][method], params: outcome } } return false; } /** * Registers new route * @param method * @param path * @param callbacks * @param options */ Router.prototype.add = function (method, path, callbacks, options) { function flatten(arr, ret) { var ret = ret || [] , len = arr.length; for (var i = 0; i < len; ++i) { if (Array.isArray(arr[i])) { flatten(arr[i], ret); } else { ret.push(arr[i]); } } return ret; } callbacks = [callbacks] method = method.toLowerCase(); options = options || {}; this.routesByMethodAndPath[method] = this.routesByMethodAndPath[method] || {}; options.caseSensitive = options.caseSensitive == undefined ? this.caseSensitive : options.caseSensitive; if (this.routesByMethodAndPath[method][path] == undefined) { var route = new Route(path, options); route.__defineGetter__('name', function() {return this.options.name}); if (this.expressMode) { route.generate = expressGenerateRoute(require("path-to-regexp").compile(path)); } this.routesByMethod[method] = this.routesByMethod[method] || []; this.routesByMethod[method].push(route); this.routesByMethodAndPath[method][path] = route; if (options.name != undefined) { this.routesByNameAndMethod[options.name] = this.routesByNameAndMethod[options.name] || {}; this.routesByNameAndMethod[options.name][method] = route; } } else { this.routesByMethodAndPath[method][path].setOptions(options); } this.callbacksByPathAndMethod[path] = this.callbacksByPathAndMethod[path] || {}; this.callbacksByPathAndMethod[path][method] = flatten(callbacks); } var expressGenerateRoute = function(compile) { return function(userParams) { var foundAtLeastOneNull = false; if (userParams) { var keys = Object.keys(userParams); for (var i = 0; i < keys.length; i++) { if (userParams[keys[i]] === null) { userParams[keys[i]] = "__NULL_PLACEHOLDER__"; foundAtLeastOneNull = true; } } } var routeName = compile(userParams); if (!foundAtLeastOneNull) return routeName; for (var i = 0; i < keys.length; i++) { if (userParams[keys[i]] === "__NULL_PLACEHOLDER__") { userParams[keys[i]] = null; } } return routeName.replace(/\/__NULL_PLACEHOLDER__/g, ""); } } /** * Builds a URL based on the route name, method and parameters provided * * @param name * @param params * @param method * @return {String} */ Router.prototype.build = function (name, params, method) { if (this.routesByNameAndMethod[name] == undefined) throw new Error('No route found with the name:' + name); var possibleRoutes = this.routesByNameAndMethod[name]; method = method || Object.keys(possibleRoutes)[0]; return possibleRoutes[method].generate(params); } /** * Register template helper functions with exress.js application * @param app * @return Router */ Router.prototype.registerAppHelpers = function (app) { var self = this; var helperName = 'url'; if (app.helpers) { var helpers = {}; helpers[helperName] = function(name, params, method) { return self.build(name, params, method)}; app.helpers(helpers); } else { app.locals[helperName] = function(name, params, method) { return self.build(name, params, method)}; } return this; } /** * Register a param callback `callback` for the given parameter `name`. * * @param {String|Function} name * @param {Function} callback * @return {*|Router} for chaining * @api public */ Router.prototype.param = function (name, callback) { // No name passed, add a modifier if ('function' == typeof name) { this.parameterCallbackWrappers.push(name); return; } // apply param functions var callbackWrappers = this.parameterCallbackWrappers; var modifiedCallback; callbackWrappers.forEach(function (wrapper) { if (modifiedCallback = wrapper(name, callback)) { callback = modifiedCallback; } }); // ensure we end up with a // middleware function if (typeof callback != 'function') { throw new Error('invalid callback call for `' + name + '`, got ' + callback); } (this.callbacksPerParameter[name] = this.callbacksPerParameter[name] || []).push(callback); return this; }; /** * Chainable routing dispatch * (analogous to express middeware concept) * * @param req * @param res * @param next */ Router.prototype.dispatch = function (req, res, next) { var self = this; var callbacksPerParameter = this.callbacksPerParameter; nextRoute(); function nextRoute(err) { // match route var match = self.match(req); if (match == false) return next(err); req.route = match.route; req.params = match.params; // workaround vars for the recursion var i = 0; var paramIndex = 0; var paramCallbackIndex; var parameterNames = Object.keys(req.params); var paramValue; var paramName; var paramCallbacks; //Start the execution chain. Process parameters, next invoke route middleware callbacks processNextParameter(); // Callbacks for each parameter // We need this to be recursive rather then a loop in order to allow the fluid `next()` concept from the actual callbacks function processNextParameter(err) { paramCallbackIndex = 0; paramName = parameterNames[paramIndex++]; paramValue = paramName && req.params[paramName]; paramCallbacks = paramName && callbacksPerParameter[paramName]; try { if ('route' == err) { // Specific case where the error means `next route please`? Strange, anyways this is inherited by express.js nextRoute(); } else if (err) { // Handle errors. Assumption is made that there's a global error handler or a specific route callback that handle the errors nextRouteMiddleware(err); } else if (paramCallbacks) { // No errors so just run through the parameter callbacks if there's any nextCallbackForParam(); } else if (paramName) { // No callbacks processNextParameter(); } else { // Parameter callbacks ended, start running route middleware callbacks nextRouteMiddleware(); } } catch (err) { processNextParameter(err); } } // single param callbacks function nextCallbackForParam(err) { var callback = paramCallbacks[paramCallbackIndex++]; if (err || !callback) return processNextParameter(err); callback(req, res, nextCallbackForParam, paramValue, paramName); } // invoke route callbacks function nextRouteMiddleware(err) { var callback = match.callbacks[i++]; try { if ('route' == err) { // Specific case where the error means `next route please`? Strange.. anyways this is inherited by express.js nextRoute(); } else if (err && callback) { // Handle errors. If the current callback doesn't support error handling try next one if (callback.length < 4) return nextRouteMiddleware(err); callback(err, req, res, nextRouteMiddleware); } else if (callback) { callback(req, res, nextRouteMiddleware); } else { // No more callbacks nextRoute(err); } } catch (err) { nextRouteMiddleware(err); } } } }; /** * Extend express.js application * @param app * @return Router */ Router.prototype.extendExpress = function (app) { var methods = require('methods'); this.expressMode = true; app.namedRoutes = this; app._routingContext = []; methods.forEach(function (method) { var originalMethod = app[method]; app[method] = function (key) { if ('get' == method && 1 == arguments.length && typeof key == 'string') return this.set(key); var args = this._routingContext.concat([].slice.call(arguments)); var path = args[0]; var name = ""; // Check if second argument is the route name if (typeof args[1] == 'string') { name = args[1]; args[1] = function(req, res, next){ req.route.name = name; next(); }; } this.namedRoutes.add(method, path, [], {name: name}); return originalMethod.apply(this, args); } }); app.all = function() { var methods = require('methods'); var args = [].slice.call(arguments); return methods.forEach(function(method){ app[method].apply(app, args); }); }; return this; }