router.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. /**
  2. * Dependencies
  3. */
  4. var debug = require('debug')('koa-router')
  5. , methods = require('methods')
  6. , Route = require('./route');
  7. /**
  8. * Expose `Router`
  9. */
  10. module.exports = Router;
  11. /**
  12. * Initialize Router.
  13. *
  14. * @param {Application=} app Optional. Extends app with methods such
  15. * as `app.get()`, `app.post()`, etc.
  16. * @param {Object=} opts Optional. Passed to `path-to-regexp`.
  17. * @return {Router}
  18. * @api public
  19. */
  20. function Router(app, opts) {
  21. if (!(this instanceof Router)) {
  22. var router = new Router(app, opts);
  23. return router.middleware();
  24. }
  25. if (app && !app.use) {
  26. opts = app;
  27. app = null;
  28. }
  29. this.opts = opts || {};
  30. this.methods = ['OPTIONS'];
  31. this.routes = [];
  32. this.params = {};
  33. // extend application
  34. if (app) this.extendApp(app);
  35. };
  36. /**
  37. * Router prototype
  38. */
  39. var router = Router.prototype;
  40. /**
  41. * Router middleware factory. Returns router middleware which dispatches route
  42. * middleware corresponding to the request.
  43. *
  44. * @param {Function} next
  45. * @return {Function}
  46. * @api public
  47. */
  48. router.middleware = function() {
  49. var router = this;
  50. return function *dispatch(next) {
  51. var matchedRoutes;
  52. // Parameters for this route
  53. if (!(this.params instanceof Array)) {
  54. this.params = [];
  55. }
  56. var pathname = router.opts.routerPath || this.routerPath || this.path;
  57. debug('%s %s', this.method, pathname);
  58. // Find routes matching requested path
  59. if (matchedRoutes = router.match(pathname)) {
  60. var methodsAvailable = {};
  61. // Find matched route for requested method
  62. for (var len = matchedRoutes.length, i=0; i<len; i++) {
  63. var route = matchedRoutes[i].route;
  64. var params = matchedRoutes[i].params;
  65. for (var l = route.methods.length, n=0; n<l; n++) {
  66. var method = route.methods[n];
  67. methodsAvailable[method] = true;
  68. // if method and path match, dispatch route middleware
  69. if (method === this.method) {
  70. this.route = route;
  71. // Merge the matching routes params into context params
  72. merge(this.params, params);
  73. debug('dispatch "%s" %s', route.path, route.regexp);
  74. return yield *route.middleware.call(this, next);
  75. }
  76. }
  77. }
  78. // matches path but not method, so return 405 Method Not Allowed
  79. // unless this is an OPTIONS request.
  80. this.status = (this.method === 'OPTIONS' ? 204 : 405);
  81. this.set('Allow', Object.keys(methodsAvailable).join(", "));
  82. }
  83. else {
  84. // Could not find any route matching the requested path
  85. // simply yield to downstream koa middleware
  86. return yield *next;
  87. }
  88. // a route matched the path but not method.
  89. // currently status is prepared as 204 or 405
  90. // If the method is in fact unknown at the router level,
  91. // send 501 Not Implemented
  92. if (!~router.methods.indexOf(this.method)) {
  93. this.status = 501;
  94. }
  95. };
  96. };
  97. /**
  98. * Create `router.verb()` methods, where *verb* is one of the HTTP verbes such
  99. * as `router.get()` or `router.post()`.
  100. */
  101. methods.forEach(function(method) {
  102. router[method] = function(name, path, middleware) {
  103. var args = Array.prototype.slice.call(arguments);
  104. if ((typeof path === 'string') || (path instanceof RegExp)) {
  105. args.splice(2, 0, [method]);
  106. } else {
  107. args.splice(1, 0, [method]);
  108. }
  109. this.register.apply(this, args);
  110. return this;
  111. };
  112. });
  113. // Alias for `router.delete()` because delete is a reserved word
  114. router.del = router['delete'];
  115. /**
  116. * Register route with all methods.
  117. *
  118. * @param {String} name Optional.
  119. * @param {String|RegExp} path
  120. * @param {Function} middleware You may also pass multiple middleware.
  121. * @return {Route}
  122. * @api public
  123. */
  124. router.all = function(name, path, middleware) {
  125. var args = Array.prototype.slice.call(arguments);
  126. args.splice(typeof path == 'function' ? 1 : 2, 0, methods);
  127. this.register.apply(this, args);
  128. return this;
  129. };
  130. /**
  131. * Redirect `path` to `destination` URL with optional 30x status `code`.
  132. *
  133. * @param {String} source URL, RegExp, or route name.
  134. * @param {String} destination URL or route name.
  135. * @param {Number} code HTTP status code (default: 301).
  136. * @return {Route}
  137. * @api public
  138. */
  139. router.redirect = function(source, destination, code) {
  140. // lookup source route by name
  141. if (source instanceof RegExp || source[0] != '/') {
  142. source = this.url(source);
  143. }
  144. // lookup destination route by name
  145. if (destination instanceof RegExp || destination[0] != '/') {
  146. destination = this.url(destination);
  147. }
  148. return this.all(source, function *() {
  149. this.redirect(destination);
  150. this.status = code || 301;
  151. });
  152. };
  153. /**
  154. * Create and register a route.
  155. *
  156. * @param {String} name Optional.
  157. * @param {String|RegExp} path Path string or regular expression.
  158. * @param {Array} methods Array of HTTP verbs.
  159. * @param {Function} middleware Multiple middleware also accepted.
  160. * @return {Route}
  161. * @api public
  162. */
  163. router.register = function(name, path, methods, middleware) {
  164. if (path instanceof Array) {
  165. middleware = Array.prototype.slice.call(arguments, 2);
  166. methods = path;
  167. path = name;
  168. name = null;
  169. }
  170. else {
  171. middleware = Array.prototype.slice.call(arguments, 3);
  172. }
  173. // create route
  174. var route = new Route(path, methods, middleware, name, this.opts);
  175. // add parameter middleware
  176. Object.keys(this.params).forEach(function(param) {
  177. route.param(param, this.params[param]);
  178. }, this);
  179. // register route with router
  180. this.routes.push(route);
  181. // register route methods with router (for 501 responses)
  182. route.methods.forEach(function(method) {
  183. if (!~this.methods.indexOf(method)) {
  184. this.methods.push(method);
  185. }
  186. }, this);
  187. return route;
  188. };
  189. /**
  190. * Lookup route with given `name`.
  191. *
  192. * @param {String} name
  193. * @return {Route|false}
  194. * @api public
  195. */
  196. router.route = function(name) {
  197. for (var len = this.routes.length, i=0; i<len; i++) {
  198. if (this.routes[i].name == name) {
  199. return this.routes[i];
  200. }
  201. }
  202. return false;
  203. };
  204. /**
  205. * Generate URL for route using given `params`.
  206. *
  207. * @param {String} name route name
  208. * @param {Object} params url parameters
  209. * @return {String|Error}
  210. * @api public
  211. */
  212. router.url = function(name, params) {
  213. var route = this.route(name);
  214. if (route) {
  215. var args = Array.prototype.slice.call(arguments, 1);
  216. return route.url.apply(route, args);
  217. }
  218. return new Error("No route found for name: " + name);
  219. };
  220. /**
  221. * Match given `path` and return corresponding routes.
  222. *
  223. * @param {String} path
  224. * @param {Array} params populated with captured url parameters
  225. * @return {Array|false} Returns matched routes or false.
  226. * @api private
  227. */
  228. router.match = function(path) {
  229. var routes = this.routes;
  230. var matchedRoutes = [];
  231. for (var len = routes.length, i=0; i<len; i++) {
  232. debug('test "%s" %s', routes[i].path, routes[i].regexp);
  233. var params = routes[i].match(path);
  234. if (params) {
  235. debug('match "%s" %s', routes[i].path, routes[i].regexp);
  236. matchedRoutes.push({ route: routes[i], params: params });
  237. }
  238. }
  239. return matchedRoutes.length > 0 ? matchedRoutes : false;
  240. };
  241. router.param = function(param, fn) {
  242. this.params[param] = fn;
  243. this.routes.forEach(function(route) {
  244. route.param(param, fn);
  245. });
  246. return this;
  247. };
  248. /**
  249. * Extend given `app` with router methods.
  250. *
  251. * @param {Application} app
  252. * @return {Application}
  253. * @api private
  254. */
  255. router.extendApp = function(app) {
  256. var router = this;
  257. app.url = router.url.bind(router);
  258. app.router = router;
  259. ['all', 'redirect', 'register', 'del', 'param']
  260. .concat(methods)
  261. .forEach(function(method) {
  262. app[method] = function() {
  263. router[method].apply(router, arguments);
  264. return this;
  265. };
  266. });
  267. return app;
  268. };
  269. /**
  270. * Merge b into a.
  271. *
  272. * @param {Object} a
  273. * @param {Object} b
  274. * @return {Object} a
  275. * @api private
  276. */
  277. function merge(a, b) {
  278. if (!b) return a;
  279. for (var k in b) a[k] = b[k];
  280. return a;
  281. }