## [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
---
-[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
* @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);
}
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) {
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);
}
+ /**
+ * @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
IfModifiedSince: 'If-Modified-Since',
IfNoneMatch: 'If-None-Match',
LastModified: 'Last-Modified',
+ Location: 'Location',
RequestId: 'Request-ID',
Vary: 'Vary',
XCorrelationId: 'X-Correlation-ID',
* @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) {
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));
{
"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",
{
"name": "@squeep/api-dingus",
- "version": "1.1.0",
+ "version": "1.2.0",
"description": "A minimal API server framework",
"main": "index.js",
"scripts": {
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 () {
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
describe('on', function () {
let pathsByLengthOrig;
const stubHandler = () => {};
+ const stubEntry = {
+ handler: stubHandler,
+ handlerArgs: [],
+ };
beforeEach(function () {
pathsByLengthOrig = router.pathsByLength;
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);
});
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);
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);
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);
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);
});
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 () {
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 () {
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 () {
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 () {
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 () {
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';