From aee1a77b22217893a609e581de7e4d3521e2c54e Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Wed, 27 Apr 2022 15:11:53 -0700 Subject: [PATCH] minor documentation updates --- lib/common.js | 72 ++++++++++++++++++++--------- lib/patches/incoming-message.js | 6 +-- lib/router.js | 82 ++++++++++++++++++++++++++------- test/lib/common.js | 26 +++++++++++ 4 files changed, 144 insertions(+), 42 deletions(-) diff --git a/lib/common.js b/lib/common.js index 21070b8..40a9f55 100644 --- a/lib/common.js +++ b/lib/common.js @@ -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| +)(.*)$/; diff --git a/lib/patches/incoming-message.js b/lib/patches/incoming-message.js index 894ca65..0b2e259 100644 --- a/lib/patches/incoming-message.js +++ b/lib/patches/incoming-message.js @@ -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) { diff --git a/lib/router.js b/lib/router.js index 61a8d11..2bad3e2 100644 --- a/lib/router.js +++ b/lib/router.js @@ -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} 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} 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} 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)); diff --git a/test/lib/common.js b/test/lib/common.js index bacd6be..57c4113 100644 --- a/test/lib/common.js +++ b/test/lib/common.js @@ -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 = { -- 2.45.2