Skip to content

Commit

Permalink
feat: openapi spec contributor extension point
Browse files Browse the repository at this point in the history
  • Loading branch information
jannyHou committed Dec 16, 2019
1 parent 58c331c commit b859065
Show file tree
Hide file tree
Showing 17 changed files with 535 additions and 15 deletions.
159 changes: 159 additions & 0 deletions packages/openapi-v3/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions packages/openapi-v3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
"node": ">=8.9"
},
"dependencies": {
"@loopback/context": "^1.25.0",
"@loopback/repository-json-schema": "^1.11.3",
"@loopback/core": "^1.12.0",
"debug": "^4.1.1",
"lodash": "^4.17.15",
"openapi3-ts": "^1.3.0"
"openapi3-ts": "^1.3.0",
"json-merge-patch": "^0.2.3"
},
"devDependencies": {
"@loopback/build": "^3.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Application, createBindingFromClass} from '@loopback/core';
import {OASEnhancerService, OAS_ENHANCER_SERVICE} from '../../../..';
import {InfoSpecEnhancer} from './info.spec.extension';
import {SecuritySpecEnhancer} from './security.spec.extension';

export class SpecServiceApplication extends Application {
constructor() {
super();
this.add(
createBindingFromClass(OASEnhancerService, {
key: OAS_ENHANCER_SERVICE,
}),
);
this.add(createBindingFromClass(SecuritySpecEnhancer));
this.add(createBindingFromClass(InfoSpecEnhancer));
}

async main() {}

getSpecService() {
return this.get(OAS_ENHANCER_SERVICE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/openapi-v3
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {bind} from '@loopback/core';
import debugModule from 'debug';
import {inspect} from 'util';
import {defaultMergeFn} from '../../../..';
import {asSpecEnhancer, OASEnhancer} from '../../../../enhancers/types';
import {OpenApiSpec} from '../../../../types';
const debug = debugModule('loopback:openapi:spec-enhancer');

/**
* A spec enhancer to add OpenAPI info spec
*/
@bind(asSpecEnhancer)
export class InfoSpecEnhancer implements OASEnhancer {
name = 'info';

modifySpec(spec: OpenApiSpec): OpenApiSpec {
const InfoPatchSpec = {
info: {title: 'LoopBack Test Application', version: '1.0.1'},
};
const mergedSpec = defaultMergeFn(spec, InfoPatchSpec);
debug(`security spec extension, merged spec: ${inspect(mergedSpec)}`);
return mergedSpec;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/openapi-v3
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {bind} from '@loopback/core';
import debugModule from 'debug';
import {inspect} from 'util';
import {
defaultMergeFn,
ReferenceObject,
SecuritySchemeObject,
} from '../../../..';
import {asSpecEnhancer, OASEnhancer} from '../../../../enhancers/types';
import {OpenApiSpec} from '../../../../types';
const debug = debugModule('loopback:openapi:spec-enhancer');

export type SecuritySchemeObjects = {
[securityScheme: string]: SecuritySchemeObject | ReferenceObject;
};

export const SECURITY_SCHEME_SPEC: SecuritySchemeObjects = {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
};

/**
* A spec enhancer to add bearer token OpenAPI security entry to
* `spec.component.securitySchemes`
*/
@bind(asSpecEnhancer)
export class SecuritySpecEnhancer implements OASEnhancer {
name = 'security';

modifySpec(spec: OpenApiSpec): OpenApiSpec {
const patchSpec = {components: {securitySchemes: SECURITY_SCHEME_SPEC}};
const mergedSpec = defaultMergeFn(spec, patchSpec);
debug(`security spec extension, merged spec: ${inspect(mergedSpec)}`);
return mergedSpec;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/openapi-v3
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {OASEnhancerService} from '../../..';
import {SpecServiceApplication} from './fixtures/application';

describe('spec-enhancer-extension-point', () => {
let app: SpecServiceApplication;
let specService: OASEnhancerService;

beforeEach(givenAppWithSpecComponent);
beforeEach(findSpecService);

it('setter - can set spec', async () => {
const EXPECTED_SPEC = {
openapi: '3.0.0',
info: {title: 'LoopBack Application', version: '1.0.0'},
// setter adds paths spec based on the default spec
paths: getSamplePathSpec(),
};
const PATHS_SPEC = getSamplePathSpec();
specService.spec = {...specService.spec, paths: PATHS_SPEC};
expect(specService.spec).to.eql(EXPECTED_SPEC);
});

it('generateSpec - loads and create spec for ALL registered extensions', async () => {
const EXPECTED_SPEC = {
openapi: '3.0.0',
// info object is updated by the info enhancer
info: {title: 'LoopBack Test Application', version: '1.0.1'},
paths: {},
// the security scheme entry is added by the security enhancer
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
};
const specFromService = await specService.generateSpec();
expect(specFromService).to.eql(EXPECTED_SPEC);
});

it('getEnhancerByName - returns the enhancer with a given name', async () => {
const enhancer = await specService.getEnhancerByName('info');
expect(enhancer).to.not.be.undefined();
expect(enhancer?.name).to.equal('info');
});

it('applyEnhancerByName - returns the merged spec after applying a given enhancer', async () => {
const EXPECTED_SPEC = {
openapi: '3.0.0',
// info object is updated by the info enhancer
info: {title: 'LoopBack Test Application', version: '1.0.1'},
paths: {},
};
const mergedSpec = await specService.applyEnhancerByName('info');
expect(mergedSpec).to.eql(EXPECTED_SPEC);
});

function givenAppWithSpecComponent() {
app = new SpecServiceApplication();
}

async function findSpecService() {
specService = await app.getSpecService();
}

function getSamplePathSpec() {
return {
'/pets': {
get: {
description:
'Returns all pets from the system that the user has access to',
responses: {
'200': {
description: 'A list of pets.',
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/pet',
},
},
},
},
},
},
},
},
};
}
});
Loading

0 comments on commit b859065

Please sign in to comment.