UNPKG

serverless-openapi-documentation

Version:

Serverless 1.0 plugin to generate OpenAPI V3 documentation from serverless configuration

288 lines 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const jst_1 = require("@jdw/jst"); // tslint:disable-next-line no-submodule-imports const validate_1 = require("swagger2openapi/validate"); const uuid = require("uuid"); const utils_1 = require("./utils"); class DefinitionGenerator { /** * Constructor * @param serviceDescriptor IServiceDescription */ constructor(config) { // The OpenAPI version we currently validate against this.version = '3.0.0'; // Base configuration object this.definition = { openapi: this.version, components: {}, }; this.config = utils_1.clone(config); } parse() { const { title = '', description = '', version = uuid.v4(), models, } = this.config; utils_1.merge(this.definition, { openapi: this.version, info: { title, description, version }, paths: {}, components: { schemas: {}, securitySchemes: {}, }, }); if (utils_1.isIterable(models)) { for (const model of models) { if (!model.schema) { continue; } this.definition.components.schemas[model.name] = this.cleanSchema(jst_1.dereference(model.schema)); } } return this; } validate() { const payload = {}; try { validate_1.validateSync(this.definition, payload); } catch (error) { payload.error = error.message; } return payload; } /** * Add Paths to OpenAPI Configuration from Serverless function documentation * @param config Add */ readFunctions(config) { // loop through function configurations for (const funcConfig of config) { // loop through http events for (const httpEvent of this.getHttpEvents(funcConfig.events)) { const httpEventConfig = httpEvent.http; if (httpEventConfig.documentation) { // Build OpenAPI path configuration structure for each method const pathConfig = { [`/${httpEventConfig.path}`]: { [httpEventConfig.method.toLowerCase()]: this.getOperationFromConfig(funcConfig._functionName, httpEventConfig.documentation), }, }; // merge path configuration into main configuration utils_1.merge(this.definition.paths, pathConfig); } } } } /** * Cleans schema objects to make them OpenAPI compatible * @param schema JSON Schema Object */ cleanSchema(schema) { // Clone the schema for manipulation const cleanedSchema = utils_1.clone(schema); // Strip $schema from schemas if (cleanedSchema.$schema) { delete cleanedSchema.$schema; } // Return the cleaned schema return cleanedSchema; } /** * Generate Operation objects from the Serverless Config. * * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/3.0.0.md#operationObject * @param funcName * @param documentationConfig */ getOperationFromConfig(funcName, documentationConfig) { const operationObj = { operationId: funcName, }; if (documentationConfig.summary) { operationObj.summary = documentationConfig.summary; } if (documentationConfig.description) { operationObj.description = documentationConfig.description; } if (documentationConfig.tags) { operationObj.tags = documentationConfig.tags; } if (documentationConfig.deprecated) { operationObj.deprecated = true; } if (operationObj.requestBody) { operationObj.requestBody = this.getRequestBodiesFromConfig(documentationConfig); } if (operationObj.parameters) { operationObj.parameters = this.getParametersFromConfig(documentationConfig); } operationObj.responses = this.getResponsesFromConfig(documentationConfig); return operationObj; } /** * Derives Path, Query and Request header parameters from Serverless documentation * @param documentationConfig */ getParametersFromConfig(documentationConfig) { const parameters = []; // Build up parameters from configuration for each parameter type for (const type of ['path', 'query', 'header', 'cookie']) { let paramBlock; if (type === 'path' && documentationConfig.pathParams) { paramBlock = documentationConfig.pathParams; } else if (type === 'query' && documentationConfig.queryParams) { paramBlock = documentationConfig.queryParams; } else if (type === 'header' && documentationConfig.requestHeaders) { paramBlock = documentationConfig.requestHeaders; } else if (type === 'cookie' && documentationConfig.cookieParams) { paramBlock = documentationConfig.cookieParams; } else { continue; } // Loop through each parameter in a parameter block and add parameters to array for (const parameter of paramBlock) { const parameterConfig = { name: parameter.name, in: type, description: parameter.description || '', required: parameter.required || false, }; // if type is path, then required must be true (@see OpenAPI 3.0-RC1) if (type === 'path') { parameterConfig.required = true; } else if (type === 'query') { parameterConfig.allowEmptyValue = parameter.allowEmptyValue || false; // OpenAPI default is false if ('allowReserved' in parameter) { parameterConfig.allowReserved = parameter.allowReserved || false; } } if ('deprecated' in parameter) { parameterConfig.deprecated = parameter.deprecated; } if ('style' in parameter) { parameterConfig.style = parameter.style; parameterConfig.explode = parameter.explode ? parameter.explode : parameter.style === 'form'; } if (parameter.schema) { parameterConfig.schema = this.cleanSchema(parameter.schema); } if (parameter.example) { parameterConfig.example = parameter.example; } else if (parameter.examples && Array.isArray(parameter.examples)) { parameterConfig.examples = parameter.examples; } if (parameter.content) { parameterConfig.content = parameter.content; } parameters.push(parameterConfig); } } return parameters; } /** * Derives request body schemas from event documentation configuration * @param documentationConfig */ getRequestBodiesFromConfig(documentationConfig) { const requestBodies = {}; if (!documentationConfig.requestModels) { throw new Error(`Required requestModels in: ${JSON.stringify(documentationConfig, null, 2)}`); } // Does this event have a request model? if (documentationConfig.requestModels) { // For each request model type (Sorted by "Content-Type") for (const requestModelType of Object.keys(documentationConfig.requestModels)) { // get schema reference information const requestModel = this.config.models.filter((model) => model.name === documentationConfig.requestModels[requestModelType]).pop(); if (requestModel) { const reqModelConfig = { schema: { $ref: `#/components/schemas/${documentationConfig.requestModels[requestModelType]}`, }, }; this.attachExamples(requestModel, reqModelConfig); const reqBodyConfig = { content: { [requestModelType]: reqModelConfig, }, }; if (documentationConfig.requestBody && 'description' in documentationConfig.requestBody) { reqBodyConfig.description = documentationConfig.requestBody.description; } utils_1.merge(requestBodies, reqBodyConfig); } } } return requestBodies; } attachExamples(target, config) { if (target.examples && Array.isArray(target.examples)) { utils_1.merge(config, { examples: utils_1.clone(target.examples) }); } else if (target.example) { utils_1.merge(config, { example: utils_1.clone(target.example) }); } } /** * Gets response bodies from documentation config * @param documentationConfig */ getResponsesFromConfig(documentationConfig) { const responses = {}; if (documentationConfig.methodResponses) { for (const response of documentationConfig.methodResponses) { const methodResponseConfig = { description: ((response.responseBody && 'description' in response.responseBody) ? response.responseBody.description : `Status ${response.statusCode} Response`), content: this.getResponseContent(response.responseModels), }; if (response.responseHeaders) { methodResponseConfig.headers = {}; for (const header of response.responseHeaders) { methodResponseConfig.headers[header.name] = { description: header.description || `${header.name} header`, }; if (header.schema) { methodResponseConfig.headers[header.name].schema = this.cleanSchema(header.schema); } } } utils_1.merge(responses, { [response.statusCode]: methodResponseConfig, }); } } return responses; } getResponseContent(response) { const content = {}; for (const responseKey of Object.keys(response)) { const responseModel = this.config.models.find((model) => model.name === response[responseKey]); if (responseModel) { const resModelConfig = { schema: { $ref: `#/components/schemas/${response[responseKey]}`, }, }; this.attachExamples(responseModel, resModelConfig); utils_1.merge(content, { [responseKey]: resModelConfig }); } } return content; } getHttpEvents(funcConfig) { return funcConfig.filter((event) => event.http ? true : false); } } exports.DefinitionGenerator = DefinitionGenerator; //# sourceMappingURL=DefinitionGenerator.js.map