Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Router route method (#19) #33

Merged
merged 1 commit into from
Mar 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
75 changes: 75 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,79 @@ 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) {
* // Handle GETs to /hello
* res.json(...);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment

* })
* .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;
}
99 changes: 98 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 @@ -592,4 +592,101 @@ 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);
putSpy.resetHistory();
});

it('registers route handlers properly', () => {
let 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
const handlers = _.reduce(methods, (memo, method) => {
let handler = spy((_req: Request, resp: Response) => { resp.send(`Test ${method}`); });

// Save the handler spy for testing later
memo[method] = handler;

// add the handler to our route
route[method](handler);

return memo;
}, {} as { [k: string]: SinonSpy });

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

// Run once for each method type
// Both a path with and without a trailing slash should match
_.each([ '/test', '/test/' ], (path) => {
_.each(methods, (method) => {
testOutcome(method.toUpperCase(), path, `Test ${method}`);

// Check that the "all" handler was called
assert.calledOnce(allHandler);
allHandler.resetHistory();

// Check that only the one handler was called
_.each(handlers, (handler, handlerMethod) => {
if (method === handlerMethod) {
assert.calledOnce(handler);
} else {
assert.notCalled(handler);
}
handler.resetHistory();
});
});
});

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

});

});