Skip to content
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
77 changes: 76 additions & 1 deletion packages/rest/src/rest-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!',
Expand Down Expand Up @@ -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);
}
}
5 changes: 4 additions & 1 deletion packages/rest/src/rest-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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());
}
}

Expand Down
11 changes: 4 additions & 7 deletions packages/rest/src/rest-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
78 changes: 78 additions & 0 deletions packages/rest/test/acceptance/routing/routing.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
RestBindings,
RestServer,
RestComponent,
RestApplication,
} from '../../..';

import {api, get, param} from '@loopback/openapi-v2';
Expand Down Expand Up @@ -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 = <OperationObject>{
parameters: [
<ParameterObject>{name: 'name', in: 'query', type: 'string'},
],
responses: {
200: <ResponseObject>{
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() {
Expand Down
39 changes: 29 additions & 10 deletions packages/rest/test/acceptance/sequence/sequence.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
RestBindings,
RestServer,
RestComponent,
RestApplication,
} from '../../..';
import {api} from '@loopback/openapi-v2';
import {Application} from '@loopback/core';
Expand All @@ -36,7 +37,7 @@ describe('Sequence', () => {
let server: RestServer;
beforeEach(givenAppWithController);
it('provides a default sequence', async () => {
whenIMakeRequestTo(app)
whenIRequest()
.get('/name')
.expect('SequenceApp');
});
Expand All @@ -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');
});
Expand All @@ -61,7 +62,7 @@ describe('Sequence', () => {
// bind user defined sequence
server.sequence(MySequence);

whenIMakeRequestTo(app)
whenIRequest()
.get('/')
.expect('hello world');
});
Expand All @@ -86,19 +87,37 @@ 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');
response.end(`CUSTOM FORMAT: ${result}`);
};
server.bind(SequenceActions.SEND).to(send);

return whenIMakeRequestTo(app)
return whenIRequest()
.get('/name')
.expect('CUSTOM FORMAT: SequenceApp');
});
Expand All @@ -110,7 +129,7 @@ describe('Sequence', () => {
};
server.bind(SequenceActions.REJECT).to(reject);

return whenIMakeRequestTo(app)
return whenIRequest()
.get('/unknown-url')
.expect(418);
});
Expand All @@ -122,7 +141,7 @@ describe('Sequence', () => {
sequence.send(response, sequence.ctx.getSync('test'));
});

return whenIMakeRequestTo(app)
return whenIRequest()
.get('/')
.expect('hello world');
});
Expand All @@ -149,7 +168,7 @@ describe('Sequence', () => {
server.sequence(MySequence);
app.bind('test').to('hello world');

return whenIMakeRequestTo(app)
return whenIRequest()
.get('/')
.expect('hello world');
});
Expand Down Expand Up @@ -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);
}
});