minor refactoring in router, clarifying names and complicating parameter objects
[squeep-api-dingus] / lib / router.js
diff --git a/lib/router.js b/lib/router.js
deleted file mode 100644 (file)
index 2bad3e2..0000000
+++ /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<String|Router~ParamPart>} 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<String>} 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<String>} 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;