@loopback/openapi-spec-builder
Version:
Make it easy to create OpenAPI (Swagger) specification documents in your tests using the builder pattern.
395 lines (361 loc) • 10.5 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
// Node module: @loopback/openapi-spec-builder
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import assert from 'assert';
import {
CallbackObject,
ComponentsObject,
ExampleObject,
HeaderObject,
ISpecificationExtension,
LinkObject,
OpenAPIObject,
OperationObject,
ParameterObject,
ReferenceObject,
RequestBodyObject,
ResponseObject,
SchemaObject,
SecuritySchemeObject,
} from 'openapi3-ts';
/**
* Create a new instance of OpenApiSpecBuilder.
*
* @param basePath - The base path on which the API is served.
*/
export function anOpenApiSpec() {
return new OpenApiSpecBuilder();
}
/**
* Create a new instance of OperationSpecBuilder.
*/
export function anOperationSpec() {
return new OperationSpecBuilder();
}
/**
* Create a new instance of ComponentsSpecBuilder.
*/
export function aComponentsSpec() {
return new ComponentsSpecBuilder();
}
export class BuilderBase<T extends ISpecificationExtension> {
protected _spec: T;
constructor(initialSpec: T) {
this._spec = initialSpec;
}
/**
* Add a custom (extension) property to the spec object.
*
* @param key - The property name starting with "x-".
* @param value - The property value.
*/
withExtension(
key: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
): this {
assert(
key.startsWith('x-'),
`Invalid extension ${key}, extension keys must be prefixed with "x-"`,
);
// `this._spec[key] = value;` is broken in TypeScript 3.5
// See https://github.com/microsoft/TypeScript/issues/31661
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this._spec as Record<string, any>)[key] = value;
return this;
}
/**
* Build the spec object.
*/
build(): T {
// TODO(bajtos): deep-clone
return this._spec;
}
}
/**
* A builder for creating OpenApiSpec documents.
*/
export class OpenApiSpecBuilder extends BuilderBase<OpenAPIObject> {
/**
* @param basePath - The base path on which the API is served.
*/
constructor() {
super({
openapi: '3.0.0',
info: {
title: 'LoopBack Application',
version: '1.0.0',
},
paths: {},
servers: [{url: '/'}],
});
}
/**
* Define a new OperationObject at the given path and verb (method).
*
* @param verb - The HTTP verb.
* @param path - The path relative to basePath.
* @param spec - Additional specification of the operation.
*/
withOperation(
verb: string,
path: string,
spec: OperationObject | OperationSpecBuilder,
): this {
if (spec instanceof OperationSpecBuilder) spec = spec.build();
if (!this._spec.paths[path]) this._spec.paths[path] = {};
this._spec.paths[path][verb] = spec;
return this;
}
/**
* Define a new operation that returns a string response.
*
* @param verb - The HTTP verb.
* @param path - The path relative to basePath.
* @param operationName - The name of the controller method implementing
* this operation (`x-operation-name` field).
*/
withOperationReturningString(
verb: string,
path: string,
operationName?: string,
): this {
const spec = anOperationSpec().withStringResponse(200);
if (operationName) spec.withOperationName(operationName);
return this.withOperation(verb, path, spec);
}
/**
* Define a new ComponentsObject.
*
* @param spec - Specification of the components.
*/
withComponents(spec: ComponentsObject | ComponentsSpecBuilder): this {
if (spec instanceof ComponentsSpecBuilder) spec = spec.build();
if (!this._spec.components) this._spec.components = spec;
return this;
}
}
/**
* A builder for creating OperationObject specifications.
*/
export class OperationSpecBuilder extends BuilderBase<OperationObject> {
constructor() {
super({
responses: {'200': {description: 'An undocumented response body.'}},
});
}
/**
* Describe a response for a given HTTP status code.
* @param status - HTTP status code or string "default"
* @param responseSpec - Specification of the response
*/
withResponse(status: number | 'default', responseSpec: ResponseObject): this {
// OpenAPI spec uses string indices, i.e. 200 OK uses "200" as the index
this._spec.responses[status.toString()] = responseSpec;
return this;
}
withStringResponse(status: number | 'default' = 200): this {
return this.withResponse(status, {
description: 'The string result.',
content: {
'text/plain': {
schema: {type: 'string'},
},
},
});
}
/**
* Describe one more parameters accepted by the operation.
* Note that parameters are positional in OpenAPI Spec, therefore
* the first call of `withParameter` defines the first parameter,
* the second call defines the second parameter, etc.
* @param parameterSpecs
*/
withParameter(...parameterSpecs: ParameterObject[]): this {
if (!this._spec.parameters) this._spec.parameters = [];
this._spec.parameters.push(...parameterSpecs);
return this;
}
withRequestBody(requestBodySpec: RequestBodyObject): this {
this._spec.requestBody = requestBodySpec;
return this;
}
/**
* Define the operation name (controller method name).
*
* @param name - The name of the controller method implementing this operation.
*/
withOperationName(name: string): this {
this.withExtension('x-operation-name', name);
this.setupOperationId();
return this;
}
/**
* Define the controller name (controller name).
*
* @param name - The name of the controller containing this operation.
*/
withControllerName(name: string): this {
this.withExtension('x-controller-name', name);
this.setupOperationId();
return this;
}
/**
* Set up the `operationId` if not configured
*/
private setupOperationId() {
if (this._spec.operationId) return;
const controllerName = this._spec['x-controller-name'];
const operationName = this._spec['x-operation-name'];
if (controllerName && operationName) {
// Build the operationId as `<controllerName>.<operationName>`
// Please note API explorer (https://github.com/swagger-api/swagger-js/)
// will normalize it as `<controllerName>_<operationName>`
this._spec.operationId = controllerName + '.' + operationName;
}
}
/**
* Define the operationId
* @param operationId - Operation id
*/
withOperationId(operationId: string): this {
this._spec.operationId = operationId;
return this;
}
/**
* Describe tags associated with the operation
* @param tags
*/
withTags(tags: string | string[]): this {
if (!this._spec.tags) this._spec.tags = [];
if (typeof tags === 'string') tags = [tags];
this._spec.tags.push(...tags);
return this;
}
}
/**
* A builder for creating ComponentsObject specifications.
*/
export class ComponentsSpecBuilder extends BuilderBase<ComponentsObject> {
constructor() {
super({});
}
/**
* Define a component schema.
*
* @param name - The name of the schema
* @param schema - Specification of the schema
*
*/
withSchema(name: string, schema: SchemaObject | ReferenceObject): this {
if (!this._spec.schemas) this._spec.schemas = {};
this._spec.schemas[name] = schema;
return this;
}
/**
* Define a component response.
*
* @param name - The name of the response
* @param response - Specification of the response
*
*/
withResponse(name: string, response: ResponseObject | ReferenceObject): this {
if (!this._spec.responses) this._spec.responses = {};
this._spec.responses[name] = response;
return this;
}
/**
* Define a component parameter.
*
* @param name - The name of the parameter
* @param parameter - Specification of the parameter
*
*/
withParameter(
name: string,
parameter: ParameterObject | ReferenceObject,
): this {
if (!this._spec.parameters) this._spec.parameters = {};
this._spec.parameters[name] = parameter;
return this;
}
/**
* Define a component example.
*
* @param name - The name of the example
* @param example - Specification of the example
*
*/
withExample(name: string, example: ExampleObject | ReferenceObject): this {
if (!this._spec.examples) this._spec.examples = {};
this._spec.examples[name] = example;
return this;
}
/**
* Define a component request body.
*
* @param name - The name of the request body
* @param requestBody - Specification of the request body
*
*/
withRequestBody(
name: string,
requestBody: RequestBodyObject | ReferenceObject,
): this {
if (!this._spec.requestBodies) this._spec.requestBodies = {};
this._spec.requestBodies[name] = requestBody;
return this;
}
/**
* Define a component header.
*
* @param name - The name of the header
* @param header - Specification of the header
*
*/
withHeader(name: string, header: HeaderObject | ReferenceObject): this {
if (!this._spec.headers) this._spec.headers = {};
this._spec.headers[name] = header;
return this;
}
/**
* Define a component security scheme.
*
* @param name - The name of the security scheme
* @param securityScheme - Specification of the security scheme
*
*/
withSecurityScheme(
name: string,
securityScheme: SecuritySchemeObject | ReferenceObject,
): this {
if (!this._spec.securitySchemes) this._spec.securitySchemes = {};
this._spec.securitySchemes[name] = securityScheme;
return this;
}
/**
* Define a component link.
*
* @param name - The name of the link
* @param link - Specification of the link
*
*/
withLink(name: string, link: LinkObject | ReferenceObject): this {
if (!this._spec.links) this._spec.links = {};
this._spec.links[name] = link;
return this;
}
/**
* Define a component callback.
*
* @param name - The name of the callback
* @param callback - Specification of the callback
*
*/
withCallback(name: string, callback: CallbackObject | ReferenceObject): this {
if (!this._spec.callbacks) this._spec.callbacks = {};
this._spec.callbacks[name] = callback;
return this;
}
}