UNPKG

koa-router

Version:

Router middleware for koa. Provides RESTful resource routing.

307 lines (250 loc) 7.22 kB
/** * Dependencies */ var debug = require('debug')('koa-router') , methods = require('methods') , parse = require('url').parse , Route = require('./route'); /** * Expose `Router` */ module.exports = Router; /** * Initialize Router. * * @param {Application} app Optional. Extends app with methods such * as `app.get()`, `app.post()`, etc. * @return {Router} * @api public */ function Router(app) { if (!(this instanceof Router)) { var router = new Router(app); return router.middleware(); } this.methods = ['OPTIONS']; this.routes = []; // extend application if (app) this.extendApp(app); }; /** * Router prototype */ var router = Router.prototype; /** * Router middleware factory. Returns router middleware which dispatches route * middleware corresponding to the request. * * @param {Function} next * @return {Function} * @api public */ router.middleware = function() { var router = this; return function *dispatch(next) { var matchedRoutes; // Parameters for this route if (!(this.params instanceof Array)) { this.params = []; } debug('%s %s', this.method, this.path); // Find routes matching requested path if (matchedRoutes = router.match(this.path)) { var methodsAvailable = {}; // Find matched route for requested method for (var len = matchedRoutes.length, i=0; i<len; i++) { var route = matchedRoutes[i].route; var params = matchedRoutes[i].params; for (var l = route.methods.length, n=0; n<l; n++) { var method = route.methods[n]; methodsAvailable[method] = true; // if method and path match, dispatch route middleware if (method === this.method) { // Merge the matching routes params into context params merge(this.params, params); debug('dispatch "%s" %s', route.path, route.regexp); return yield route.middleware.call(this, next); } } } // matches path but not method, so return 405 Method Not Allowed // unless this is an OPTIONS request. this.status = (this.method === 'OPTIONS' ? 204 : 405); this.set('Allow', Object.keys(methodsAvailable).join(", ")); } else { // Could not find any route matching the requested path // simply yield to downstream koa middleware return yield next; } // a route matched the path but not method. // currently status is prepared as 204 or 405 // If the method is in fact unknown at the router level, // send 501 Not Implemented if (!~router.methods.indexOf(this.method)) { this.status = 501; } }; }; /** * Create `router.verb()` methods, where *verb* is one of the HTTP verbes such * as `router.get()` or `router.post()`. */ methods.forEach(function(method) { router[method] = function(name, path, middleware) { var args = Array.prototype.slice.call(arguments); args.splice(typeof path == 'function' ? 1 : 2, 0, [method]); this.register.apply(this, args); return this; }; }); // Alias for `router.delete()` because delete is a reserved word router.del = router['delete']; /** * Register route with all methods. * * @param {String} name Optional. * @param {String|RegExp} path * @param {Function} middleware You may also pass multiple middleware. * @return {Route} * @api public */ router.all = function(name, path, middleware) { var args = Array.prototype.slice.call(arguments); args.splice(typeof path == 'function' ? 1 : 2, 0, methods); return this.register.apply(this, args); }; /** * Redirect `path` to `destination` URL with optional 30x status `code`. * * @param {String} source URL, RegExp, or route name. * @param {String} destination URL or route name. * @param {Number} code HTTP status code (default: 301). * @return {Route} * @api public */ router.redirect = function(source, destination, code) { // lookup source route by name if (source instanceof RegExp || source[0] != '/') { source = this.url(source); } // lookup destination route by name if (destination instanceof RegExp || destination[0] != '/') { destination = this.url(destination); } return this.all(source, function *() { this.redirect(destination); this.status = code || 301; }); }; /** * Create and register a route. * * @param {String} name Optional. * @param {String|RegExp} path Path string or regular expression. * @param {Array} methods Array of HTTP verbs. * @param {Function} middleware Multiple middleware also accepted. * @return {Route} * @api public */ router.register = function(name, path, methods, middleware) { if (path instanceof Array) { middleware = Array.prototype.slice.call(arguments, 2); methods = path; path = name; name = null; } else { middleware = Array.prototype.slice.call(arguments, 3); } // create route var route = new Route(path, methods, middleware, name); // register route with router this.routes.push(route); // register route methods with router (for 501 responses) route.methods.forEach(function(method) { if (!~this.methods.indexOf(method)) { this.methods.push(method); } }, this); return route; }; /** * Lookup route with given `name`. * * @param {String} name * @return {Route|false} * @api public */ router.route = function(name) { for (var len = this.routes.length, i=0; i<len; i++) { if (this.routes[i].name == name) { return this.routes[i]; } } return false; }; /** * Generate URL for route using given `params`. * * @param {String} name route name * @param {Object} params url parameters * @return {String|Error} * @api public */ router.url = function(name, params) { var route = this.route(name); if (route) { var args = Array.prototype.slice.call(arguments, 1); return route.url.apply(route, args); } return new Error("No route found for name: " + name); }; /** * Match given `path` and return corresponding routes. * * @param {String} path * @param {Array} params populated with captured url parameters * @return {Array|false} Returns matched routes or false. * @api private */ router.match = function(path) { var routes = this.routes; var matchedRoutes = []; for (var len = routes.length, i=0; i<len; i++) { debug('test "%s" %s', routes[i].path, routes[i].regexp); var params = routes[i].match(path); if (params) { debug('match "%s" %s', routes[i].path, routes[i].regexp); matchedRoutes.push({ route: routes[i], params: params }); } } return matchedRoutes.length > 0 ? matchedRoutes : false; }; /** * Extend given `app` with router methods. * * @param {Application} app * @return {Application} * @api private */ router.extendApp = function(app) { ['all', 'redirect', 'register', 'url', 'del'].concat(methods) .forEach(function(method) { app[method] = Router.prototype[method].bind(this); }, this); return app; }; /** * Merge b into a. * * @param {Object} a * @param {Object} b * @return {Object} a * @api private */ function merge(a, b) { if (!b) return a; for (var k in b) a[k] = b[k]; return a; }