allow additional arguments to be passed to handler functions
authorJustin Wind <justin.wind+git@gmail.com>
Fri, 10 Sep 2021 17:56:49 +0000 (10:56 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Fri, 10 Sep 2021 17:56:49 +0000 (10:56 -0700)
also add basic handlers for redirects and static files

CHANGELOG.md
lib/dingus.js
lib/enum.js
lib/router.js
package-lock.json
package.json
test/lib/dingus.js
test/lib/router.js

index 8e0e33187309f285ff53f496d1edd6e1e3ca082e..185fafd971292396f11e5e86c0e849c272a0cf57 100644 (file)
@@ -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
index 0143585c1d83732a9e064d2f8687596af88f42be..343283974d7c723a6cbb8eb85fc3e3b942230fff 100644 (file)
@@ -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
index 83e5094893b8527982c85a3823620e150dcd2df3..ef8e77342b709d8a7954024f45f9becf2e6e27ce 100644 (file)
@@ -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',
index 92a85006458d4544a0336e532c11649e1b596946..61a8d11cb3e55e2f4d434940166a4027f0b2ab51 100644 (file)
@@ -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));
index fcf4174aa7e2167bec02c327a9036772febb70f4..bee85015ac0a539725424a31308f504a393533e7 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "@squeep/api-dingus",
-  "version": "1.1.0",
+  "version": "1.2.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
       }
     },
     "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",
index 0090320259af087f2343949a0c03cb4b91a0709b..3253b8d53ee22edca572ef4e002526c293209982 100644 (file)
@@ -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": {
index 60b0dad59d68ea21bfba45c759df89b5a7ccebe9..7036384494cf7d265b7c287d038541c22063c1b8 100644 (file)
@@ -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
index f4c54a997fb26a0adcf944f208dfa1e81cb73fbf..7cc92a04d984fb9cdac009b9ce672e55acb24691 100644 (file)
@@ -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';