mach
Version:
HTTP for JavaScript
175 lines (146 loc) • 4.51 kB
JavaScript
;
/* 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;