Skip to content

Commit 5c3fd62

Browse files
committed
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).
1 parent 18eed2d commit 5c3fd62

File tree

5 files changed

+191
-19
lines changed

5 files changed

+191
-19
lines changed

packages/rest/src/rest-application.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import {SequenceHandler, SequenceFunction} from './sequence';
99
import {Binding, Constructor} from '@loopback/context';
1010
import {format} from 'util';
1111
import {RestBindings} from './keys';
12+
import {RouteEntry, RestServer} from '.';
13+
import {ControllerClass} from './router/routing-table';
14+
import {OperationObject, OpenApiSpec} from '@loopback/openapi-spec';
1215

1316
export const ERR_NO_MULTI_SERVER = format(
1417
'RestApplication does not support multiple servers!',
@@ -46,7 +49,79 @@ export class RestApplication extends Application {
4649
handler(handlerFn: SequenceFunction) {
4750
// FIXME(kjdelisle): I attempted to mimic the pattern found in RestServer
4851
// with no success, so until I've got a better way, this is functional.
49-
const server = this.getSync('servers.RestServer');
52+
const server: RestServer = this.getSync('servers.RestServer');
5053
server.handler(handlerFn);
5154
}
55+
56+
/**
57+
* Register a new Controller-based route.
58+
*
59+
* ```ts
60+
* class MyController {
61+
* greet(name: string) {
62+
* return `hello ${name}`;
63+
* }
64+
* }
65+
* app.route('get', '/greet', operationSpec, MyController, 'greet');
66+
* ```
67+
*
68+
* @param verb HTTP verb of the endpoint
69+
* @param path URL path of the endpoint
70+
* @param spec The OpenAPI spec describing the endpoint (operation)
71+
* @param controller Controller constructor
72+
* @param methodName The name of the controller method
73+
*/
74+
route(
75+
verb: string,
76+
path: string,
77+
spec: OperationObject,
78+
controller: ControllerClass,
79+
methodName: string,
80+
): Binding;
81+
82+
/**
83+
* Register a new route.
84+
*
85+
* ```ts
86+
* function greet(name: string) {
87+
* return `hello ${name}`;
88+
* }
89+
* const route = new Route('get', '/', operationSpec, greet);
90+
* app.route(route);
91+
* ```
92+
*
93+
* @param route The route to add.
94+
*/
95+
route(route: RouteEntry): Binding;
96+
97+
route(
98+
routeOrVerb: RouteEntry | string,
99+
path?: string,
100+
spec?: OperationObject,
101+
controller?: ControllerClass,
102+
methodName?: string,
103+
): Binding {
104+
// FIXME(bajtos): This is a workaround based on app.handler() above
105+
const server: RestServer = this.getSync('servers.RestServer');
106+
if (typeof routeOrVerb === 'object') {
107+
return server.route(routeOrVerb);
108+
} else {
109+
return server.route(routeOrVerb, path!, spec!, controller!, methodName!);
110+
}
111+
}
112+
113+
/**
114+
* Set the OpenAPI specification that defines the REST API schema for this
115+
* application. All routes, parameter definitions and return types will be
116+
* defined in this way.
117+
*
118+
* Note that this will override any routes defined via decorators at the
119+
* controller level (this function takes precedent).
120+
*
121+
* @param {OpenApiSpec} spec The OpenAPI specification, as an object.
122+
* @returns {Binding}
123+
*/
124+
api(spec: OpenApiSpec): Binding {
125+
return this.bind(RestBindings.API_SPEC).to(spec);
126+
}
52127
}

packages/rest/src/rest-component.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
SendProvider,
2424
} from './providers';
2525
import {RestServer, RestServerConfig} from './rest-server';
26+
import {DefaultSequence} from '.';
27+
import {createEmptyApiSpec} from '@loopback/openapi-spec';
2628

