X-Git-Url: https://git.squeep.com/?a=blobdiff_plain;f=lib%2Frouter.js;h=bcb45f2d00bbfacad8bb33c5fe593cbfce6183b5;hb=HEAD;hp=61a8d11cb3e55e2f4d434940166a4027f0b2ab51;hpb=842a9b1e5b62aa642a53269a8466fd1e021e4ff2;p=squeep-api-dingus diff --git a/lib/router.js b/lib/router.js deleted file mode 100644 index 61a8d11..0000000 --- a/lib/router.js +++ /dev/null @@ -1,213 +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: ':', -}; - -class Router { - /** - * @param {Object} options - * @param {Boolean} options.ignoreTrailingSlash - * @param {Boolean} options.paramPrefix - */ - 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; - } - - - /** - * Prepare a path for insertion into search list. - * A searchable path is a list of path parts, with a property of method handlers. - * @param {string} pathDefinition - */ - _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 {*} fixedPath - * @param {*} checkPath - */ - 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 {*} fixedPath - * @param {*} checkPath - * @param {*} returnParams - */ - 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; - } - - - /** - * Search for an existing path, return matched path and path parameters. - * @param {Array} matchParts - */ - _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 {*} matchParts - */ - _pathFindExact(matchParts) { - const pathsByLength = this.pathsByLength[matchParts.length]; - if (pathsByLength) { - for (const p of pathsByLength) { - if (Router._pathCompareExact(p, matchParts)) { - return p; - } - } - } - return undefined; - } - - - /** - * Insert a new path handler. - * @param {string|string[]} methods - * @param {string} urlPath - * @param {fn} handler - * @param {*[]} 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 parameters on context. - * @param {string} method - * @param {string[]} urlPath - * @param {object} ctx - * @returns {object} - */ - 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;