diff --git a/packages/openapi-v3/package-lock.json b/packages/openapi-v3/package-lock.json index 461f647e086e..087f3537bafb 100644 --- a/packages/openapi-v3/package-lock.json +++ b/packages/openapi-v3/package-lock.json @@ -30,6 +30,112 @@ "ms": "^2.1.1" } }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "es-abstract": { + "version": "1.17.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0-next.1.tgz", + "integrity": "sha512-7MmGr03N7Rnuid6+wyhD9sHNE2n4tFSwExnU2lQl3lIo2ShXWGePY80zYaoMOmILWv57H0amMjZGHNzzGG70Rw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.0", + "string.prototype.trimright": "^2.1.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "json-merge-patch": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-merge-patch/-/json-merge-patch-0.2.3.tgz", + "integrity": "sha1-+ixrWvh9p3uuKWalidUuI+2B/kA=", + "requires": { + "deep-equal": "^1.0.0" + } + }, "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", @@ -40,10 +146,63 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" + }, + "object-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", + "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, "openapi3-ts": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-1.3.0.tgz", "integrity": "sha512-Xk3hsB0PzB4dzr/r/FdmK+VfQbZH7lQQ2iipMS1/1eoz1wUvh5R7rmOakYvw0bQJJE6PYrOLx8UHsYmzgTr+YQ==" + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "string.prototype.trimleft": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", + "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", + "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } } } } diff --git a/packages/openapi-v3/package.json b/packages/openapi-v3/package.json index 9860f951f9ac..8a081dab7b80 100644 --- a/packages/openapi-v3/package.json +++ b/packages/openapi-v3/package.json @@ -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", diff --git a/packages/openapi-v3/src/__tests__/unit/enhancers/fixtures/application.ts b/packages/openapi-v3/src/__tests__/unit/enhancers/fixtures/application.ts new file mode 100644 index 000000000000..bac323dd77d8 --- /dev/null +++ b/packages/openapi-v3/src/__tests__/unit/enhancers/fixtures/application.ts @@ -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); + } +} diff --git a/packages/openapi-v3/src/__tests__/unit/enhancers/fixtures/info.spec.extension.ts b/packages/openapi-v3/src/__tests__/unit/enhancers/fixtures/info.spec.extension.ts new file mode 100644 index 000000000000..aa524f81e1f6 --- /dev/null +++ b/packages/openapi-v3/src/__tests__/unit/enhancers/fixtures/info.spec.extension.ts @@ -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; + } +} diff --git a/packages/openapi-v3/src/__tests__/unit/enhancers/fixtures/security.spec.extension.ts b/packages/openapi-v3/src/__tests__/unit/enhancers/fixtures/security.spec.extension.ts new file mode 100644 index 000000000000..4abea1fb66d2 --- /dev/null +++ b/packages/openapi-v3/src/__tests__/unit/enhancers/fixtures/security.spec.extension.ts @@ -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; + } +} diff --git a/packages/openapi-v3/src/__tests__/unit/enhancers/spec-enhancer.extensions.unit.ts b/packages/openapi-v3/src/__tests__/unit/enhancers/spec-enhancer.extensions.unit.ts new file mode 100644 index 000000000000..99cfc5adfb5e --- /dev/null +++ b/packages/openapi-v3/src/__tests__/unit/enhancers/spec-enhancer.extensions.unit.ts @@ -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', + }, + }, + }, + }, + }, + }, + }, + }, + }; + } +}); diff --git a/packages/openapi-v3/src/controller-spec.ts b/packages/openapi-v3/src/controller-spec.ts index f6ac1587c31a..41249c151291 100644 --- a/packages/openapi-v3/src/controller-spec.ts +++ b/packages/openapi-v3/src/controller-spec.ts @@ -3,13 +3,13 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {DecoratorFactory, MetadataInspector} from '@loopback/context'; +import {DecoratorFactory, MetadataInspector} from '@loopback/core'; import { getJsonSchema, getJsonSchemaRef, JsonSchemaOptions, } from '@loopback/repository-json-schema'; -import _ from 'lodash'; +import {includes} from 'lodash'; import {resolveSchema} from './generate-schema'; import {jsonToSchemaObject, SchemaRef} from './json-to-schema'; import {OAI3Keys} from './keys'; @@ -222,7 +222,7 @@ function resolveControllerSpec(constructor: Function): ControllerSpec { const paramTypes = opMetadata.parameterTypes; const isComplexType = (ctor: Function) => - !_.includes([String, Number, Boolean, Array, Object], ctor); + !includes([String, Number, Boolean, Array, Object], ctor); for (const p of paramTypes) { if (isComplexType(p)) { diff --git a/packages/openapi-v3/src/decorators/api.decorator.ts b/packages/openapi-v3/src/decorators/api.decorator.ts index b4f349fedb9d..94a267f35a30 100644 --- a/packages/openapi-v3/src/decorators/api.decorator.ts +++ b/packages/openapi-v3/src/decorators/api.decorator.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ClassDecoratorFactory} from '@loopback/context'; +import {ClassDecoratorFactory} from '@loopback/core'; import {ControllerSpec} from '../controller-spec'; import {OAI3Keys} from '../keys'; diff --git a/packages/openapi-v3/src/decorators/operation.decorator.ts b/packages/openapi-v3/src/decorators/operation.decorator.ts index b7b0d2f3bf48..8551f23ea16b 100644 --- a/packages/openapi-v3/src/decorators/operation.decorator.ts +++ b/packages/openapi-v3/src/decorators/operation.decorator.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {MethodDecoratorFactory} from '@loopback/context'; +import {MethodDecoratorFactory} from '@loopback/core'; import {RestEndpoint} from '../controller-spec'; import {OAI3Keys} from '../keys'; import {OperationObject} from '../types'; diff --git a/packages/openapi-v3/src/decorators/parameter.decorator.ts b/packages/openapi-v3/src/decorators/parameter.decorator.ts index edb09e4e200a..afda71d78644 100644 --- a/packages/openapi-v3/src/decorators/parameter.decorator.ts +++ b/packages/openapi-v3/src/decorators/parameter.decorator.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/context'; +import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/core'; import {resolveSchema} from '../generate-schema'; import {OAI3Keys} from '../keys'; import { diff --git a/packages/openapi-v3/src/decorators/request-body.decorator.ts b/packages/openapi-v3/src/decorators/request-body.decorator.ts index d378b2f46090..b262130d69fe 100644 --- a/packages/openapi-v3/src/decorators/request-body.decorator.ts +++ b/packages/openapi-v3/src/decorators/request-body.decorator.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/context'; +import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/core'; import _ from 'lodash'; import {inspect} from 'util'; import {resolveSchema} from '../generate-schema'; diff --git a/packages/openapi-v3/src/enhancers/index.ts b/packages/openapi-v3/src/enhancers/index.ts new file mode 100644 index 000000000000..64656b151d4f --- /dev/null +++ b/packages/openapi-v3/src/enhancers/index.ts @@ -0,0 +1,3 @@ +export * from './keys'; +export * from './spec-enhancer.service'; +export * from './types'; diff --git a/packages/openapi-v3/src/enhancers/keys.ts b/packages/openapi-v3/src/enhancers/keys.ts new file mode 100644 index 000000000000..367d33a7028a --- /dev/null +++ b/packages/openapi-v3/src/enhancers/keys.ts @@ -0,0 +1,14 @@ +// 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 {BindingKey} from '@loopback/core'; +import {OASEnhancerService} from './spec-enhancer.service'; + +/** + * Strongly-typed binding key for SpecService + */ +export const OAS_ENHANCER_SERVICE = BindingKey.create( + 'services.SpecService', +); diff --git a/packages/openapi-v3/src/enhancers/spec-enhancer.service.ts b/packages/openapi-v3/src/enhancers/spec-enhancer.service.ts new file mode 100644 index 000000000000..c97ba8fd8ab1 --- /dev/null +++ b/packages/openapi-v3/src/enhancers/spec-enhancer.service.ts @@ -0,0 +1,121 @@ +// 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 {config, extensionPoint, extensions, Getter} from '@loopback/core'; +import debugModule from 'debug'; +import * as _ from 'lodash'; +import {inspect} from 'util'; +import {OpenApiSpec} from '../types'; +import {OASEnhancer, OAS_ENHANCER_EXTENSION_POINT_NAME} from './types'; +const jsonmergepatch = require('json-merge-patch'); + +const debug = debugModule('loopback:openapi:spec-enhancer'); + +/** + * Options for the OpenAPI Spec enhancer extension point + */ +export interface OASEnhancerServiceOptions { + // no-op +} + +/** + * An extension point for OpenAPI Spec enhancement + * This service is used for enhancing an OpenAPI spec by loading and applying one or more + * registered enhancers. + * + * A typical use of it would be generating the OpenAPI spec for the endpoints on a server + * in the `@loopback/rest` module. + */ +@extensionPoint(OAS_ENHANCER_EXTENSION_POINT_NAME) +export class OASEnhancerService { + constructor( + /** + * Inject a getter function to fetch spec enhancers + */ + @extensions() + private getEnhancers: Getter, + /** + * An extension point should be able to receive its options via dependency + * injection. + */ + @config() + public readonly options?: OASEnhancerServiceOptions, + ) {} + + private _spec: OpenApiSpec = { + openapi: '3.0.0', + info: { + title: 'LoopBack Application', + version: '1.0.0', + }, + paths: {}, + }; + + /** + * Getter for `_spec` + */ + get spec(): OpenApiSpec { + return this._spec; + } + /** + * Setter for `_spec` + */ + set spec(value: OpenApiSpec) { + this._spec = value; + } + + /** + * Find an enhancer by its name + * @param name The name of the enhancer you want to find + */ + async getEnhancerByName(name: string): Promise { + // Get the latest list of enhancers + const enhancers = await this.getEnhancers(); + return enhancers.find(e => e.name === name); + } + + /** + * Apply a given enhancer's merge function. Return the latest _spec. + * @param name The name of the enhancer you want to apply + */ + async applyEnhancerByName(name: string): Promise { + const enhancer = await this.getEnhancerByName(name); + if (enhancer) this._spec = enhancer.modifySpec(this._spec); + return this._spec; + } + + /** + * Generate OpenAPI spec by apply ALL registered enhancers + * TBD: load enhancers by group names + */ + async generateSpec(options = {}): Promise { + const enhancers = await this.getEnhancers(); + if (_.isEmpty(enhancers)) return this._spec; + for (const e of enhancers) { + this._spec = e.modifySpec(this._spec); + } + debug(`Spec enhancer service, generated spec: ${inspect(this._spec)}`); + return this._spec; + } +} + +/** + * The default merge function to patch the current OpenAPI spec. + * It leverages module `json-merge-patch`'s merge API to merge two json object. + * It returns a new merged object without modifying the original one. + * + * A list of merging rules can be found in test file: + * https://github.com/pierreinglebert/json-merge-patch/blob/master/test/lib/merge.js + * + * @param currentSpec The original spec + * @param patchSpec The patch spec to be merged into the original spec + */ +export function defaultMergeFn( + currentSpec: Partial, + patchSpec: Partial, +) { + const mergedSpec = jsonmergepatch.merge(currentSpec, patchSpec); + return mergedSpec; +} diff --git a/packages/openapi-v3/src/enhancers/types.ts b/packages/openapi-v3/src/enhancers/types.ts new file mode 100644 index 000000000000..ee1193d7fc99 --- /dev/null +++ b/packages/openapi-v3/src/enhancers/types.ts @@ -0,0 +1,30 @@ +// 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 {BindingTemplate, extensionFor} from '@loopback/core'; +import {OpenApiSpec} from '../types'; + +/** + * Typically an extension point defines an interface as the contract for + * extensions to implement + */ +export interface OASEnhancer { + name: string; + modifySpec(spec: OpenApiSpec): OpenApiSpec; +} + +/** + * Name/id of the OAS enhancer extension point + */ +export const OAS_ENHANCER_EXTENSION_POINT_NAME = 'oas-enhancer'; + +/** + * A binding template for spec contributor extensions + */ +export const asSpecEnhancer: BindingTemplate = binding => { + extensionFor(OAS_ENHANCER_EXTENSION_POINT_NAME)(binding); + // is it ok to have a different namespace than the extension point name? + binding.tag({namespace: 'oas-enhancer'}); +}; diff --git a/packages/openapi-v3/src/index.ts b/packages/openapi-v3/src/index.ts index 886c2412a730..08f3a6489ac9 100644 --- a/packages/openapi-v3/src/index.ts +++ b/packages/openapi-v3/src/index.ts @@ -6,6 +6,7 @@ export * from '@loopback/repository-json-schema'; export * from './controller-spec'; export * from './decorators'; +export * from './enhancers'; export * from './filter-schema'; export * from './json-to-schema'; export * from './types'; diff --git a/packages/openapi-v3/src/keys.ts b/packages/openapi-v3/src/keys.ts index 935cfc75ec85..00045ea84d60 100644 --- a/packages/openapi-v3/src/keys.ts +++ b/packages/openapi-v3/src/keys.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {MetadataAccessor} from '@loopback/context'; +import {MetadataAccessor} from '@loopback/core'; import {ControllerSpec, RestEndpoint} from './controller-spec'; import {ParameterObject, RequestBodyObject} from './types';