minor documentation updates
authorJustin Wind <justin.wind+git@gmail.com>
Wed, 27 Apr 2022 22:11:53 +0000 (15:11 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Wed, 27 Apr 2022 22:11:53 +0000 (15:11 -0700)
lib/common.js
lib/patches/incoming-message.js
lib/router.js
test/lib/common.js

index 21070b8782918f267bf32e3adf96537df08e615a..40a9f554f9ee4f687fe63402e3fed0908162408b 100644 (file)
@@ -11,8 +11,15 @@ const uuid = require('uuid');
 const Enum = require('./enum');
 
 /**
- * Return a function which combines a part of the filename with a scope, for use in logging.
- * @param {string} filename
+ * @callback ScopeFn
+ * @param {String} scope
+ * @returns {String}
+ */
+/**
+ * Return a function which prefixes a provided scope with the most-
+ * relevant part of the filename, for use in logging.
+ * @param {String} filename
+ * @returns {ScopeFn}
  */
 const fileScope = (filename) => {
   let fScope = path.basename(filename, '.js');
@@ -24,9 +31,10 @@ const fileScope = (filename) => {
 
 /**
  * Simple ETag from data.
- * @param {string} filePath 
- * @param {object} fileStat 
- * @param {*} fileData 
+ * @param {String} filePath (currently unused)
+ * @param {fs.Stats} fileStat
+ * @param {crypto.BinaryLike} fileData content
+ * @returns {String}
  */
 const generateETag = (_filePath, fileStat, fileData) => {
   const hash = crypto.createHash('sha256');
@@ -39,16 +47,19 @@ const generateETag = (_filePath, fileStat, fileData) => {
 };
 
 /**
- * @param {object} obj 
- * @param {string} prop 
- * @param {*} def 
+ * Access property with default.
+ * @param {Object} obj
+ * @param {String} prop
+ * @param {*} def default value if prop does not exist for obj
+ * @return {*}
  */
 const get = (obj, prop, def) => obj && prop && (prop in obj) ? obj[prop] : def;
 
 /**
  * @param {http.ClientRequest} req 
  * @param {http.ServerResponse} res 
- * @param {object} ctx 
+ * @param {Object} ctx 
+ * @deprecated after v1.2.5 (integrated into logger module)
  */
 const handlerLogData = (req, res, ctx) => ({
   req: requestLogData(req),
@@ -57,10 +68,12 @@ const handlerLogData = (req, res, ctx) => ({
 });
 
 /**
- * 
+ * Determine whether a client has already requested a resource,
+ * based on If-Modified-Since and If-None-Match headers.
  * @param {http.ClientRequest} req 
  * @param {Number} modifiedTimeMs 
- * @param {string} eTag 
+ * @param {String} eTag 
+ * @returns {Boolean}
  */
 const isClientCached = (req, modifiedTimeMs, eTag) => {
   let clientCached = false;
@@ -93,6 +106,7 @@ const isClientCached = (req, modifiedTimeMs, eTag) => {
  * Expects only one-level deep, is not recursive!
  * @param {Object} origEnum
  * @param {Object} additionalEnum
+ * @returns {Object}
  */
 const mergeEnum = (origEnum, additionalEnum) => {
   for (const e of Object.keys(additionalEnum)) {
@@ -118,9 +132,9 @@ const httpStatusCodeClass = (statusCode) => Math.floor(statusCode / 100);
 const _isObject = (obj) => obj && typeof obj === 'object';
 const _isArray = (obj) => Array.isArray(obj);
 /**
- * Return a new object with all objects combined.
- * @param  {...any} objects
- * @returns
+ * Return a new object with all objects combined, later properties taking precedence.
+ * @param  {...Object} objects
+ * @returns {Object}
  */
 const mergeDeep = (...objects) => {
   return objects.reduce((acc, obj) => {
@@ -144,7 +158,8 @@ const mergeDeep = (...objects) => {
 /**
  * Return a new object with selected props.
  * @param {Object} obj
- * @param {string[]} props
+ * @param {String[]} props
+ * @returns {Object}
  */
 const pick = (obj, props) => {
   const picked = {};
@@ -160,6 +175,7 @@ const pick = (obj, props) => {
  * Return a subset of a request object, suitable for logging.
  * Obscures sensitive header values.
  * @param {http.ClientRequest} req 
+ * @deprecated after v1.2.5 (integrated into logger module)
  */
 const requestLogData = (req) => {
   const data = pick(req, [
@@ -178,6 +194,7 @@ const requestLogData = (req) => {
  * Remove sensitive header data.
  * @param {Object} data
  * @param {Object} data.headers
+ * @deprecated after v1.2.5 (integrated into logger module)
  */
 const scrubHeaderObject = (data) => {
   if (data && data.headers && 'authorization' in data.headers) {
@@ -192,6 +209,7 @@ const scrubHeaderObject = (data) => {
  * Hide sensitive part of an Authorization header.
  * @param {String} authHeader
  * @returns {String}
+ * @deprecated after v1.2.5 (integrated into logger module)
  */
 const obscureAuthorizationHeader = (authHeader) => {
   if (!authHeader) {
@@ -206,6 +224,7 @@ const obscureAuthorizationHeader = (authHeader) => {
 /**
  * Return a subset of a response object, suitable for logging.
  * @param {http.ServerResponse} res 
+ * @deprecated after v1.2.5 (integrated into logger module)
  */
 const responseLogData = (res) => {
   const response = pick(res, [
@@ -218,7 +237,7 @@ const responseLogData = (res) => {
 
 
 /**
- * Store updates to defaultOptions, but no new properties.
+ * Store all properties in defaultOptions on target from either options or defaultOptions.
  * @param {Object} target 
  * @param {Object} defaultOptions 
  * @param {Object} options 
@@ -228,10 +247,10 @@ const setOptions = (target, defaultOptions, options) => {
 };
 
 /**
- * Return a list of source split at first delimiter.
- * @param {string} src 
- * @param {string} delimiter 
- * @param {string} fill trailing stand-in if no delimiter in src
+ * Return a two-item list of src, split at first delimiter encountered.
+ * @param {String} src 
+ * @param {String} delimiter 
+ * @param {String} fill trailing stand-in if no delimiter in src
  */
 const splitFirst = (src, delimiter, fill) => {
   const idx = src.indexOf(delimiter);
@@ -243,14 +262,21 @@ const splitFirst = (src, delimiter, fill) => {
 };
 
 /**
- * Generate a new request identifier.
+ * Generate a new request identifier, a time/host-based uuid.
  * @returns {String}
  */
 const requestId = () => {
   return uuid.v1();
 };
 
+/**
+ * Do nothing.
+ */
 const nop = () => { /**/ };
+
+/**
+ * A logger object which does nothing.
+ */
 const nullLogger = {
   error: nop,
   warn: nop,
@@ -260,8 +286,9 @@ const nullLogger = {
 };
 
 /**
- * Populates any absent logger levels.
+ * Populates any absent logger level functions on a logger object.
  * @param {Object} logger
+ * @returns {Object}
  */
 const ensureLoggerLevels = (logger = {}) => {
   for (const level in nullLogger) {
@@ -275,6 +302,7 @@ const ensureLoggerLevels = (logger = {}) => {
 /**
  * Merges folded header lines
  * @param {String[]} lines
+ * @returns {String}
  */
 const unfoldHeaderLines = (lines) => {
   const foldedLineRE = /^(\t| +)(.*)$/;
index 894ca65428d79e9ff2ba7decd844d2573c7944d8..0b2e259c499ec932708c783dfbd44391b9e93564 100644 (file)
@@ -3,9 +3,9 @@ const { IncomingMessage } = require('http');
 
 /**
  * Welp, here we are, already into the crazy.
- * Per documentation (https://nodejs.org/docs/latest-v12.x/api/http.html#http_request_getheader_name)
- * this should exist, yet (as of 12.18.4) it does not.  So let us change this pitch up, and patch
-*/
+ * This originally existed due to a misinterpretation of the documentation,
+ * but it's really quite useful, so it stays in our world.
+ */
 /* istanbul ignore else */
 if (typeof IncomingMessage.getHeader !== 'function') {
   IncomingMessage.prototype.getHeader = function (name) {
index 61a8d11cb3e55e2f4d434940166a4027f0b2ab51..2bad3e2b509373d22279d6a866f1dbd1e57a11c8 100644 (file)
@@ -18,11 +18,26 @@ const defaultOptions = {
   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 
-   * @param {Boolean} options.paramPrefix 
+   * @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);
@@ -36,14 +51,25 @@ class Router {
     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 method handlers.
-   * @param {string} pathDefinition 
+   * 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);
+    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();
@@ -57,8 +83,10 @@ class Router {
 
   /**
    * Compare checkPath to fixedPath, no param substitution, params must match.
-   * @param {*} fixedPath 
-   * @param {*} checkPath 
+   * @param {Router~SearchPath} fixedPath 
+   * @param {Router~SearchPath} checkPath 
+   * @returns {Boolean}
+   * @private
    */
   static _pathCompareExact(fixedPath, checkPath) {
     if (fixedPath.length !== checkPath.length) {
@@ -81,9 +109,11 @@ class Router {
 
   /**
    * Compare checkPath to fixedPath, populating params.
-   * @param {*} fixedPath 
-   * @param {*} checkPath 
-   * @param {*} returnParams 
+   * @param {Router~SearchPath} fixedPath 
+   * @param {Array<String>} checkPath 
+   * @param {Object} returnParams 
+   * @returns {Boolean}
+   * @private
    */
   static _pathCompareParam(fixedPath, checkPath, returnParams = {}) {
     const params = {};
@@ -105,9 +135,16 @@ class Router {
   }
 
 
+  /**
+   * @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 
+   * @param {Array<String>} matchParts 
+   * @returns {Router~MatchedPath}
+   * @private
    */
   _pathFind(matchParts) {
     const result = {
@@ -129,7 +166,9 @@ class Router {
 
   /**
    * Return a matching path, no param substitution, params must match
-   * @param {*} matchParts 
+   * @param {Router~SearchPath} matchParts 
+   * @returns {Router~SearchPath=}
+   * @private
    */
   _pathFindExact(matchParts) {
     const pathsByLength = this.pathsByLength[matchParts.length];
@@ -144,12 +183,20 @@ class Router {
   }
 
 
+  /**
+   * @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 {fn} handler 
-   * @param {*[]} handlerArgs
+   * @param {Router~HandlerFn} handler
+   * @param {any[]} handlerArgs
    */
   on(methods, urlPath, handler, handlerArgs = []) {
     const matchParts = this._pathDefinitionToPathMatch(urlPath);
@@ -179,11 +226,12 @@ class Router {
   /**
    * Return an object, which contains a matching handler and any extra
    * arguments, for a requested url.
-   * Also sets path parameters on context.
+   * 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 {object}
+   * @returns {Router~PathHandler}
    */
   lookup(method, urlPath, ctx = {}) {
     const pathParts = urlPath.split('/').map((part) => decodeURIComponent(part));
index bacd6be3d54d2aaa7e6b019c98e0e664050392dc..57c4113a9db986dbee499942e6e11ea0a78964b9 100644 (file)
@@ -196,6 +196,32 @@ describe('common', function () {
     });
   }); // responseLogData
 
+  describe('handlerLogData', function () {
+    it('covers', function () {
+      const req = {
+        method: 'GET',
+        somethingElse: 'blah',
+      };
+      const res = {
+        getHeaders: () => ({}),
+        statusCode: 200,
+        blah: 'blah',
+      };
+      const ctx = {};
+      const result = common.handlerLogData(req, res, ctx);
+      assert.deepStrictEqual(result, {
+        req: {
+          method: 'GET',
+        },
+        res: {
+          headers: {},
+          statusCode: 200,
+        },
+        ctx: {},
+      });
+    });
+  }); // handlerLogData
+
   describe('setOptions', function () {
     it('sets options', function () {
       const expected = {