layer.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. var debug = require('debug')('koa-router');
  2. var pathToRegExp = require('path-to-regexp');
  3. var uri = require('urijs');
  4. module.exports = Layer;
  5. /**
  6. * Initialize a new routing Layer with given `method`, `path`, and `middleware`.
  7. *
  8. * @param {String|RegExp} path Path string or regular expression.
  9. * @param {Array} methods Array of HTTP verbs.
  10. * @param {Array} middleware Layer callback/middleware or series of.
  11. * @param {Object=} opts
  12. * @param {String=} opts.name route name
  13. * @param {String=} opts.sensitive case sensitive (default: false)
  14. * @param {String=} opts.strict require the trailing slash (default: false)
  15. * @returns {Layer}
  16. * @private
  17. */
  18. function Layer(path, methods, middleware, opts) {
  19. this.opts = opts || {};
  20. this.name = this.opts.name || null;
  21. this.methods = [];
  22. this.paramNames = [];
  23. this.stack = Array.isArray(middleware) ? middleware : [middleware];
  24. methods.forEach(function(method) {
  25. var l = this.methods.push(method.toUpperCase());
  26. if (this.methods[l-1] === 'GET') {
  27. this.methods.unshift('HEAD');
  28. }
  29. }, this);
  30. // ensure middleware is a function
  31. this.stack.forEach(function(fn) {
  32. var type = (typeof fn);
  33. if (type !== 'function') {
  34. throw new Error(
  35. methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
  36. + "must be a function, not `" + type + "`"
  37. );
  38. }
  39. }, this);
  40. this.path = path;
  41. this.regexp = pathToRegExp(path, this.paramNames, this.opts);
  42. debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
  43. };
  44. /**
  45. * Returns whether request `path` matches route.
  46. *
  47. * @param {String} path
  48. * @returns {Boolean}
  49. * @private
  50. */
  51. Layer.prototype.match = function (path) {
  52. return this.regexp.test(path);
  53. };
  54. /**
  55. * Returns map of URL parameters for given `path` and `paramNames`.
  56. *
  57. * @param {String} path
  58. * @param {Array.<String>} captures
  59. * @param {Object=} existingParams
  60. * @returns {Object}
  61. * @private
  62. */
  63. Layer.prototype.params = function (path, captures, existingParams) {
  64. var params = existingParams || {};
  65. for (var len = captures.length, i=0; i<len; i++) {
  66. if (this.paramNames[i]) {
  67. var c = captures[i];
  68. params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c;
  69. }
  70. }
  71. return params;
  72. };
  73. /**
  74. * Returns array of regexp url path captures.
  75. *
  76. * @param {String} path
  77. * @returns {Array.<String>}
  78. * @private
  79. */
  80. Layer.prototype.captures = function (path) {
  81. if (this.opts.ignoreCaptures) return [];
  82. return path.match(this.regexp).slice(1);
  83. };
  84. /**
  85. * Generate URL for route using given `params`.
  86. *
  87. * @example
  88. *
  89. * ```javascript
  90. * var route = new Layer(['GET'], '/users/:id', fn);
  91. *
  92. * route.url({ id: 123 }); // => "/users/123"
  93. * ```
  94. *
  95. * @param {Object} params url parameters
  96. * @returns {String}
  97. * @private
  98. */
  99. Layer.prototype.url = function (params, options) {
  100. var args = params;
  101. var url = this.path.replace(/\(\.\*\)/g, '');
  102. var toPath = pathToRegExp.compile(url);
  103. var replaced;
  104. if (typeof params != 'object') {
  105. args = Array.prototype.slice.call(arguments);
  106. if (typeof args[args.length - 1] == 'object') {
  107. options = args[args.length - 1];
  108. args = args.slice(0, args.length - 1);
  109. }
  110. }
  111. var tokens = pathToRegExp.parse(url);
  112. var replace = {};
  113. if (args instanceof Array) {
  114. for (var len = tokens.length, i=0, j=0; i<len; i++) {
  115. if (tokens[i].name) replace[tokens[i].name] = args[j++];
  116. }
  117. } else if (tokens.some(token => token.name)) {
  118. replace = params;
  119. } else {
  120. options = params;
  121. }
  122. replaced = toPath(replace);
  123. if (options && options.query) {
  124. var replaced = new uri(replaced)
  125. replaced.search(options.query);
  126. return replaced.toString();
  127. }
  128. return replaced;
  129. };
  130. /**
  131. * Run validations on route named parameters.
  132. *
  133. * @example
  134. *
  135. * ```javascript
  136. * router
  137. * .param('user', function (id, ctx, next) {
  138. * ctx.user = users[id];
  139. * if (!user) return ctx.status = 404;
  140. * next();
  141. * })
  142. * .get('/users/:user', function (ctx, next) {
  143. * ctx.body = ctx.user;
  144. * });
  145. * ```
  146. *
  147. * @param {String} param
  148. * @param {Function} middleware
  149. * @returns {Layer}
  150. * @private
  151. */
  152. Layer.prototype.param = function (param, fn) {
  153. var stack = this.stack;
  154. var params = this.paramNames;
  155. var middleware = function (ctx, next) {
  156. return fn.call(this, ctx.params[param], ctx, next);
  157. };
  158. middleware.param = param;
  159. var names = params.map(function (p) {
  160. return p.name;
  161. });
  162. var x = names.indexOf(param);
  163. if (x > -1) {
  164. // iterate through the stack, to figure out where to place the handler fn
  165. stack.some(function (fn, i) {
  166. // param handlers are always first, so when we find an fn w/o a param property, stop here
  167. // if the param handler at this part of the stack comes after the one we are adding, stop here
  168. if (!fn.param || names.indexOf(fn.param) > x) {
  169. // inject this param handler right before the current item
  170. stack.splice(i, 0, middleware);
  171. return true; // then break the loop
  172. }
  173. });
  174. }
  175. return this;
  176. };
  177. /**
  178. * Prefix route path.
  179. *
  180. * @param {String} prefix
  181. * @returns {Layer}
  182. * @private
  183. */
  184. Layer.prototype.setPrefix = function (prefix) {
  185. if (this.path) {
  186. this.path = prefix + this.path;
  187. this.paramNames = [];
  188. this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
  189. }
  190. return this;
  191. };
  192. /**
  193. * Safe decodeURIComponent, won't throw any error.
  194. * If `decodeURIComponent` error happen, just return the original value.
  195. *
  196. * @param {String} text
  197. * @returns {String} URL decode original string.
  198. * @private
  199. */
  200. function safeDecodeURIComponent(text) {
  201. try {
  202. return decodeURIComponent(text);
  203. } catch (e) {
  204. return text;
  205. }
  206. }