serverless-openapi
Version:
Serverless plugin to generate OpenAPI V3 documentation from serverless configuration
290 lines • 12.5 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const _ = require("lodash");
// tslint:disable-next-line no-submodule-imports
const validate_1 = require("swagger2openapi/validate");
const uuid = require("uuid");
const parse_1 = require("./parse");
const utils_1 = require("./utils");
class DefinitionGenerator {
/**
* Constructor
*/
constructor(config, root) {
// The OpenAPI version we currently validate against
this.version = "3.0.0";
// Base configuration object
this.definition = {
openapi: this.version,
components: {}
};
this.config = _.cloneDeep(config);
this.root = root;
}
parse() {
return __awaiter(this, void 0, void 0, function* () {
const { title = "", description = "", version = uuid.v4(), models, security, securitySchemes, servers } = this.config;
_.merge(this.definition, {
openapi: this.version,
info: { title, description, version },
paths: {},
components: {
schemas: {}
}
});
if (security) {
this.definition.security = security;
}
if (securitySchemes) {
this.definition.components.securitySchemes = securitySchemes;
}
if (servers) {
this.definition.servers = servers;
}
this.definition.components.schemas = yield parse_1.parseModels(models, this.root);
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 || httpEvent.httpApi; // support httpApi event
if (httpEventConfig.documentation) {
// Remove leading slash from path
const normalizedPath = httpEventConfig.path.replace(/^\/+/g, "");
// Build OpenAPI path configuration structure for each method
const pathConfig = {
[`/${normalizedPath}`]: {
[httpEventConfig.method.toLowerCase()]: this.getOperationFromConfig(funcConfig._functionName, httpEventConfig.documentation)
}
};
// merge path configuration into main configuration
_.merge(this.definition.paths, pathConfig);
}
}
}
}
/**
* 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 (documentationConfig.requestBody) {
operationObj.requestBody = this.getRequestBodiesFromConfig(documentationConfig);
}
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 // Note: all path parameters must be required
};
// 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 = utils_1.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;
}
_.merge(requestBodies, reqBodyConfig);
}
}
}
return requestBodies;
}
attachExamples(target, config) {
if (target.examples && Array.isArray(target.examples)) {
_.merge(config, { examples: _.cloneDeep(target.examples) });
}
else if (target.example) {
_.merge(config, { example: _.cloneDeep(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 = utils_1.cleanSchema(header.schema);
}
}
}
_.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);
_.merge(content, { [responseKey]: resModelConfig });
}
}
return content;
}
getHttpEvents(funcConfig) {
return funcConfig.filter(event => event.http || event.httpApi ? true : false);
}
}
exports.DefinitionGenerator = DefinitionGenerator;
//# sourceMappingURL=DefinitionGenerator.js.map