route.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /**
  2. * Dependencies
  3. */
  4. var compose = require('koa-compose')
  5. , debug = require('debug')('koa-router')
  6. , pathToRegexp = require('path-to-regexp');
  7. /**
  8. * Expose `Route`.
  9. */
  10. module.exports = Route;
  11. /**
  12. * Initialize a new Route with given `method`, `path`, and `middleware`.
  13. *
  14. * @param {String|RegExp} path Path string or regular expression.
  15. * @param {Array} methods Array of HTTP verbs.
  16. * @param {Array} middleware Route callback/middleware or series of.
  17. * @param {String} name Optional.
  18. * @param {Object=} opts Optional. Passed to `path-to-regexp`.
  19. * @return {Route}
  20. * @api private
  21. */
  22. function Route(path, methods, middleware, name, opts) {
  23. this.name = name || null;
  24. this.methods = [];
  25. methods.forEach(function(method) {
  26. this.methods.push(method.toUpperCase());
  27. }, this);
  28. this.params = [];
  29. this.fns = {
  30. params: {},
  31. middleware: []
  32. };
  33. if (path instanceof RegExp) {
  34. this.path = path.source;
  35. this.regexp = path;
  36. }
  37. else {
  38. this.path = path;
  39. this.regexp = pathToRegexp(path, this.params, opts);
  40. }
  41. // ensure middleware is a function
  42. middleware.forEach(function(fn) {
  43. var type = (typeof fn);
  44. if (type != 'function') {
  45. throw new Error(
  46. methods.toString() + " `" + (name || path) +"`: `middleware` "
  47. + "must be a function, not `" + type + "`"
  48. );
  49. }
  50. });
  51. if (middleware.length > 1) {
  52. this.middleware = compose(middleware);
  53. }
  54. else {
  55. this.middleware = middleware[0];
  56. }
  57. this.fns.middleware = middleware;
  58. debug('defined route %s %s', this.methods, this.path);
  59. };
  60. /**
  61. * Route prototype
  62. */
  63. var route = Route.prototype;
  64. /**
  65. * Check if given request `path` matches route,
  66. * and if so populate `route.params`.
  67. *
  68. * @param {String} path
  69. * @return {Array} of matched params or null if not matched
  70. * @api private
  71. */
  72. route.match = function(path) {
  73. if (this.regexp.test(path)) {
  74. var params = [];
  75. var captures = [];
  76. // save route capture groups
  77. var matches = path.match(this.regexp);
  78. if (matches && matches.length > 0) {
  79. captures = matches.slice(1);
  80. }
  81. if (this.params.length) {
  82. // If route has parameterized capture groups,
  83. // use parameter names for properties
  84. for (var len = captures.length, i=0; i<len; i++) {
  85. if (this.params[i]) {
  86. var c = captures[i];
  87. params[this.params[i].name] = c ? safeDecodeURIComponent(c) : c;
  88. }
  89. }
  90. }
  91. else {
  92. for (var i=0, len=captures.length; i<len; i++) {
  93. var c = captures[i];
  94. params[i] = c ? safeDecodeURIComponent(c) : c;
  95. }
  96. }
  97. return params;
  98. }
  99. return null;
  100. };
  101. /**
  102. * Generate URL for route using given `params`.
  103. *
  104. * @example
  105. *
  106. * var route = new Route(['GET'], '/users/:id', fn);
  107. *
  108. * route.url({ id: 123 });
  109. * // => "/users/123"
  110. *
  111. * @param {Object} params url parameters
  112. * @return {String}
  113. * @api private
  114. */
  115. route.url = function(params) {
  116. var args = params;
  117. var url = this.path;
  118. // argument is of form { key: val }
  119. if (typeof params != 'object') {
  120. args = Array.prototype.slice.call(arguments);
  121. }
  122. if (args instanceof Array) {
  123. for (var len = args.length, i=0; i<len; i++) {
  124. url = url.replace(/:[^\/]+/, args[i]);
  125. }
  126. }
  127. else {
  128. for (var key in args) {
  129. url = url.replace(':' + key, args[key]);
  130. }
  131. }
  132. url.split('/').forEach(function(component) {
  133. url = url.replace(component, encodeURIComponent(component));
  134. });
  135. return url;
  136. };
  137. /**
  138. * Run validations on route named parameters.
  139. *
  140. * @example
  141. *
  142. * router
  143. * .param('user', function *(id, next) {
  144. * this.user = users[id];
  145. * if (!user) return this.status = 404;
  146. * yield next;
  147. * })
  148. * .get('/users/:user', function *(next) {
  149. * this.body = this.user;
  150. * });
  151. *
  152. * @param {String} param
  153. * @param {Function *(id, next)} fn
  154. * @api public
  155. */
  156. route.param = function(param, fn) {
  157. var middleware = [];
  158. this.fns.params[param] = function *(next) {
  159. yield *fn.call(this, this.params[param], next);
  160. };
  161. this.params.forEach(function(param) {
  162. var fn = this.fns.params[param.name];
  163. if (fn) {
  164. middleware.push(fn);
  165. }
  166. }, this);
  167. this.middleware = compose(middleware.concat(this.fns.middleware));
  168. return this;
  169. };
  170. /**
  171. * Safe decodeURIComponent, won't throw any error.
  172. * If `decodeURIComponent` error happen, just return the original value.
  173. *
  174. * @param {String} text
  175. * @return {String} URL decode original string.
  176. */
  177. function safeDecodeURIComponent(text) {
  178. try {
  179. return decodeURIComponent(text);
  180. } catch (e) {
  181. return text;
  182. }
  183. }