Skip to content

Commit

Permalink
feat: add example enhancers
Browse files Browse the repository at this point in the history
add example enhancers

Signed-off-by: Douglas McConnachie <[email protected]>
  • Loading branch information
dougal83 committed Feb 3, 2020
1 parent 440eb51 commit 71efa98
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
OperationSpecBuilder,
} from '@loopback/openapi-spec-builder';
import {expect} from '@loopback/testlab';
import {ConsolidationEnhancer} from '../../..';
import {ConsolidateSpecEnhancer} from '../../..';

const consolidationEnhancer = new ConsolidationEnhancer();
const specEnhancer = new ConsolidateSpecEnhancer();

describe('consolidateSchemaObjects', () => {
it('moves schema with title to component.schemas, replace with reference', () => {
Expand Down Expand Up @@ -64,7 +64,7 @@ describe('consolidateSchemaObjects', () => {
)
.build();

expect(consolidationEnhancer.modifySpec(inputSpec)).to.eql(expectedSpec);
expect(specEnhancer.modifySpec(inputSpec)).to.eql(expectedSpec);
});

it('ignores schema without title property', () => {
Expand All @@ -89,7 +89,7 @@ describe('consolidateSchemaObjects', () => {
)
.build();

expect(consolidationEnhancer.modifySpec(inputSpec)).to.eql(inputSpec);
expect(specEnhancer.modifySpec(inputSpec)).to.eql(inputSpec);
});

it('Avoids naming collision', () => {
Expand Down Expand Up @@ -161,6 +161,6 @@ describe('consolidateSchemaObjects', () => {
)
.build();

expect(consolidationEnhancer.modifySpec(inputSpec)).to.eql(expectedSpec);
expect(specEnhancer.modifySpec(inputSpec)).to.eql(expectedSpec);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import {
OpenApiSpec,
ReferenceObject,
SchemaObject,
} from '../types';
import {asSpecEnhancer, OASEnhancer} from './types';
} from '../../types';
import {asSpecEnhancer, OASEnhancer} from '../types';

/**
* A spec enhancer to consolidate OpenAPI specs
*
*/
@bind(asSpecEnhancer)
export class ConsolidationEnhancer implements OASEnhancer {
export class ConsolidateSpecEnhancer implements OASEnhancer {
name = 'consolidate';

modifySpec(spec: OpenApiSpec): OpenApiSpec {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {bind} from '@loopback/core';
import {OperationObject} from 'openapi3-ts';
import {OpenApiSpec} from '../../types';
import {asSpecEnhancer, OASEnhancer} from '../types';

/**
* A spec enhancer to add verbose deprecation status to OperationObjects
*
*/
@bind(asSpecEnhancer)
export class DeprecateSpecEnhancer implements OASEnhancer {
name = 'deprecate';

modifySpec(spec: OpenApiSpec): OpenApiSpec {
try {
return this.defaultDeprecate(spec);
} catch {
console.log('Default Deprecate Enhancer failed, returned original spec.');
return spec;
}
}

/**
* add verbose deprecation status to OperationObjects if not set
*
*/
private defaultDeprecate(spec: OpenApiSpec): OpenApiSpec {
Object.keys(spec.paths).forEach(path =>
Object.keys(spec.paths[path]).forEach(op => {
const OpObj = spec.paths[path][op] as OperationObject;
if (!OpObj.deprecated) OpObj.deprecated = false;
}),
);

return spec;
}
}
103 changes: 103 additions & 0 deletions packages/openapi-v3/src/enhancers/extensions/prune.spec.extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {bind} from '@loopback/core';
import _ from 'lodash';
import {ISpecificationExtension, OpenApiSpec} from '../..';
import {asSpecEnhancer, OASEnhancer} from '../types';

/**
* A spec enhancer to remove unneccesary properties from the OpenAPI spec
*
*/
@bind(asSpecEnhancer)
export class PruneSpecEnhancer implements OASEnhancer {
name = 'prune';

modifySpec(spec: OpenApiSpec): OpenApiSpec {
try {
// note, title is valid but pruning as not needed atm
const values = ['x-', 'title'];
return this.pruneSchemas({spec, values});
} catch {
console.log('Prune Enhancer failed, returned original spec.');
return spec;
}
}

/**
* Recursively walk OpenApiSpec and prune excess properties
*
*/
private pruneSchemas(ctx: PruneContext): OpenApiSpec {
// use 'paths' as crawl root
this.recursiveWalk(ctx.spec.paths, ['paths'], ctx);
// use 'components.schemas' as crawl root
if (ctx.spec.components && ctx.spec.components.schemas) {
this.recursiveWalk(
ctx.spec.components.schemas,
['components', 'schemas'],
ctx,
);
}

return ctx.spec;
}

private recursiveWalk(
rootSchema: ISpecificationExtension,
parentPath: Array<string>,
ctx: PruneContext,
) {
if (this.isTraversable(rootSchema)) {
Object.entries(rootSchema).forEach(([key, subSchema]) => {
if (subSchema) {
this.recursiveWalk(subSchema, parentPath.concat(key), ctx);
if (!this.isTraversable(subSchema)) {
this.processSchema(rootSchema, parentPath, ctx);
}
}
});
}
}

/**
* Carry out schema pruning after tree traversal. If key starts with any of
* the propsToRemove then unset.
*
* @param schema - current schema element to process
* @param parentPath - path object to parent
* @param ctx - prune context object
*
*/
private async processSchema(
schema: ISpecificationExtension,
parentPath: Array<string>,
ctx: PruneContext,
) {
Object.keys(schema).forEach(key => {
ctx.values.forEach(name => {
if (key.startsWith(name)) {
this.patchRemove(parentPath.concat(key), ctx);
}
});
});
}

patchRemove(path: Array<string>, ctx: PruneContext) {
_.unset(ctx.spec, path);
}

private isTraversable(schema: ISpecificationExtension): boolean {
return schema && typeof schema === 'object' ? true : false;
}
}

/**
* Prune context
*
* @param spec - subject openapi specification
* @param values - array of properties to remove
*
*/
interface PruneContext {
spec: OpenApiSpec;
values: Array<string>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {bind} from '@loopback/core';
import {OpenApiSpec} from '../../types';
import {asSpecEnhancer, OASEnhancer} from '../types';

/**
* A spec enhancer to restructure the OpenAPI spec in an opinionated manner.
* (dougal83)
*
*/
@bind(asSpecEnhancer)
export class StructureSpecEnhancer implements OASEnhancer {
name = 'structure';

modifySpec(spec: OpenApiSpec): OpenApiSpec {
try {
return this.restructureSpec(spec);
} catch {
console.log('Restructure Enhancer failed, returned original spec.');
return spec;
}
}

/**
* Structure OpenApiSpec in an opinionated manner
*
*/
private restructureSpec(spec: OpenApiSpec): OpenApiSpec {
spec = Object.assign(
{
openapi: undefined,
info: undefined,
servers: undefined,
paths: undefined,
components: undefined,
tags: undefined,
},
spec,
);

Object.keys(spec.paths).forEach(path =>
Object.keys(spec.paths[path]).forEach(op => {
spec.paths[path][op] = Object.assign(
{
tags: undefined,
summary: undefined,
description: undefined,
operationId: undefined,
parameters: undefined,
responses: undefined,
depreciated: undefined,
},
spec.paths[path][op],
);
}),
);

return spec;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {bind} from '@loopback/core';
import {OperationObject} from 'openapi3-ts';
import {OpenApiSpec} from '../../types';
import {asSpecEnhancer, OASEnhancer} from '../types';

/**
* A spec enhancer to combine OperationObject Tags OpenApiSpec into spec.tags
*
*/
@bind(asSpecEnhancer)
export class TagsSpecEnhancer implements OASEnhancer {
name = 'tags';

modifySpec(spec: OpenApiSpec): OpenApiSpec {
try {
return this.tagsConsolidate(spec);
} catch {
console.log('Tags Enhancer failed, returned original spec.');
return spec;
}
}

/**
* Combine OperationObject Tags OpenApiSpec into spec.tags
*
*/
private tagsConsolidate(spec: OpenApiSpec): OpenApiSpec {
Object.keys(spec.paths).forEach(path =>
Object.keys(spec.paths[path]).forEach(op => {
const OpObj = spec.paths[path][op] as OperationObject;
if (OpObj.tags) this.patchTags(OpObj.tags, spec);
}),
);

return spec;
}

// TODO(dougal83) desc. resolution
private patchTags(tags: Array<string>, spec: OpenApiSpec) {
if (!spec.tags) {
spec.tags = [];
}
tags.forEach(name => {
if (spec.tags!.findIndex(tag => tag.name === name) <= 0)
spec.tags!.push({name, description: ''});
});
}
}
6 changes: 5 additions & 1 deletion packages/openapi-v3/src/enhancers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './consolidate-schema.enhancer';
export * from './extensions/consolidate.spec.extension';
export * from './extensions/deprecate.spec.extension';
export * from './extensions/prune.spec.extension';
export * from './extensions/security.spec.extension';
export * from './extensions/structure.spec.extension';
export * from './extensions/tags.spec.extension';
export * from './keys';
export * from './spec-enhancer.service';
export * from './types';
16 changes: 12 additions & 4 deletions packages/rest/src/rest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ import {
import {Application, CoreBindings, Server} from '@loopback/core';
import {HttpServer, HttpServerOptions} from '@loopback/http-server';
import {
ConsolidationEnhancer,
ConsolidateSpecEnhancer,
DeprecateSpecEnhancer,
getControllerSpec,
OASEnhancerService,
OAS_ENHANCER_SERVICE,
OpenAPIObject,
OpenApiSpec,
OperationObject,
PruneSpecEnhancer,
SecuritySpecEnhancer,
ServerObject,
StructureSpecEnhancer,
TagsSpecEnhancer,
} from '@loopback/openapi-v3';
import {AssertionError} from 'assert';
import cors from 'cors';
Expand Down Expand Up @@ -236,10 +240,16 @@ export class RestServer extends Context implements Server, HttpServerLike {
}).inScope(BindingScope.SINGLETON),
);
this._registerCoreSpecEnhancers();
// register example extensions
this.add(createBindingFromClass(PruneSpecEnhancer));
this.add(createBindingFromClass(DeprecateSpecEnhancer));
this.add(createBindingFromClass(TagsSpecEnhancer));
this.add(createBindingFromClass(StructureSpecEnhancer));
this._OASEnhancer = this.getSync(OAS_ENHANCER_SERVICE);
}

private _registerCoreSpecEnhancers() {
this.add(createBindingFromClass(ConsolidateSpecEnhancer));
this.add(createBindingFromClass(SecuritySpecEnhancer));
}

Expand Down Expand Up @@ -742,12 +752,10 @@ export class RestServer extends Context implements Server, HttpServerLike {
if (requestContext) {
spec = this.updateSpecFromRequest(spec, requestContext);
}
const consolidationEnhancer = new ConsolidationEnhancer();
spec = consolidationEnhancer.modifySpec(spec);

// Apply OAS enhancers to the OpenAPI specification
this.OASEnhancer.spec = spec;
spec = await this.OASEnhancer.applyEnhancerByName('security');
spec = await this.OASEnhancer.applyAllEnhancers();

return spec;
}
Expand Down

0 comments on commit 71efa98

Please sign in to comment.