From 1bfd3f26e768c390a6be543281e79b7ea5c4b9c5 Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Fri, 10 Sep 2021 10:56:49 -0700 Subject: [PATCH] allow additional arguments to be passed to handler functions also add basic handlers for redirects and static files --- CHANGELOG.md | 10 +++++++- lib/dingus.js | 41 ++++++++++++++++++++++++++++---- lib/enum.js | 1 + lib/router.js | 13 ++++++++--- package-lock.json | 8 +++---- package.json | 2 +- test/lib/dingus.js | 58 +++++++++++++++++++++++++++++++++++++++++++++- test/lib/router.js | 38 ++++++++++++++++++++---------- 8 files changed, 144 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0e331..185fafd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ Releases and notable changes to this project are documented here. ## [Unreleased] +## [v1.2.0] - 2021-09-10 + +### Added + +- handlers can be provided additional arguments when defining routes +- simple handlers for redirects and static files + ## [v1.1.0] - 2021-08-08 ### Added @@ -18,6 +25,7 @@ Releases and notable changes to this project are documented here. --- -[Unreleased]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=HEAD;hp=v1.1.0 +[Unreleased]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=HEAD;hp=v1.2.0 +[v1.2.0]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=v1.2.0;hp=v1.1.0 [v1.1.0]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=v1.1.0;hp=v1.0.0 [v1.0.0]: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff;h=v1.0.0;hp=v0.0.0 diff --git a/lib/dingus.js b/lib/dingus.js index 0143585..3432839 100644 --- a/lib/dingus.js +++ b/lib/dingus.js @@ -105,8 +105,8 @@ class Dingus { * @param {string} urlPath * @param {fn} handler */ - on(method, urlPath, handler) { - this.router.on(method, urlPath, handler); + on(method, urlPath, handler, ...handlerArgs) { + this.router.on(method, urlPath, handler, handlerArgs); } @@ -259,9 +259,9 @@ class Dingus { const { pathPart, queryParams } = this._splitUrl(req.url); ctx.queryParams = queryParams; - let handler; + let handler, handlerArgs = []; try { - handler = this.router.lookup(req.method, pathPart, ctx); + ({ handler, handlerArgs } = this.router.lookup(req.method, pathPart, ctx)); } catch (e) { if (e instanceof DingusError) { switch (e.message) { @@ -285,7 +285,7 @@ class Dingus { try { await this.preHandler(req, res, ctx); - return await handler(req, res, ctx); + return await handler(req, res, ctx, ...handlerArgs); } catch (e) { ctx.error = e; this.sendErrorResponse(e, req, res, ctx); @@ -624,6 +624,37 @@ class Dingus { } + /** + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + * @param {String} file - override ctx.params.file + */ + async handlerGetStaticFile(req, res, ctx, file) { + Dingus.setHeadHandler(req, res, ctx); + + // Set a default response type to handle any errors; will be re-set to serve actual static content type. + this.setResponseType(this.responseTypes, req, res, ctx); + + await this.serveFile(req, res, ctx, this.staticPath, file || ctx.params.file); + } + + + /** + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {Object} ctx + * @param {String} newPath + * @param {Number} statusCode + */ + async handlerRedirect(req, res, ctx, newPath, statusCode = 307) { + this.setResponseType(this.responseTypes, req, res, ctx); + res.setHeader(Enum.Header.Location, newPath); + res.statusCode = statusCode; + res.end(); + } + + /** * @param {http.ClientRequest} req * @param {http.ServerResponse} res diff --git a/lib/enum.js b/lib/enum.js index 83e5094..ef8e773 100644 --- a/lib/enum.js +++ b/lib/enum.js @@ -86,6 +86,7 @@ const Header = { IfModifiedSince: 'If-Modified-Since', IfNoneMatch: 'If-None-Match', LastModified: 'Last-Modified', + Location: 'Location', RequestId: 'Request-ID', Vary: 'Vary', XCorrelationId: 'X-Correlation-ID', diff --git a/lib/router.js b/lib/router.js index 92a8500..61a8d11 100644 --- a/lib/router.js +++ b/lib/router.js @@ -149,8 +149,9 @@ class Router { * @param {string|string[]} methods * @param {string} urlPath * @param {fn} handler + * @param {*[]} handlerArgs */ - on(methods, urlPath, handler) { + on(methods, urlPath, handler, handlerArgs = []) { const matchParts = this._pathDefinitionToPathMatch(urlPath); let existingPath = this._pathFindExact(matchParts); if (!existingPath) { @@ -163,20 +164,26 @@ class Router { 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; + existingPath[METHODS][method] = { handler, handlerArgs }; }); } /** - * Return a matching handler for a request, sets path parameters on context. + * 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)); diff --git a/package-lock.json b/package-lock.json index fcf4174..bee8501 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@squeep/api-dingus", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2826,9 +2826,9 @@ } }, "y18n": { - "version": "4.0.0", - "resolved": "", - "dev": true + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, "yallist": { "version": "4.0.0", diff --git a/package.json b/package.json index 0090320..3253b8d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@squeep/api-dingus", - "version": "1.1.0", + "version": "1.2.0", "description": "A minimal API server framework", "main": "index.js", "scripts": { diff --git a/test/lib/dingus.js b/test/lib/dingus.js index 60b0dad..7036384 100644 --- a/test/lib/dingus.js +++ b/test/lib/dingus.js @@ -439,7 +439,13 @@ describe('Dingus', function () { await dingus.dispatch(req, res, ctx); assert(dingus.handlerBadRequest.called); }); - + it('calls handler with additional arguments', async function () { + dingus.on('GET', '/', stubHandler, 'foo', 'bar'); + await dingus.dispatch(req, res, ctx); + assert(stubHandler.called); + assert.strictEqual(stubHandler.args[0][3], 'foo'); + assert.strictEqual(stubHandler.args[0][4], 'bar'); + }); }); // dispatch describe('parseBody', function () { @@ -905,4 +911,54 @@ Content-Type: image/sgi assert(pfxDingus.handlerNotFound.called); }); }); // proxyPrefix + + describe('handlerRedirect', function () { + let req, res, ctx; + beforeEach(function () { + req = { + getHeader: sinon.stub(), + }; + res = { + setHeader: sinon.stub(), + end: sinon.stub(), + }; + ctx = {}; + }); + it('covers', async function () { + await dingus.handlerRedirect(req, res, ctx); + assert(res.setHeader.called); + assert(res.end.called); + }); + it('covers non-defaults', async function () { + await dingus.handlerRedirect(req, res, ctx, 308); + assert(res.setHeader.called); + assert(res.end.called); + }); + }); // handlerRedirect + + describe('handlerGetStaticFile', function () { + let req, res, ctx; + beforeEach(function () { + req = { + getHeader: sinon.stub(), + }; + res = { + setHeader: sinon.stub(), + }; + ctx = { + params: { + file: '', + }, + }; + sinon.stub(dingus, 'serveFile'); + }); + it('covers', async function () { + await dingus.handlerGetStaticFile(req, res, ctx); + assert(dingus.serveFile.called); + }); + it('covers specified file', async function () { + await dingus.handlerGetStaticFile(req, res, ctx, 'file.txt'); + assert(dingus.serveFile.called); + }); + }); // handlerGetStaticFile }); \ No newline at end of file diff --git a/test/lib/router.js b/test/lib/router.js index f4c54a9..7cc92a0 100644 --- a/test/lib/router.js +++ b/test/lib/router.js @@ -183,6 +183,10 @@ describe('Router', function () { describe('on', function () { let pathsByLengthOrig; const stubHandler = () => {}; + const stubEntry = { + handler: stubHandler, + handlerArgs: [], + }; beforeEach(function () { pathsByLengthOrig = router.pathsByLength; @@ -197,7 +201,7 @@ describe('Router', function () { it('adds new path', function () { const urlPath = '/a/:id'; const expected = router._pathDefinitionToPathMatch(urlPath); - expected[router.METHODS]['GET'] = stubHandler; + expected[router.METHODS]['GET'] = stubEntry; router.on('GET', urlPath, stubHandler); assert.deepStrictEqual(router.pathsByLength[3][0], expected); }); @@ -205,8 +209,8 @@ describe('Router', function () { it('adds new method to path', function () { const urlPath = '/a/:id'; const expected = router._pathDefinitionToPathMatch(urlPath); - expected[router.METHODS]['GET'] = stubHandler; - expected[router.METHODS]['POST'] = stubHandler; + expected[router.METHODS]['GET'] = stubEntry; + expected[router.METHODS]['POST'] = stubEntry; router.on('GET', urlPath, stubHandler); router.on('POST', urlPath, stubHandler); @@ -216,7 +220,7 @@ describe('Router', function () { it('add some more paths', function () { let urlPath = '/a/b/c/d'; const expected = router._pathDefinitionToPathMatch(urlPath); - expected[router.METHODS]['GET'] = stubHandler; + expected[router.METHODS]['GET'] = stubEntry; router.on('GET', urlPath, stubHandler); urlPath = '/a/b/x/y'; router.on('GET', urlPath, stubHandler); @@ -227,8 +231,8 @@ describe('Router', function () { it('adds multiple methods', function () { const urlPath = '/:id'; const expected = router._pathDefinitionToPathMatch(urlPath); - expected[router.METHODS]['GET'] = stubHandler; - expected[router.METHODS]['HEAD'] = stubHandler; + expected[router.METHODS]['GET'] = stubEntry; + expected[router.METHODS]['HEAD'] = stubEntry; router.on(['GET', 'HEAD'], urlPath, stubHandler); assert.deepStrictEqual(router.pathsByLength[2][0], expected); @@ -237,7 +241,7 @@ describe('Router', function () { it('adds new wildcard path', function () { const urlPath = '/a/:id'; const expected = router._pathDefinitionToPathMatch(urlPath); - expected[router.METHODS]['*'] = stubHandler; + expected[router.METHODS]['*'] = stubEntry; router.on('*', urlPath, stubHandler); assert.deepStrictEqual(router.pathsByLength[3][0], expected); }); @@ -252,6 +256,16 @@ describe('Router', function () { assert.strictEqual(e.message, 'invalid method \'FLARP\''); } }); + + it('requires args to be array', function () { + const urlPath = '/a'; + try { + router.on('GET', urlPath, stubHandler, {}); + assert.fail('expected an exception'); + } catch (e) { + assert(e instanceof TypeError); + } + }); }); // on describe('lookup', function () { @@ -274,7 +288,7 @@ describe('Router', function () { router.on(method, urlPath, stubHandler); const path = '/abc'; - const handler = router.lookup(method, path, ctx); + const { handler } = router.lookup(method, path, ctx); assert.strictEqual(handler, stubHandler); }); it('does not find handler with trailing slash', function () { @@ -299,7 +313,7 @@ describe('Router', function () { router.on(method, urlPath, stubHandler); const path = '/abc/'; - const handler = router.lookup(method, path, ctx); + const { handler } = router.lookup(method, path, ctx); assert.strictEqual(handler, stubHandler); }); it('finds handler without context', async function () { @@ -308,7 +322,7 @@ describe('Router', function () { router.on(method, urlPath, stubHandler); const path = '/abc'; - const handler = router.lookup(method, path); + const { handler } = router.lookup(method, path); assert.strictEqual(handler, stubHandler); }); it('finds fallback handler', async function () { @@ -317,7 +331,7 @@ describe('Router', function () { router.on('*', urlPath, stubHandler); const path = '/abc/def'; - const handler = router.lookup(method, path, ctx); + const { handler } = router.lookup(method, path, ctx); assert.strictEqual(handler, stubHandler); }); it('calls unsupported method', async function () { @@ -334,7 +348,7 @@ describe('Router', function () { assert.strictEqual(e.message, 'NoMethod'); } }); - it('does not lookup nonexistant path', async function () { + it('does not lookup non-existent path', async function () { const path = '/foo/bar'; const method = 'GET'; -- 2.45.2