From dd701aae3867b5caf0efbd777b802533a60e08ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 14 Feb 2018 11:15:32 +0100 Subject: [PATCH] feat(rest): app.route() and app.api() Enhance `RestApplication` class to provide shortcuts for registering routes and configuring the master OpenAPI spec. Fix restApp.sequence() - modify RestServer to inherit the sequence binding from the application, modify RestComponent to register the DefaultSequence at the app level (instead of RestServer level). Similarly, modify RestServer to inherit the API_SPEC binding from the application, modify RestComponent to register an empty spec at the app level (instead of RestServer level). --- packages/rest/src/rest-application.ts | 77 +++++++++++++++++- packages/rest/src/rest-component.ts | 5 +- packages/rest/src/rest-server.ts | 11 +-- .../acceptance/routing/routing.acceptance.ts | 78 +++++++++++++++++++ .../sequence/sequence.acceptance.ts | 39 +++++++--- 5 files changed, 191 insertions(+), 19 deletions(-) diff --git a/packages/rest/src/rest-application.ts b/packages/rest/src/rest-application.ts index 1894beeab647..251901517e6c 100644 --- a/packages/rest/src/rest-application.ts +++ b/packages/rest/src/rest-application.ts @@ -9,6 +9,9 @@ import {SequenceHandler, SequenceFunction} from './sequence'; import {Binding, Constructor} from '@loopback/context'; import {format} from 'util'; import {RestBindings} from './keys'; +import {RouteEntry, RestServer} from '.'; +import {ControllerClass} from './router/routing-table'; +import {OperationObject, OpenApiSpec} from '@loopback/openapi-spec'; export const ERR_NO_MULTI_SERVER = format( 'RestApplication does not support multiple servers!', @@ -46,7 +49,79 @@ export class RestApplication extends Application { handler(handlerFn: SequenceFunction) { // FIXME(kjdelisle): I attempted to mimic the pattern found in RestServer // with no success, so until I've got a better way, this is functional. - const server = this.getSync('servers.RestServer'); + const server: RestServer = this.getSync('servers.RestServer'); server.handler(handlerFn); } + + /** + * Register a new Controller-based route. + * + * ```ts + * class MyController { + * greet(name: string) { + * return `hello ${name}`; + * } + * } + * app.route('get', '/greet', operationSpec, MyController, 'greet'); + * ``` + * + * @param verb HTTP verb of the endpoint + * @param path URL path of the endpoint + * @param spec The OpenAPI spec describing the endpoint (operation) + * @param controller Controller constructor + * @param methodName The name of the controller method + */ + route( + verb: string, + path: string, + spec: OperationObject, + controller: ControllerClass, + methodName: string, + ): Binding; + + /** + * Register a new route. + * + * ```ts + * function greet(name: string) { + * return `hello ${name}`; + * } + * const route = new Route('get', '/', operationSpec, greet); + * app.route(route); + * ``` + * + * @param route The route to add. + */ + route(route: RouteEntry): Binding; + + route( + routeOrVerb: RouteEntry | string, + path?: string, + spec?: OperationObject, + controller?: ControllerClass, + methodName?: string, + ): Binding { + // FIXME(bajtos): This is a workaround based on app.handler() above + const server: RestServer = this.getSync('servers.RestServer'); + if (typeof routeOrVerb === 'object') { + return server.route(routeOrVerb); + } else { + return server.route(routeOrVerb, path!, spec!, controller!, methodName!); + } + } + + /** + * Set the OpenAPI specification that defines the REST API schema for this + * application. All routes, parameter definitions and return types will be + * defined in this way. + * + * Note that this will override any routes defined via decorators at the + * controller level (this function takes precedent). + * + * @param {OpenApiSpec} spec The OpenAPI specification, as an object. + * @returns {Binding} + */ + api(spec: OpenApiSpec): Binding { + return this.bind(RestBindings.API_SPEC).to(spec); + } } diff --git a/packages/rest/src/rest-component.ts b/packages/rest/src/rest-component.ts index 0de73e9dd447..27aab58a4177 100644 --- a/packages/rest/src/rest-component.ts +++ b/packages/rest/src/rest-component.ts @@ -23,6 +23,8 @@ import { SendProvider, } from './providers'; import {RestServer, RestServerConfig} from './rest-server'; +import {DefaultSequence} from '.'; +import {createEmptyApiSpec} from '@loopback/openapi-spec'; export class RestComponent implements Component { providers: ProviderMap = { @@ -45,7 +47,8 @@ export class RestComponent implements Component { @inject(CoreBindings.APPLICATION_INSTANCE) app: Application, @inject(RestBindings.CONFIG) config?: RestComponentConfig, ) { - if (!config) config = {}; + app.bind(RestBindings.SEQUENCE).toClass(DefaultSequence); + app.bind(RestBindings.API_SPEC).to(createEmptyApiSpec()); } } diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 328cbf7cb32b..6cadcc4c7215 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -9,11 +9,7 @@ import {safeDump} from 'js-yaml'; import {Binding, Context, Constructor, inject} from '@loopback/context'; import {Route, ControllerRoute, RouteEntry} from './router/routing-table'; import {ParsedRequest} from './internal-types'; -import { - OpenApiSpec, - createEmptyApiSpec, - OperationObject, -} from '@loopback/openapi-spec'; +import {OpenApiSpec, OperationObject} from '@loopback/openapi-spec'; import {ServerRequest, ServerResponse, createServer} from 'http'; import * as Http from 'http'; import {Application, CoreBindings, Server} from '@loopback/core'; @@ -141,9 +137,10 @@ export class RestServer extends Context implements Server { } this.bind(RestBindings.PORT).to(options.port); this.bind(RestBindings.HOST).to(options.host); - this.api(createEmptyApiSpec()); - this.sequence(options.sequence ? options.sequence : DefaultSequence); + if (options.sequence) { + this.sequence(options.sequence); + } this.handleHttp = (req: ServerRequest, res: ServerResponse) => { try { diff --git a/packages/rest/test/acceptance/routing/routing.acceptance.ts b/packages/rest/test/acceptance/routing/routing.acceptance.ts index f0c25fdf1b7a..c665efd8f0ea 100644 --- a/packages/rest/test/acceptance/routing/routing.acceptance.ts +++ b/packages/rest/test/acceptance/routing/routing.acceptance.ts @@ -10,6 +10,7 @@ import { RestBindings, RestServer, RestComponent, + RestApplication, } from '../../..'; import {api, get, param} from '@loopback/openapi-v2'; @@ -487,6 +488,83 @@ describe('Routing', () => { await client.get('/greet?name=world').expect(200, 'hello world'); }); + describe('RestApplication', () => { + it('supports function-based routes declared via app.route()', async () => { + const app = new RestApplication(); + + const routeSpec = { + parameters: [ + {name: 'name', in: 'query', type: 'string'}, + ], + responses: { + 200: { + description: 'greeting text', + schema: {type: 'string'}, + }, + }, + }; + + function greet(name: string) { + return `hello ${name}`; + } + + const route = new Route('get', '/greet', routeSpec, greet); + app.route(route); + + const server = await givenAServer(app); + const client = whenIMakeRequestTo(server); + await client.get('/greet?name=world').expect(200, 'hello world'); + }); + + it('supports controller routes declared via app.api()', async () => { + const app = new RestApplication(); + + class MyController { + greet(name: string) { + return `hello ${name}`; + } + } + + const spec = anOpenApiSpec() + .withOperation( + 'get', + '/greet', + anOperationSpec() + .withParameter({name: 'name', in: 'query', type: 'string'}) + .withExtension('x-operation-name', 'greet') + .withExtension('x-controller-name', 'MyController'), + ) + .build(); + + app.api(spec); + app.controller(MyController); + + const server = await givenAServer(app); + const client = whenIMakeRequestTo(server); + await client.get('/greet?name=world').expect(200, 'hello world'); + }); + + it('supports controller routes defined via app.route()', async () => { + const app = new RestApplication(); + + class MyController { + greet(name: string) { + return `hello ${name}`; + } + } + + const spec = anOperationSpec() + .withParameter({name: 'name', in: 'query', type: 'string'}) + .build(); + + app.route('get', '/greet', spec, MyController, 'greet'); + + const server = await givenAServer(app); + const client = whenIMakeRequestTo(server); + await client.get('/greet?name=world').expect(200, 'hello world'); + }); + }); + /* ===== HELPERS ===== */ function givenAnApplication() { diff --git a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts index 8f36d2484c54..c44411bee688 100644 --- a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts +++ b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts @@ -16,6 +16,7 @@ import { RestBindings, RestServer, RestComponent, + RestApplication, } from '../../..'; import {api} from '@loopback/openapi-v2'; import {Application} from '@loopback/core'; @@ -36,7 +37,7 @@ describe('Sequence', () => { let server: RestServer; beforeEach(givenAppWithController); it('provides a default sequence', async () => { - whenIMakeRequestTo(app) + whenIRequest() .get('/name') .expect('SequenceApp'); }); @@ -45,7 +46,7 @@ describe('Sequence', () => { server.handler((sequence, request, response) => { sequence.send(response, 'hello world'); }); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/') .expect('hello world'); }); @@ -61,7 +62,7 @@ describe('Sequence', () => { // bind user defined sequence server.sequence(MySequence); - whenIMakeRequestTo(app) + whenIRequest() .get('/') .expect('hello world'); }); @@ -86,11 +87,29 @@ describe('Sequence', () => { server.sequence(MySequence); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/name') .expect('MySequence SequenceApp'); }); + it('allows users to bind a custom sequence class via app.sequence()', async () => { + class MySequence { + constructor(@inject(SequenceActions.SEND) protected send: Send) {} + + async handle(req: ParsedRequest, res: ServerResponse) { + this.send(res, 'MySequence was invoked.'); + } + } + + const restApp = new RestApplication(); + restApp.sequence(MySequence); + + const appServer = await restApp.getServer(RestServer); + await whenIRequest(appServer) + .get('/name') + .expect('MySequence was invoked.'); + }); + it('user-defined Send', () => { const send: Send = (response, result) => { response.setHeader('content-type', 'text/plain'); @@ -98,7 +117,7 @@ describe('Sequence', () => { }; server.bind(SequenceActions.SEND).to(send); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/name') .expect('CUSTOM FORMAT: SequenceApp'); }); @@ -110,7 +129,7 @@ describe('Sequence', () => { }; server.bind(SequenceActions.REJECT).to(reject); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/unknown-url') .expect(418); }); @@ -122,7 +141,7 @@ describe('Sequence', () => { sequence.send(response, sequence.ctx.getSync('test')); }); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/') .expect('hello world'); }); @@ -149,7 +168,7 @@ describe('Sequence', () => { server.sequence(MySequence); app.bind('test').to('hello world'); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/') .expect('hello world'); }); @@ -194,7 +213,7 @@ describe('Sequence', () => { app.controller(controller); } - function whenIMakeRequestTo(application: Application): Client { - return createClientForHandler(server.handleHttp); + function whenIRequest(restServer: RestServer = server): Client { + return createClientForHandler(restServer.handleHttp); } });