Skip to content

Commit

Permalink
refactor(rest): implement /openapi.json routes as regular LB4 route…
Browse files Browse the repository at this point in the history
… entries
  • Loading branch information
bajtos committed Mar 7, 2019
1 parent a5652c6 commit 2b5be22
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -787,9 +787,9 @@ paths:
});

it('controls server urls', async () => {
const response = await createClientForHandler(server.requestHandler).get(
'/openapi.json',
);
const response = await createClientForHandler(server.requestHandler)
.get('openapi.json')
.expect(200);
expect(response.body.servers).to.containEql({url: '/api'});
});

Expand Down
65 changes: 9 additions & 56 deletions packages/rest/src/rest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

import {
Binding,
BindingAddress,
BindingScope,
Constructor,
Context,
inject,
BindingScope,
BindingAddress,
} from '@loopback/context';
import {Application, CoreBindings, Server} from '@loopback/core';
import {HttpServer, HttpServerOptions} from '@loopback/http-server';
Expand All @@ -26,7 +26,6 @@ import * as express from 'express';
import {PathParams} from 'express-serve-static-core';
import {IncomingMessage, ServerResponse} from 'http';
import {ServerOptions} from 'https';
import {safeDump} from 'js-yaml';
import {ServeStaticOptions} from 'serve-static';
import {BodyParser, REQUEST_BODY_PARSER_TAG} from './body-parsers';
import {HttpHandler} from './http-handler';
Expand All @@ -38,22 +37,23 @@ import {
ControllerInstance,
ControllerRoute,
createControllerFactoryForBinding,
ExternalExpressRoutes,
RedirectRoute,
Route,
RouteEntry,
RoutingTable,
ExternalExpressRoutes,
RedirectRoute,
} from './router';
import {ServeApiSpecRoute} from './router/serve-api-spec.route';
import {DefaultSequence, SequenceFunction, SequenceHandler} from './sequence';
import {
FindRoute,
InvokeMethod,
ParseParams,
Reject,
Request,
RequestBodyParserOptions,
Response,
Send,
RequestBodyParserOptions,
} from './types';

const debug = debugFactory('loopback:rest:server');
Expand Down Expand Up @@ -236,21 +236,13 @@ export class RestServer extends Context implements Server, HttpServerLike {
*/
protected _setupOpenApiSpecEndpoints() {
if (this.config.openApiSpec.disabled) return;
// NOTE(bajtos) Regular routes are handled through Sequence.
// IMO, this built-in endpoint should not run through a Sequence,
// because it's not part of the application API itself.
// E.g. if the app implements access/audit logs, I don't want
// this endpoint to trigger a log entry. If the server implements
// content-negotiation to support XML clients, I don't want the OpenAPI
// spec to be converted into an XML response.
const mapping = this.config.openApiSpec.endpointMapping!;
// Serving OpenAPI spec
const mapping = this.config.openApiSpec.endpointMapping!;
for (const p in mapping) {
this._expressApp.get(p, (req, res) =>
this._serveOpenApiSpec(req, res, mapping[p]),
);
this.route(new ServeApiSpecRoute(p, this, mapping[p]));
}

// Redirect to externally hosted swagger-ui instance
const explorerPaths = ['/swagger-ui', '/explorer'];
this._expressApp.get(explorerPaths, (req, res, next) =>
this._redirectToSwaggerUI(req, res, next),
Expand Down Expand Up @@ -361,45 +353,6 @@ export class RestServer extends Context implements Server, HttpServerLike {
);
}

private async _serveOpenApiSpec(
request: Request,
response: Response,
specForm?: OpenApiSpecForm,
) {
const requestContext = new RequestContext(
request,
response,
this,
this.config,
);

specForm = specForm || {version: '3.0.0', format: 'json'};
let specObj = this.getApiSpec();
if (this.config.openApiSpec.setServersFromRequest) {
specObj = Object.assign({}, specObj);
specObj.servers = [{url: requestContext.requestedBaseUrl}];
}

const basePath = requestContext.basePath;
if (specObj.servers && basePath) {
for (const s of specObj.servers) {
// Update the default server url to honor `basePath`
if (s.url === '/') {
s.url = basePath;
}
}
}

if (specForm.format === 'json') {
const spec = JSON.stringify(specObj, null, 2);
response.setHeader('content-type', 'application/json; charset=utf-8');
response.end(spec, 'utf-8');
} else {
const yaml = safeDump(specObj, {});
response.setHeader('content-type', 'text/yaml; charset=utf-8');
response.end(yaml, 'utf-8');
}
}
private async _redirectToSwaggerUI(
request: Request,
response: Response,
Expand Down
69 changes: 69 additions & 0 deletions packages/rest/src/router/serve-api-spec.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright IBM Corp. 2017, 2018. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {OperationObject} from '@loopback/openapi-v3-types';
import {safeDump} from 'js-yaml';
import {RouteEntry} from '.';
import {RequestContext} from '../request-context';
import {OpenApiSpecForm, RestServer} from '../rest.server';
import {OperationArgs, OperationRetval} from '../types';

export class ServeApiSpecRoute implements RouteEntry {
readonly verb: string = 'get';
readonly spec: OperationObject = {
description: 'LoopBack route serving OpenAPI spec',
'x-visibility': 'undocumented',
responses: {},
};

constructor(
public readonly path: string,
private readonly server: RestServer,
private readonly specForm: OpenApiSpecForm = {
version: '3.0.0',
format: 'json',
},
) {}

async invokeHandler(
requestContext: RequestContext,
args: OperationArgs,
): Promise<OperationRetval> {
const {response, requestedBaseUrl, basePath} = requestContext;
console.log('requested url %s basePath %s', requestedBaseUrl, basePath);
let specObj = this.server.getApiSpec();
if (requestContext.serverConfig.openApiSpec.setServersFromRequest) {
specObj = Object.assign({}, specObj);
specObj.servers = [{url: requestedBaseUrl}];
}

if (specObj.servers && basePath) {
for (const s of specObj.servers) {
// Update the default server url to honor `basePath`
if (s.url === '/') {
s.url = basePath;
}
}
}

if (this.specForm.format === 'json') {
const spec = JSON.stringify(specObj, null, 2);
response.setHeader('content-type', 'application/json; charset=utf-8');
response.end(spec, 'utf-8');
} else {
const yaml = safeDump(specObj, {});
response.setHeader('content-type', 'text/yaml; charset=utf-8');
response.end(yaml, 'utf-8');
}
}

updateBindings(requestContext: RequestContext) {
// no-op
}

describe(): string {
return `${this.spec.description} at ${this.path}`;
}
}

0 comments on commit 2b5be22

Please sign in to comment.