X-Git-Url: https://git.squeep.com/?a=blobdiff_plain;f=lib%2Frouter.js;fp=lib%2Frouter.js;h=0000000000000000000000000000000000000000;hb=9fe1e9b22b8e753c44dec77d1dee3d0061b2e991;hp=2bad3e2b509373d22279d6a866f1dbd1e57a11c8;hpb=91197ffbd90707333da06a714aaec0b8b8143577;p=squeep-api-dingus diff --git a/lib/router.js b/lib/router.js deleted file mode 100644 index 2bad3e2..0000000 --- a/lib/router.js +++ /dev/null @@ -1,261 +0,0 @@ -/* eslint-disable security/detect-object-injection */ -'use strict'; - -/** - * A very simple router. - */ - -const { METHODS: httpMethods } = require('http'); -const common = require('./common'); -const { DingusError } = require('./errors'); - -// Internal identifiers for route entries. -const METHODS = Symbol('METHODS'); -const PARAM = Symbol('PARAM'); - -const defaultOptions = { - ignoreTrailingSlash: false, - paramPrefix: ':', -}; - -/** - * A naïve router which maps incoming requests to handler functions - * by way of url path and request method. - * - * Regex parsing of paths was eschewed, as a design decision. - * - * Instead, each path to be searched for is deconstructed into a list - * of its constituent parts as strings or objects, for invariant or - * parameterized parts respectively. Each search path is assigned a - * mapping of methods to handler functions. - * - * @property {Object} pathsByLength index to registered paths by number of parts - * @property {Symbol} METHODS key to method:handler map on search paths - * @property {Symbol} PARAM key to parameter name in search path parts - */ -class Router { - /** - * @param {Object} options - * @param {Boolean} options.ignoreTrailingSlash discard any trailing slashes when registering and comparing paths (default: false) - * @param {String} options.paramPrefix prefix of a path part denoting a named parameter when registering paths (default: ':') - */ - constructor(options = {}) { - common.setOptions(this, defaultOptions, options); - - // Keep lists of paths to match, indexed by path length. - this.pathsByLength = { - 1: [], - }; - - this.METHODS = METHODS; - this.PARAM = PARAM; - } - - /** - * @typedef {Object} Router~ParamPart - * @property {String} PARAM (symbol key) - */ - /** - * @typedef {Array} Router~SearchPath - * @property {Object} METHODS (symbol key) - */ - /** - * Prepare a path for insertion into search list. - * A searchable path is a list of path parts, with a property of an object of method handlers. - * @param {String} pathDefinition - * @returns {Router~SearchPath} - * @private - */ - _pathDefinitionToPathMatch(pathDefinition) { - const pathMatch = pathDefinition - .split('/') - .map((p) => p.startsWith(this.paramPrefix) ? { [PARAM]: p.slice(this.paramPrefix.length) } : p); - if (this.ignoreTrailingSlash - && pathMatch[pathMatch.length - 1] === '') { - pathMatch.pop(); - } - pathMatch[METHODS] = {}; - pathMatch.forEach((p) => Object.freeze(p)); - Object.freeze(pathMatch); - return pathMatch; - } - - - /** - * Compare checkPath to fixedPath, no param substitution, params must match. - * @param {Router~SearchPath} fixedPath - * @param {Router~SearchPath} checkPath - * @returns {Boolean} - * @private - */ - static _pathCompareExact(fixedPath, checkPath) { - if (fixedPath.length !== checkPath.length) { - return false; - } - for (let i = 0; i < fixedPath.length; i++) { - const fixedPart = fixedPath[i]; - const checkPart = checkPath[i]; - if (typeof fixedPart === 'object' && typeof checkPart === 'object') { - if (fixedPart[PARAM] !== checkPart[PARAM]) { - return false; - } - } else if (fixedPart !== checkPart) { - return false; - } - } - return true; - } - - - /** - * Compare checkPath to fixedPath, populating params. - * @param {Router~SearchPath} fixedPath - * @param {Array} checkPath - * @param {Object} returnParams - * @returns {Boolean} - * @private - */ - static _pathCompareParam(fixedPath, checkPath, returnParams = {}) { - const params = {}; - - if (fixedPath.length !== checkPath.length) { - return false; - } - for (let i = 0; i < fixedPath.length; i++) { - const fixedPart = fixedPath[i]; - const checkPart = checkPath[i]; - if (typeof fixedPart === 'object') { - params[fixedPart[PARAM]] = checkPart; - } else if (fixedPart !== checkPart) { - return false; - } - } - Object.assign(returnParams, params); - return true; - } - - - /** - * @typedef Router~MatchedPath - * @property {Object} pathParams populated param fields - * @property {Router~SearchPath} matchedPath - */ - /** - * Search for an existing path, return matched path and path parameters. - * @param {Array} matchParts - * @returns {Router~MatchedPath} - * @private - */ - _pathFind(matchParts) { - const result = { - pathParams: {}, - matchedPath: undefined, - }; - const pathsByLength = this.pathsByLength[matchParts.length]; - if (pathsByLength) { - for (const p of pathsByLength) { - if (Router._pathCompareParam(p, matchParts, result.pathParams)) { - result.matchedPath = p; - break; - } - } - } - return result; - } - - - /** - * Return a matching path, no param substitution, params must match - * @param {Router~SearchPath} matchParts - * @returns {Router~SearchPath=} - * @private - */ - _pathFindExact(matchParts) { - const pathsByLength = this.pathsByLength[matchParts.length]; - if (pathsByLength) { - for (const p of pathsByLength) { - if (Router._pathCompareExact(p, matchParts)) { - return p; - } - } - } - return undefined; - } - - - /** - * @callback Router~HandlerFn - */ - /** - * @typedef {Object} Router~PathHandler - * @property {Router~HandlerFn} handler - * @property {any[]} handlerArgs - */ - /** - * Insert a new path handler. - * @param {string|string[]} methods - * @param {string} urlPath - * @param {Router~HandlerFn} handler - * @param {any[]} handlerArgs - */ - on(methods, urlPath, handler, handlerArgs = []) { - const matchParts = this._pathDefinitionToPathMatch(urlPath); - let existingPath = this._pathFindExact(matchParts); - if (!existingPath) { - existingPath = matchParts; - if (!(matchParts.length in this.pathsByLength)) { - this.pathsByLength[matchParts.length] = []; - } - this.pathsByLength[matchParts.length].push(existingPath); - } - if (!Array.isArray(methods)) { - methods = [methods]; - } - if (!Array.isArray(handlerArgs)) { - throw new TypeError(`handlerArgs must be an Array, not '${typeof handlerArgs}'`); - } - methods.forEach((method) => { - if (!httpMethods.includes(method) && method !== '*') { - throw new DingusError(`invalid method '${method}'`); - } - existingPath[METHODS][method] = { handler, handlerArgs }; - }); - } - - - /** - * Return an object, which contains a matching handler and any extra - * arguments, for a requested url. - * Also sets path named-parameters as #params and the matched path as - * #matchedPath on the context. - * @param {string} method - * @param {string[]} urlPath - * @param {object} ctx - * @returns {Router~PathHandler} - */ - lookup(method, urlPath, ctx = {}) { - const pathParts = urlPath.split('/').map((part) => decodeURIComponent(part)); - if (this.ignoreTrailingSlash - && pathParts[pathParts.length - 1] === '') { - pathParts.pop(); - } - const { matchedPath, pathParams } = this._pathFind(pathParts); - ctx.params = pathParams; - if (matchedPath) { - ctx.matchedPath = matchedPath; - if (method in matchedPath[METHODS]) { - return matchedPath[METHODS][method]; - } - if ('*' in matchedPath[METHODS]) { - return matchedPath[METHODS]['*']; - } - throw new DingusError('NoMethod'); - } - ctx.unmatchedPath = pathParts; - throw new DingusError('NoPath'); - } - - -} - -module.exports = Router;