UNPKG

@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
// 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; } }