UNPKG

strata

Version:

A modular, streaming HTTP server

167 lines (139 loc) 4 kB
var util = require('util'); var utils = require('./utils'); /** * A middleware that provides pattern-based routing for URL's, with optional * support for restricting matches to a specific request method. Populates * `env.route` with an object containing the results of the match for all * downstream apps. * * var app = strata.router(); * * app.get('/', function (env, callback) { * callback(200, {}, 'Welcome home!'); * }); * * app.get('/login', function (env, callback) { * callback(200, {}, 'Please login.'); * }); * * app.post('/login', function (env, callback) { * // login logic goes here... * }); * * app.get('/users/:id', function (env, callback) { * var userId = env.route.id; * // find the user with the given id... * }); * * Note: All routes are tried in the order they were defined. */ module.exports = function (app) { app = app || utils.notFound; var routes = {}; function router(env, callback) { var method = env.requestMethod; var localRoutes = (routes[method] || []).concat(routes.ANY || []); var tryRoute = tryRoutes(localRoutes, env, callback, app); tryRoute(0); } /** * Routes all requests with a pathInfo that match the given pattern to the * given app. If the pattern is a string, it is compiled using * `utils.compileRoute`. * * If methods are given, the match will only occur when the requestMethod * matches one of those methods. Otherwise, the route is not restricted to a * certain request method. */ router.route = function (pattern, app, methods) { methods = methods || ['ANY']; if (typeof methods === 'string') { methods = [methods]; } var keys = []; if (typeof pattern === 'string') { pattern = utils.compileRoute(pattern, keys); } if (!util.isRegExp(pattern)) { throw new Error('Pattern must be a RegExp'); } var method; for (var i = 0, len = methods.length; i < len; ++i) { method = methods[i].toUpperCase(); if (!routes[method]) { routes[method] = []; } routes[method].push([pattern, keys, app]); } }; // Add sugar methods for common HTTP verbs. Note that get defines // routes for both GET *and* HEAD requests. var methods = { get: ['GET', 'HEAD'], post: 'POST', put: 'PUT', delete: 'DELETE', head: 'HEAD', options: 'OPTIONS' }; for (var method in methods) { (function (verbs) { router[method] = function (pattern, app) { this.route(pattern, app, verbs); }; })(methods[method]); } /** * Specifies the app to run when no routes match. */ router.run = function (localApp) { app = localApp; }; return router; }; function tryRoutes(routes, env, callback, defaultApp) { function tryRoute(n) { var route = routes[n]; if (!route) { defaultApp(env, callback); return; } var pattern = route[0]; var keys = route[1]; var app = route[2]; var match = pattern.exec(env.pathInfo); if (match) { var obj = Array.prototype.slice.call(match, 0); // Define getters/setters for named keys. for (var i = 0, len = keys.length; i < len; ++i) { (function (i) { var idx = i + 1; obj.__defineGetter__(keys[i], function () { return this[idx]; }); obj.__defineSetter__(keys[i], function (value) { this[idx] = value; }); })(i); } var oldRoute = env.route; env.route = obj; app(env, function (status, headers, body) { // Restore the old route for upstream apps. if (oldRoute) { env.route = oldRoute; } else { delete env.route; } if (headers['X-Cascade'] == 'pass') { tryRoute(n + 1); } else { callback(status, headers, body); } }); } else { tryRoute(n + 1); } } return tryRoute; }