2729
export class RestComponent implements Component {
2830
providers: ProviderMap = {
@@ -45,7 +47,8 @@ export class RestComponent implements Component {
4547
@inject(CoreBindings.APPLICATION_INSTANCE) app: Application,
4648
@inject(RestBindings.CONFIG) config?: RestComponentConfig,
4749
) {
48-
if (!config) config = {};
50+
app.bind(RestBindings.SEQUENCE).toClass(DefaultSequence);
51+
app.bind(RestBindings.API_SPEC).to(createEmptyApiSpec());
4952
}
5053
}
5154

packages/rest/src/rest-server.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@ import {safeDump} from 'js-yaml';
99
import {Binding, Context, Constructor, inject} from '@loopback/context';
1010
import {Route, ControllerRoute, RouteEntry} from './router/routing-table';
1111
import {ParsedRequest} from './internal-types';
12-
import {
13-
OpenApiSpec,
14-
createEmptyApiSpec,
15-
OperationObject,
16-
} from '@loopback/openapi-spec';
12+
import {OpenApiSpec, OperationObject} from '@loopback/openapi-spec';
1713
import {ServerRequest, ServerResponse, createServer} from 'http';
1814
import * as Http from 'http';
1915
import {Application, CoreBindings, Server} from '@loopback/core';
@@ -141,9 +137,10 @@ export class RestServer extends Context implements Server {
141137
}
142138
this.bind(RestBindings.PORT).to(options.port);
143139
this.bind(RestBindings.HOST).to(options.host);
144-
this.api(createEmptyApiSpec());
145140

146-
this.sequence(options.sequence ? options.sequence : DefaultSequence);
141+
if (options.sequence) {
142+
this.sequence(options.sequence);
143+
}
147144

148145
this.handleHttp = (req: ServerRequest, res: ServerResponse) => {
149146
try {

packages/rest/test/acceptance/routing/routing.acceptance.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
RestBindings,
1111
RestServer,
1212
RestComponent,
13+
RestApplication,
1314
} from '../../..';
1415

1516
import {api, get, param} from '@loopback/openapi-v2';
@@ -487,6 +488,83 @@ describe('Routing', () => {
487488
await client.get('/greet?name=world').expect(200, 'hello world');
488489
});
489490

491+
describe('RestApplication', () => {
492+
it('supports function-based routes declared via app.route()', async () => {
493+
const app = new RestApplication();
494+
495+
const routeSpec = <OperationObject>{
496+
parameters: [
497+
<ParameterObject>{name: 'name', in: 'query', type: 'string'},
498+
],
499+
responses: {
500+
200: <ResponseObject>{
501+
description: 'greeting text',
502+
schema: {type: 'string'},
503+
},
504+
},
505+
};
506+
507+
function greet(name: string) {
508+
return `hello ${name}`;
509+
}
510+
511+
const route = new Route('get', '/greet', routeSpec, greet);
512+
app.route(route);
513+
514+
const server = await givenAServer(app);
515+
const client = whenIMakeRequestTo(server);
516+
await client.get('/greet?name=world').expect(200, 'hello world');
517+
});
518+
519+
it('supports controller routes declared via app.api()', async () => {
520+
const app = new RestApplication();
521+
522+
class MyController {
523+
greet(name: string) {
524+
return `hello ${name}`;
525+
}
526+
}
527+
528+
const spec = anOpenApiSpec()
529+
.withOperation(
530+
'get',
531+
'/greet',
532+
anOperationSpec()
533+
.withParameter({name: 'name', in: 'query', type: 'string'})
534+
.withExtension('x-operation-name', 'greet')
535+
.withExtension('x-controller-name', 'MyController'),
536+
)
537+
.build();
538+
539+
app.api(spec);
540+
app.controller(MyController);
541+
542+
const server = await givenAServer(app);
543+
const client = whenIMakeRequestTo(server);
544+
await client.get('/greet?name=world').expect(200, 'hello world');
545+
});
546+
547+
it('supports controller routes defined via app.route()', async () => {
548+
const app = new RestApplication();
549+
550+
class MyController {
551+
greet(name: string) {
552+
return `hello ${name}`;
553+
}
554+
}
555+
556+
const spec = anOperationSpec()
557+
.withParameter({name: 'name', in: 'query', type: 'string'})
558+
.build();
559+
560+
app.route('get', '/greet', spec, MyController, 'greet');
561+
562+
const server = await givenAServer(app);
563+
const client = whenIMakeRequestTo(server);
564+
await client.get('/greet?name=world').expect(200, 'hello world');
565+
});
566+
});
567+
490568
/* ===== HELPERS ===== */
491569

492570
function givenAnApplication() {

packages/rest/test/acceptance/sequence/sequence.acceptance.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
RestBindings,
1717
RestServer,
1818
RestComponent,
19+
RestApplication,
1920
} from '../../..';
2021
import {api} from '@loopback/openapi-v2';
2122
import {Application} from '@loopback/core';
@@ -36,7 +37,7 @@ describe('Sequence', () => {
3637
let server: RestServer;
3738
beforeEach(givenAppWithController);
3839
it('provides a default sequence', async () => {
39-
whenIMakeRequestTo(app)
40+
whenIRequest()
4041
.get('/name')
4142
.expect('SequenceApp');
4243
});
@@ -45,7 +46,7 @@ describe('Sequence', () => {
4546
server.handler((sequence, request, response) => {
4647
sequence.send(response, 'hello world');
4748
});
48-
return whenIMakeRequestTo(app)
49+
return whenIRequest()
4950
.get('/')
5051
.expect('hello world');
5152
});
@@ -61,7 +62,7 @@ describe('Sequence', () => {
6162
// bind user defined sequence
6263
server.sequence(MySequence);
6364

64-
whenIMakeRequestTo(app)
65+
whenIRequest()
6566
.get('/')
6667
.expect('hello world');
6768
});
@@ -86,19 +87,37 @@ describe('Sequence', () => {
8687

8788
server.sequence(MySequence);
8889

89-
return whenIMakeRequestTo(app)
90+
return whenIRequest()
9091
.get('/name')
9192
.expect('MySequence SequenceApp');
9293
});
9394

95+
it('allows users to bind a custom sequence class via app.sequence()', async () => {
96+
class MySequence {
97+
constructor(@inject(SequenceActions.SEND) protected send: Send) {}
98+
99+
async handle(req: ParsedRequest, res: ServerResponse) {
100+
this.send(res, 'MySequence was invoked.');
101+
}
102+
}
103+
104+
const restApp = new RestApplication();
105+
restApp.sequence(MySequence);
106+
107+
const appServer = await restApp.getServer(RestServer);
108+
await whenIRequest(appServer)
109+
.get('/name')
110+
.expect('MySequence was invoked.');
111+
});
112+
94113
it('user-defined Send', () => {
95114
const send: Send = (response, result) => {
96115
response.setHeader('content-type', 'text/plain');
97116
response.end(`CUSTOM FORMAT: ${result}`);
98117
};
99118
server.bind(SequenceActions.SEND).to(send);
100119

101-
return whenIMakeRequestTo(app)
120+
return whenIRequest()
102121
.get('/name')
103122
.expect('CUSTOM FORMAT: SequenceApp');
104123
});
@@ -110,7 +129,7 @@ describe('Sequence', () => {
110129
};
111130
server.bind(SequenceActions.REJECT).to(reject);
112131

113-
return whenIMakeRequestTo(app)
132+
return whenIRequest()
114133
.get('/unknown-url')
115134
.expect(418);
116135
});
@@ -122,7 +141,7 @@ describe('Sequence', () => {
122141
sequence.send(response, sequence.ctx.getSync('test'));
123142
});
124143

125-
return whenIMakeRequestTo(app)
144+
return whenIRequest()
126145
.get('/')
127146
.expect('hello world');
128147
});
@@ -149,7 +168,7 @@ describe('Sequence', () => {
149168
server.sequence(MySequence);
150169
app.bind('test').to('hello world');
151170

152-
return whenIMakeRequestTo(app)
171+
return whenIRequest()
153172
.get('/')
154173
.expect('hello world');
155174
});
@@ -194,7 +213,7 @@ describe('Sequence', () => {
194213
app.controller(controller);
195214
}
196215

197-
function whenIMakeRequestTo(application: Application): Client {
198-
return createClientForHandler(server.handleHttp);
216+
function whenIRequest(restServer: RestServer = server): Client {
217+
return createClientForHandler(restServer.handleHttp);
199218
}
200219
});

0 commit comments

Comments
 (0)