1 /* eslint-disable security/detect-object-injection */
5 * A very simple router.
8 const { METHODS: httpMethods
} = require('http');
9 const common
= require('../common');
10 const { DingusError
} = require('../errors');
11 const PathParameter
= require('./path-parameter');
13 // Internal identifiers for route entries.
14 const kPathMethods
= Symbol('kSqueepDingusRouterPathMethods');
16 const defaultOptions
= {
17 ignoreTrailingSlash: false,
22 * A naïve router which maps incoming requests to handler functions
23 * by way of url path and request method.
25 * Regex parsing of paths was eschewed, as a design decision.
27 * Instead, each path to be searched for is deconstructed into a list
28 * of its constituent parts as strings or objects, for invariant or
29 * parameterized parts respectively. Each search path is assigned a
30 * mapping of methods to handler functions.
32 * @property {Object} pathsByLength index to registered paths by number of parts
36 * @param {Object} options
37 * @param {Boolean} options.ignoreTrailingSlash discard any trailing slashes when registering and comparing paths (default: false)
38 * @param {String} options.paramPrefix prefix of a path part denoting a named parameter when registering paths (default: ':')
40 constructor(options
= {}) {
41 common
.setOptions(this, defaultOptions
, options
);
43 // Keep lists of paths to match, indexed by path length.
44 this.pathsByLength
= {
51 * @typedef {Array<String|PathParameter>} Router~RoutePath
52 * @property {Object} kPathMethods (symbol key)
55 * Prepare a path for insertion into search list.
56 * A route path is an Array of path parts, with a symbolic property of an object mapping method handlers.
57 * @param {String} rawPath
58 * @returns {Router~RoutePath}
61 _pathToRoutePath(rawPath
) {
62 const routePath
= rawPath
64 .map((p
) => p
.startsWith(this.paramPrefix
) ? new PathParameter(p
.slice(this.paramPrefix
.length
)) : p
);
65 if (this.ignoreTrailingSlash
66 && routePath
[routePath
.length
- 1] === '') {
69 routePath
[kPathMethods
] = {};
70 Object
.freeze(routePath
);
76 * Compare checkPath to fixedPath, no param substitution, params must match.
77 * @param {Router~RoutePath} routePath
78 * @param {Router~RoutePath} checkPath
82 static _pathCompareExact(routePath
, checkPath
) {
83 if (routePath
.length
!== checkPath
.length
) {
86 for (let i
= 0; i
< routePath
.length
; i
++) {
87 const fixedPart
= routePath
[i
];
88 const checkPart
= checkPath
[i
];
89 if (fixedPart
instanceof PathParameter
&& checkPart
instanceof PathParameter
) {
90 if (fixedPart
.parameter
!== checkPart
.parameter
) {
93 } else if (fixedPart
!== checkPart
) {
102 * Compare routePath to fixedPath, populating params.
103 * @param {Router~RoutePath} routePath
104 * @param {Array<String>} checkPath
105 * @param {Object} returnParams
109 static _pathCompareParam(routePath
, checkPath
, returnParams
= {}) {
112 if (routePath
.length
!== checkPath
.length
) {
115 for (let i
= 0; i
< routePath
.length
; i
++) {
116 const fixedPart
= routePath
[i
];
117 const checkPart
= checkPath
[i
];
118 if (fixedPart
instanceof PathParameter
) {
119 params
[fixedPart
.parameter
] = checkPart
;
120 } else if (fixedPart
!== checkPart
) {
124 Object
.assign(returnParams
, params
);
130 * @typedef Router~MatchedPath
131 * @property {Object} pathParams populated param fields
132 * @property {Router~RoutePath} matchedPath
135 * Search for an existing path, return matched path and path parameters.
136 * @param {Array<String>} matchParts
137 * @returns {Router~MatchedPath}
140 _pathFind(matchParts
) {
143 matchedPath: undefined,
145 const pathsByLength
= this.pathsByLength
[matchParts
.length
];
147 for (const p
of pathsByLength
) {
148 if (Router
._pathCompareParam(p
, matchParts
, result
.pathParams
)) {
149 result
.matchedPath
= p
;
159 * Return a matching path, no param substitution, params must match
160 * @param {Router~RoutePath} routePath
161 * @returns {Router~RoutePath=}
164 _pathFindExact(routePath
) {
165 const pathsByLength
= this.pathsByLength
[routePath
.length
];
167 for (const p
of pathsByLength
) {
168 if (Router
._pathCompareExact(p
, routePath
)) {
178 * @callback Router~HandlerFn
181 * @typedef {Object} Router~PathHandler
182 * @property {Router~HandlerFn} handler
183 * @property {any[]} handlerArgs
186 * Insert a new path handler.
187 * @param {string|string[]} methods
188 * @param {string} urlPath
189 * @param {Router~HandlerFn} handler
190 * @param {any[]} handlerArgs
192 on(methods
, urlPath
, handler
, handlerArgs
= []) {
193 const matchParts
= this._pathToRoutePath(urlPath
);
194 let existingPath
= this._pathFindExact(matchParts
);
196 existingPath
= matchParts
;
197 if (!(matchParts
.length
in this.pathsByLength
)) {
198 this.pathsByLength
[matchParts
.length
] = [];
200 this.pathsByLength
[matchParts
.length
].push(existingPath
);
202 if (!Array
.isArray(methods
)) {
205 if (!Array
.isArray(handlerArgs
)) {
206 throw new TypeError(`handlerArgs must be an Array, not '${typeof handlerArgs}'`);
208 methods
.forEach((method
) => {
209 if (!httpMethods
.includes(method
) && method
!== '*') {
210 throw new DingusError(`invalid method '${method}'`);
212 existingPath
[kPathMethods
][method
] = { handler
, handlerArgs
};
218 * Return an object, which contains a matching handler and any extra
219 * arguments, for a requested url.
220 * Also sets path named-parameters as #params and the matched path as
221 * #matchedPath on the context.
222 * @param {string} method
223 * @param {string[]} urlPath
224 * @param {object} ctx
225 * @returns {Router~PathHandler}
227 lookup(method
, urlPath
, ctx
= {}) {
228 const pathParts
= urlPath
.split('/').map((part
) => decodeURIComponent(part
));
229 if (this.ignoreTrailingSlash
230 && pathParts
[pathParts
.length
- 1] === '') {
233 const { matchedPath
, pathParams
} = this._pathFind(pathParts
);
234 ctx
.params
= pathParams
;
236 ctx
.matchedPath
= matchedPath
;
237 if (method
in matchedPath
[kPathMethods
]) {
238 return matchedPath
[kPathMethods
][method
];
240 if ('*' in matchedPath
[kPathMethods
]) {
241 return matchedPath
[kPathMethods
]['*'];
243 throw new DingusError('NoMethod');
245 ctx
.unmatchedPath
= pathParts
;
246 throw new DingusError('NoPath');
251 Router
.kPathMethods
= kPathMethods
;
253 module
.exports
= Router
;