Skip to content

Commit 1174414

Browse files
yokuzejthomerson
authored andcommitted
feat: Add Router route method (#19)
1 parent 17426c1 commit 1174414

4 files changed

Lines changed: 231 additions & 6 deletions

File tree

src/Route.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { IRoute, PathParams, ProcessorOrProcessors } from './interfaces';
2+
import Router from './Router';
3+
4+
export default class Route implements IRoute {
5+
6+
protected _router: Router;
7+
8+
public constructor(path: PathParams, parentRouter: Router) {
9+
this._router = new Router(parentRouter.routerOptions);
10+
parentRouter.addSubRouter(path, this._router);
11+
}
12+
13+
public all(...handlers: ProcessorOrProcessors[]): this {
14+
this._router.all('/', ...handlers);
15+
return this;
16+
}
17+
18+
public head(...handlers: ProcessorOrProcessors[]): this {
19+
this._router.head('/', ...handlers);
20+
return this;
21+
}
22+
23+
public get(...handlers: ProcessorOrProcessors[]): this {
24+
this._router.get('/', ...handlers);
25+
return this;
26+
}
27+
28+
public post(...handlers: ProcessorOrProcessors[]): this {
29+
this._router.post('/', ...handlers);
30+
return this;
31+
}
32+
33+
public put(...handlers: ProcessorOrProcessors[]): this {
34+
this._router.put('/', ...handlers);
35+
return this;
36+
}
37+
38+
public delete(...handlers: ProcessorOrProcessors[]): this {
39+
this._router.delete('/', ...handlers);
40+
return this;
41+
}
42+
43+
public patch(...handlers: ProcessorOrProcessors[]): this {
44+
this._router.patch('/', ...handlers);
45+
return this;
46+
}
47+
48+
public options(...handlers: ProcessorOrProcessors[]): this {
49+
this._router.options('/', ...handlers);
50+
return this;
51+
}
52+
53+
}

src/Router.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import {
77
RouterOptions,
88
NextCallback,
99
ErrorHandlingRequestProcessor,
10+
IRoute,
1011
} from './interfaces';
1112
import { IRequestMatchingProcessorChain } from './chains/ProcessorChain';
1213
import { Request, Response } from '.';
1314
import { wrapRequestProcessor, wrapRequestProcessors } from './utils/wrapRequestProcessor';
1415
import { RouteMatchingProcessorChain } from './chains/RouteMatchingProcessorChain';
1516
import { MatchAllRequestsProcessorChain } from './chains/MatchAllRequestsProcessorChain';
1617
import { SubRouterProcessorChain } from './chains/SubRouterProcessorChain';
18+
import Route from './Route';
1719

1820
const DEFAULT_OPTS: RouterOptions = {
1921
caseSensitive: false,
@@ -28,11 +30,9 @@ export default class Router implements IRouter {
2830
this.routerOptions = _.defaults(options, DEFAULT_OPTS);
2931
}
3032

31-
// TODO: do we need `router.route`?
32-
// https://expressjs.com/en/guide/routing.html#app-route
33-
// https://expressjs.com/en/4x/api.html#router.route
34-
// If we do add it, we need to set the case-sensitivity of the sub-router it creates
35-
// using the case-sensitivity setting of this router.
33+
public route(prefix: PathParams): IRoute {
34+
return new Route(prefix, this);
35+
}
3636

3737
public handle(originalErr: unknown, req: Request, resp: Response, done: NextCallback): void {
3838
const processors = this._processors;

src/interfaces.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,79 @@ export interface IRouter {
176176
*/
177177
handle(err: unknown, req: Request, resp: Response, done: NextCallback): void;
178178

179+
/**
180+
* Returns an instance of a route-building helper class, which you can then use to
181+
* handle HTTP verbs with optional middleware. Use app.route() to avoid duplicate route
182+
* names (and thus typo errors). For example:
183+
*
184+
* ```
185+
* app.route('/hello')
186+
* .all(function(req, res, next) {
187+
* // Runs for all HTTP verbs
188+
* })
189+
* .get(function(req, res, next) {
190+
* // Handle GETs to /hello
191+
* res.json(...);
192+
* })
193+
* .post(function(req, res, next) {
194+
* // Handle POSTs to /hello
195+
* });
196+
* ```
197+
*/
198+
route(path: PathParams): IRoute;
199+
200+
}
201+
202+
export interface IRoute {
203+
204+
/**
205+
* Express-standard routing method for adding handlers that get invoked regardless of
206+
* the request method (e.g. `OPTIONS`, `GET`, `POST`, etc) for a specific path (or set
207+
* of paths).
208+
*/
209+
all: RouteProcessorAppender<this>;
210+
211+
/**
212+
* Express-standard routing method for `HEAD` requests.
213+
*/
214+
head: RouteProcessorAppender<this>;
215+
216+
/**
217+
* Express-standard routing method for `GET` requests.
218+
*/
219+
get: RouteProcessorAppender<this>;
220+
221+
/**
222+
* Express-standard routing method for `POST` requests.
223+
*/
224+
post: RouteProcessorAppender<this>;
225+
226+
/**
227+
* Express-standard routing method for `PUT` requests.
228+
*/
229+
put: RouteProcessorAppender<this>;
230+
231+
/**
232+
* Express-standard routing method for `DELETE` requests.
233+
*/
234+
delete: RouteProcessorAppender<this>;
235+
236+
/**
237+
* Express-standard routing method for `PATCH` requests.
238+
*/
239+
patch: RouteProcessorAppender<this>;
240+
241+
/**
242+
* Express-standard routing method for `OPTIONS` requests.
243+
*/
244+
options: RouteProcessorAppender<this>;
245+
246+
}
247+
248+
export interface RouteProcessorAppender<T> {
249+
250+
/**
251+
* @param handlers the processors to mount to this route's path
252+
*/
253+
(...handlers: ProcessorOrProcessors[]): T;
179254
}

tests/integration-tests.test.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { apiGatewayRequest, handlerContext, albRequest, albMultiValHeadersReques
33
import { spy, SinonSpy, assert } from 'sinon';
44
import { Application, Request, Response, Router } from '../src';
55
import { RequestEvent } from '../src/request-response-types';
6-
import { NextCallback } from '../src/interfaces';
6+
import { NextCallback, IRoute, IRouter } from '../src/interfaces';
77
import { expect } from 'chai';
88
import { StringArrayOfStringsMap, StringMap, KeyValueStringObject } from '../src/utils/common-types';
99

@@ -592,4 +592,101 @@ describe('integration tests', () => {
592592

593593
});
594594

595+
describe('building routes with router.route', () => {
596+
597+
it('is chainable', () => {
598+
let handler = (_req: Request, resp: Response): void => { resp.send('Test'); },
599+
getSpy = spy(handler),
600+
postSpy = spy(handler),
601+
putSpy = spy(handler);
602+
603+
app.route('/test')
604+
.get(getSpy)
605+
.post(postSpy)
606+
.put(putSpy);
607+
608+
// Ensure that chained handlers were registered properly
609+
610+
testOutcome('GET', '/test', 'Test');
611+
assert.calledOnce(getSpy);
612+
assert.notCalled(postSpy);
613+
assert.notCalled(putSpy);
614+
getSpy.resetHistory();
615+
616+
testOutcome('POST', '/test', 'Test');
617+
assert.calledOnce(postSpy);
618+
assert.notCalled(getSpy);
619+
assert.notCalled(putSpy);
620+
postSpy.resetHistory();
621+
622+
testOutcome('PUT', '/test', 'Test');
623+
assert.calledOnce(putSpy);
624+
assert.notCalled(getSpy);
625+
assert.notCalled(postSpy);
626+
putSpy.resetHistory();
627+
});
628+
629+
it('registers route handlers properly', () => {
630+
let methods: (keyof IRoute & keyof IRouter)[],
631+
allHandler: SinonSpy,
632+
route: IRoute;
633+
634+
route = app.route('/test');
635+
636+
// methods to test
637+
methods = [ 'get', 'post', 'put', 'delete', 'head', 'options', 'patch' ];
638+
639+
// Register handler that runs for every request
640+
allHandler = spy((_req: Request, _resp: Response, next: NextCallback) => { next(); });
641+
route.all(allHandler);
642+
643+
644+
// Register a handler for each method
645+
const handlers = _.reduce(methods, (memo, method) => {
646+
let handler = spy((_req: Request, resp: Response) => { resp.send(`Test ${method}`); });
647+
648+
// Save the handler spy for testing later
649+
memo[method] = handler;
650+
651+
// add the handler to our route
652+
route[method](handler);
653+
654+
return memo;
655+
}, {} as { [k: string]: SinonSpy });
656+
657+
app.use((_req: Request, resp: Response) => {
658+
resp.send('not found');
659+
});
660+
661+
// Run once for each method type
662+
// Both a path with and without a trailing slash should match
663+
_.each([ '/test', '/test/' ], (path) => {
664+
_.each(methods, (method) => {
665+
testOutcome(method.toUpperCase(), path, `Test ${method}`);
666+
667+
// Check that the "all" handler was called
668+
assert.calledOnce(allHandler);
669+
allHandler.resetHistory();
670+
671+
// Check that only the one handler was called
672+
_.each(handlers, (handler, handlerMethod) => {
673+
if (method === handlerMethod) {
674+
assert.calledOnce(handler);
675+
} else {
676+
assert.notCalled(handler);
677+
}
678+
handler.resetHistory();
679+
});
680+
});
681+
});
682+
683+
// Other tests
684+
_.each(methods, (method) => {
685+
// Ensure only exact matches trigger the route handler
686+
testOutcome(method.toUpperCase(), '/test/anything', 'not found');
687+
});
688+
});
689+
690+
});
691+
595692
});

0 commit comments

Comments
 (0)