mjackson/mach

View on GitHub
modules/middleware/router.js

Summary

Maintainability
C
1 day
Test Coverage
/* jshint -W084 */
var d = require('describe-property');
var objectAssign = require('object-assign');
var compileRoute = require('../utils/compileRoute');
var isRegExp = require('../utils/isRegExp');
var makeParams = require('../utils/makeParams');
var RoutingProperties = require('../utils/RoutingProperties');

var LEADING_HTTP_METHOD_MATCHER = /^(DELETE|GET|HEAD|OPTIONS|POST|PUT|TRACE)\s+(.+)$/;

/**
 * A middleware that provides pattern-based routing for URLs, with optional
 * support for restricting matches to a specific request method. Named segments
 * of the URL are added to conn.params and take precedence over all others.
 *
 *   app.use(mach.router, {
 *
 *     'GET /login': function (conn) {
 *       // conn.method == 'GET'
 *       // conn.pathname == '/login'
 *     },
 *
 *     'POST /login': function (conn) {
 *       // conn.method == 'POST'
 *       // conn.pathname == '/login'
 *     },
 *
 *     'DELETE /users/:id': function (conn) {
 *       // conn.method == 'DELETE'
 *       // conn.pathname == '/users/5'
 *       // conn.params == { id: 5 }
 *     }
 *
 *   });
 *
 * This function may also be used outside the context of a middleware stack
 * to create a standalone app. Routes may be given one at a time:
 *
 *   var app = mach.router();
 *
 *   app.get('/login', function (conn) {
 *     // ...
 *   });
 *
 *   app.delete('/users/:id', function (conn) {
 *     // ...
 *   });
 *
 * Or all at once:
 *
 *   var app = mach.router({
 *
 *     'GET /login': function (conn) {
 *       // ...
 *     },
 *
 *     'DELETE /users/:id': function (conn) {
 *       // ...
 *     }
 *
 *   });
 *
 * Note: Routes are always tried in the order they were defined.
 */
function createRouter(app, map) {
  // Allow mach.router(map)
  if (typeof app === 'object') {
    map = app;
    app = null;
  }

  var routes = {};

  function router(conn) {
    var method = conn.method;
    var routesToTry = (routes[method] || []).concat(routes.ANY || []);

    var route, match;
    for (var i = 0, len = routesToTry.length; i < len; ++i) {
      route = routesToTry[i];

      // Try to match the route.
      if (match = route.pattern.exec(conn.pathname)) {
        var params = makeParams(route.keys, Array.prototype.slice.call(match, 1));

        if (conn.params) {
          // Route params take precedence above all others.
          objectAssign(conn.params, params);
        } else {
          conn.params = params;
        }

        return conn.call(route.app);
      }
    }

    return conn.call(app);
  }

  Object.defineProperties(router, {

    /**
     * Adds a new route that runs the given app when the pattern matches the
     * path used in the request. If the pattern is a string, it is automatically
     * compiled. The following signatures are supported:
     *
     *   route('/users/:id', app)
     *   route('/users/:id', 'PUT', app)
     *   route('/users/:id', [ 'GET', 'PUT' ], app)
     *   route('GET /users/:id', app)
     */
    route: d(function (pattern, methods, app) {
      if (typeof methods === 'function') {
        app = methods;
        methods = null;
      }

      if (typeof app !== 'function')
        throw new Error('Route needs an app');

      if (typeof methods === 'string') {
        methods = [ methods ];
      } else if (!Array.isArray(methods)) {
        methods = [];
      }

      var keys = [];

      if (typeof pattern === 'string') {
        var match;

        if (match = pattern.match(LEADING_HTTP_METHOD_MATCHER)) {
          methods.push(match[1]);
          pattern = match[2];
        }

        pattern = compileRoute(pattern, keys);
      }

      if (!isRegExp(pattern))
        throw new Error('Route pattern must be a RegExp');

      var route = { pattern: pattern, keys: keys, app: app };

      if (methods.length === 0)
        methods.push('ANY');

      methods.forEach(function (method) {
        var upperMethod = method.toUpperCase();

        if (routes[upperMethod]) {
          routes[upperMethod].push(route);
        } else {
          routes[upperMethod] = [ route ];
        }
      });
    }),

    /**
     * Sets the given app as the default for this router.
     */
    run: d(function (downstreamApp) {
      app = downstreamApp;
    })

  });

  // Allow app.use(mach.router, map)
  if (typeof map === 'object')
    for (var route in map)
      if (map.hasOwnProperty(route))
        router.route(route, map[route]);

  Object.defineProperties(router, RoutingProperties);

  return router;
}

module.exports = createRouter;