Skip to content

Commit

Permalink
feat: Add Router route method (silvermine#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
yokuze committed Mar 18, 2019
1 parent 250c469 commit e5a90f3
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 6 deletions.
53 changes: 53 additions & 0 deletions src/Route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { IRoute, PathParams, ProcessorOrProcessors } from './interfaces';
import Router from './Router';

export default class Route implements IRoute {

protected _router: Router;

public constructor(path: PathParams, parentRouter: Router) {
this._router = new Router(parentRouter.routerOptions);
parentRouter.addSubRouter(path, this._router);
}

public all(...handlers: ProcessorOrProcessors[]): this {
this._router.all('/', ...handlers);
return this;
}

public head(...handlers: ProcessorOrProcessors[]): this {
this._router.head('/', ...handlers);
return this;
}

public get(...handlers: ProcessorOrProcessors[]): this {
this._router.get('/', ...handlers);
return this;
}

public post(...handlers: ProcessorOrProcessors[]): this {
this._router.post('/', ...handlers);
return this;
}

public put(...handlers: ProcessorOrProcessors[]): this {
this._router.put('/', ...handlers);
return this;
}

public delete(...handlers: ProcessorOrProcessors[]): this {
this._router.delete('/', ...handlers);
return this;
}

public patch(...handlers: ProcessorOrProcessors[]): this {
this._router.patch('/', ...handlers);
return this;
}

public options(...handlers: ProcessorOrProcessors[]): this {
this._router.options('/', ...handlers);
return this;
}

}
10 changes: 5 additions & 5 deletions src/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
RouterOptions,
NextCallback,
ErrorHandlingRequestProcessor,
IRoute,
} from './interfaces';
import { IRequestMatchingProcessorChain } from './chains/ProcessorChain';
import { Request, Response } from '.';
import { wrapRequestProcessor, wrapRequestProcessors } from './utils/wrapRequestProcessor';
import { RouteMatchingProcessorChain } from './chains/RouteMatchingProcessorChain';
import { MatchAllRequestsProcessorChain } from './chains/MatchAllRequestsProcessorChain';
import { SubRouterProcessorChain } from './chains/SubRouterProcessorChain';
import Route from './Route';

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

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

public handle(originalErr: unknown, req: Request, resp: Response, done: NextCallback): void {
const processors = this._processors;
Expand Down
74 changes: 74 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,78 @@ export interface IRouter {
*/
handle(err: unknown, req: Request, resp: Response, done: NextCallback): void;

/**
* Returns an instance of a route-building helper class, which you can then use to
* handle HTTP verbs with optional middleware. Use app.route() to avoid duplicate route
* names (and thus typo errors). For example:
*
* ```
* app.route('/hello')
* .all(function(req, res, next) {
* // Runs for all HTTP verbs
* })
* .get(function(req, res, next) {
* res.json(...);
* })
* .post(function(req, res, next) {
* // Handle POSTs to /hello
* });
* ```
*/
route(path: PathParams): IRoute;

}

export interface IRoute {

/**
* Express-standard routing method for adding handlers that get invoked regardless of
* the request method (e.g. `OPTIONS`, `GET`, `POST`, etc) for a specific path (or set
* of paths).
*/
all: RouteProcessorAppender<this>;

/**
* Express-standard routing method for `HEAD` requests.
*/
head: RouteProcessorAppender<this>;

/**
* Express-standard routing method for `GET` requests.
*/
get: RouteProcessorAppender<this>;

/**
* Express-standard routing method for `POST` requests.
*/
post: RouteProcessorAppender<this>;

/**
* Express-standard routing method for `PUT` requests.
*/
put: RouteProcessorAppender<this>;

/**
* Express-standard routing method for `DELETE` requests.
*/
delete: RouteProcessorAppender<this>;

/**
* Express-standard routing method for `PATCH` requests.
*/
patch: RouteProcessorAppender<this>;

/**
* Express-standard routing method for `OPTIONS` requests.
*/
options: RouteProcessorAppender<this>;

}

export interface RouteProcessorAppender<T> {

/**
* @param handlers the processors to mount to this route's path
*/
(...handlers: ProcessorOrProcessors[]): T;
}
88 changes: 87 additions & 1 deletion tests/integration-tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { apiGatewayRequest, handlerContext, albRequest, albMultiValHeadersReques
import { spy, SinonSpy, assert } from 'sinon';
import { Application, Request, Response, Router } from '../src';
import { RequestEvent } from '../src/request-response-types';
import { NextCallback } from '../src/interfaces';
import { NextCallback, IRoute, IRouter } from '../src/interfaces';
import { expect } from 'chai';
import { StringArrayOfStringsMap, StringMap, KeyValueStringObject } from '../src/utils/common-types';

Expand Down Expand Up @@ -547,4 +547,90 @@ describe('integration tests', () => {

});

describe('building routes with router.route', () => {

it('is chainable', () => {
let handler = (_req: Request, resp: Response): void => { resp.send('Test'); },
getSpy = spy(handler),
postSpy = spy(handler),
putSpy = spy(handler);

app.route('/test')
.get(getSpy)
.post(postSpy)
.put(putSpy);

// Ensure that chained handlers were registered properly

testOutcome('GET', '/test', 'Test');
assert.calledOnce(getSpy);
assert.notCalled(postSpy);
assert.notCalled(putSpy);
getSpy.resetHistory();

testOutcome('POST', '/test', 'Test');
assert.calledOnce(postSpy);
assert.notCalled(getSpy);
assert.notCalled(putSpy);
postSpy.resetHistory();

testOutcome('PUT', '/test', 'Test');
assert.calledOnce(putSpy);
assert.notCalled(getSpy);
assert.notCalled(postSpy);
postSpy.resetHistory();
});

it('registers route handlers properly', () => {
let handlers: SinonSpy[] = [],
methods: (keyof IRoute & keyof IRouter)[],
allHandler: SinonSpy,
route: IRoute;

route = app.route('/test');

// methods to test
methods = [ 'get', 'post', 'put', 'delete', 'head', 'options', 'patch' ];

// Register handler that runs for every request
allHandler = spy((_req: Request, _resp: Response, next: NextCallback) => { next(); });
route.all(allHandler);


// Register a handler for each method
_.each(methods, (method) => {
let handler = spy((_req: Request, resp: Response) => { resp.send('Test'); });

// Save the handler spy for testing later
handlers.push(handler);

route[method](handler);
});

app.use((_req: Request, resp: Response) => {
resp.send('not found');
});

// Run once for each method type
_.each(methods, (method) => {
testOutcome(method.toUpperCase(), '/test', 'Test');
});

// Check that each handler was called exactly once
_.each(handlers, (handler) => { assert.calledOnce(handler); });

// Check that the "all" handler was called for each method
expect(allHandler.callCount).to.strictlyEqual(methods.length);

// Other tests
_.each(methods, (method) => {
// Ensure trailing slash matches
testOutcome(method.toUpperCase(), '/test/', 'Test');
// Ensure only exact matches trigger the route handler
testOutcome(method.toUpperCase(), '/test/anyting', 'not found');
});
});

});

});

0 comments on commit e5a90f3

Please sign in to comment.