@apvee/azure-functions-openapi
Version:
An extension for Azure Functions V4 that provides support for exporting OpenAPI spec files from annotated Azure Functions.
396 lines (395 loc) • 16.5 kB
JavaScript
/**
* This code has been modified from the original version.
* Original file: "openapi3_to_swagger2.js"
* Repository: https://github.com/LucyBot-Inc/api-spec-converter
* License: MIT
*/
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_camelcase_1 = __importDefault(require("lodash.camelcase"));
const lodash_clonedeep_1 = __importDefault(require("lodash.clonedeep"));
const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
const SCHEMA_PROPERTIES = ['format', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'minLength', 'maxLength', 'multipleOf', 'minItems', 'maxItems', 'uniqueItems', 'minProperties', 'maxProperties', 'additionalProperties', 'pattern', 'enum', 'default'];
const ARRAY_PROPERTIES = ['type', 'items'];
const APPLICATION_JSON_REGEX = /^(application\/json|[^;\/ \t]+\/[^;\/ \t]+[+]json)[ \t]*(;.*)?$/i;
const SUPPORTED_MIME_TYPES = {
APPLICATION_X_WWW_URLENCODED: 'application/x-www-form-urlencoded',
MULTIPART_FORM_DATA: 'multipart/form-data'
};
function capitalizeFirstLetter(str) {
if (!str)
return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Trasforma OpenAPI 3.0 in Swagger 2.0
*/
class Converter {
constructor(openApiSpec) {
this.spec = (0, lodash_clonedeep_1.default)(openApiSpec);
}
convert() {
if (typeof this.spec === 'object' && this.spec !== null) {
this.spec.swagger = '2.0';
this.convertInfos();
this.convertOperations();
if (this.spec.components) {
this.convertSchemas();
this.convertSecurityDefinitions();
this.spec['x-components'] = this.spec.components;
delete this.spec.components;
fixRefs(this.spec);
}
}
return this.spec;
}
resolveReference(base, obj) {
if (!obj || !obj.$ref)
return obj;
const ref = obj.$ref;
if (ref.startsWith('#')) {
const keys = ref.split('/').map(k => k.replace(/~1/g, '/').replace(/~0/g, '~'));
keys.shift();
let cur = base;
keys.forEach(k => { cur = cur ? cur[k] : undefined; });
return cur ? (0, lodash_clonedeep_1.default)(cur) : undefined;
}
else {
throw new Error("Remote $ref URLs are not supported in this implementation.");
}
}
convertInfos() {
const servers = this.spec.servers;
const server = servers && servers[0];
if (server) {
let serverUrl = server.url;
const variables = server.variables || {};
for (const variable in variables) {
const variableObject = variables[variable] || {};
if (variableObject.default) {
const re = new RegExp(`{${variable}}`, 'g');
serverUrl = serverUrl.replace(re, variableObject.default);
}
}
const url = new URL(serverUrl);
this.spec.host = url.host || undefined;
this.spec.schemes = url.protocol ? [url.protocol.replace(':', '')] : undefined;
this.spec.basePath = url.pathname !== '/' ? url.pathname : undefined;
}
delete this.spec.servers;
delete this.spec.openapi;
}
convertOperations() {
if (typeof this.spec.paths !== 'object')
return;
for (const path in this.spec.paths) {
let pathObject = this.spec.paths[path] = this.resolveReference(this.spec, this.spec.paths[path]) || {};
this.convertParameters(pathObject);
for (const method in pathObject) {
if (HTTP_METHODS.includes(method)) {
const operation = pathObject[method] = this.resolveReference(this.spec, pathObject[method]) || {};
operation.operationId = operation.operationId || capitalizeFirstLetter((0, lodash_camelcase_1.default)(`${method}${operation.summary || path}`));
this.convertOperationParameters(operation);
this.convertResponses(operation);
}
}
}
}
convertOperationParameters(operation) {
operation.parameters = operation.parameters || [];
if (operation.requestBody) {
let param = this.resolveReference(this.spec, operation.requestBody) || {};
if (operation.requestBody.content) {
const contentType = getSupportedMimeTypes(operation.requestBody.content)[0];
if (contentType) {
param.name = 'body';
param.in = contentType === SUPPORTED_MIME_TYPES.APPLICATION_X_WWW_URLENCODED ||
contentType === SUPPORTED_MIME_TYPES.MULTIPART_FORM_DATA ? 'formData' : 'body';
param.schema = operation.requestBody.content[contentType].schema;
if (param.in === 'formData' && param.schema.type === 'object' && param.schema.properties) {
const required = param.schema.required || [];
for (const name in param.schema.properties) {
const schema = param.schema.properties[name];
if (!schema.readOnly) {
const formDataParam = Object.assign({ name, in: 'formData', required: required.includes(name) }, schema);
operation.parameters.push(formDataParam);
}
}
}
else {
operation.parameters.push(param);
}
operation.consumes = [contentType];
this.convertSchema(param.schema, 'request');
}
}
delete operation.requestBody;
}
this.convertParameters(operation);
}
convertParameters(obj) {
if (!Array.isArray(obj.parameters))
return;
obj.parameters.forEach((param, i) => {
param = obj.parameters[i] = this.resolveReference(this.spec, param) || {};
if (param.in !== 'body') {
this.copySchemaProperties(param, SCHEMA_PROPERTIES);
this.copySchemaProperties(param, ARRAY_PROPERTIES);
this.copySchemaXProperties(param);
if (!param.description) {
const schema = this.resolveReference(this.spec, param.schema);
if (schema && schema.description) {
param.description = schema.description;
}
}
delete param.schema;
delete param.allowReserved;
if (param.example !== undefined) {
param['x-example'] = param.example;
delete param.example;
}
}
if (param.type === 'array') {
const style = param.style || (['query', 'cookie'].includes(param.in) ? 'form' : 'simple');
param.collectionFormat = this.getCollectionFormat(style, param.explode);
}
delete param.style;
delete param.explode;
});
}
copySchemaProperties(obj, props) {
const schema = this.resolveReference(this.spec, obj.schema);
if (!schema)
return;
props.forEach(prop => {
if (schema[prop] !== undefined) {
obj[prop] = schema[prop];
}
});
}
copySchemaXProperties(obj) {
const schema = this.resolveReference(this.spec, obj.schema);
if (!schema)
return;
for (const propName in schema) {
if (propName.startsWith('x-') && !obj.hasOwnProperty(propName)) {
obj[propName] = schema[propName];
}
}
}
convertResponses(operation) {
if (typeof operation.responses !== 'object')
return;
for (const code in operation.responses) {
const response = operation.responses[code] = this.resolveReference(this.spec, operation.responses[code]) || {};
if (response.content) {
let anySchema = null;
let jsonSchema = null;
for (const mediaRange in response.content) {
const mediaType = mediaRange.includes('*') ? 'application/octet-stream' : mediaRange;
operation.produces = operation.produces || [];
if (!operation.produces.includes(mediaType)) {
operation.produces.push(mediaType);
}
const content = response.content[mediaRange];
anySchema = anySchema || content.schema;
if (!jsonSchema && isJsonMimeType(mediaType)) {
jsonSchema = content.schema;
}
if (content.example) {
response.examples = response.examples || {};
response.examples[mediaType] = content.example;
}
}
if (anySchema) {
response.schema = jsonSchema || anySchema;
const resolvedSchema = this.resolveReference(this.spec, response.schema);
if (resolvedSchema && response.schema.$ref && !response.schema.$ref.startsWith('#')) {
response.schema = resolvedSchema;
}
this.convertSchema(response.schema, 'response');
}
}
const headers = response.headers;
if (headers) {
for (const header in headers) {
const resolvedHeader = this.resolveReference(this.spec, headers[header]) || {};
if (resolvedHeader.schema) {
resolvedHeader.type = resolvedHeader.schema.type;
resolvedHeader.format = resolvedHeader.schema.format;
delete resolvedHeader.schema;
}
headers[header] = resolvedHeader;
}
}
delete response.content;
}
}
convertSchema(def, operationDirection) {
if (def.oneOf) {
delete def.oneOf;
delete def.discriminator;
}
if (def.anyOf) {
delete def.anyOf;
delete def.discriminator;
}
if (def.allOf) {
def.allOf.forEach((subDef) => this.convertSchema(subDef, operationDirection));
}
if (def.discriminator) {
if (def.discriminator.mapping) {
this.convertDiscriminatorMapping(def.discriminator.mapping);
}
def.discriminator = def.discriminator.propertyName;
}
switch (def.type) {
case 'object':
if (def.properties) {
for (const propName in def.properties) {
if (def.properties[propName].writeOnly && operationDirection === 'response') {
delete def.properties[propName];
}
else {
this.convertSchema(def.properties[propName], operationDirection);
delete def.properties[propName].writeOnly;
}
}
}
// fall through
case 'array':
if (def.items) {
this.convertSchema(def.items, operationDirection);
}
break;
}
if (def.nullable) {
def['x-nullable'] = true;
delete def.nullable;
}
if (def.deprecated !== undefined) {
def['x-deprecated'] = def.deprecated;
delete def.deprecated;
}
}
convertSchemas() {
this.spec.definitions = this.spec.components.schemas;
for (const defName in this.spec.definitions) {
this.convertSchema(this.spec.definitions[defName]);
}
delete this.spec.components.schemas;
}
convertDiscriminatorMapping(mapping) {
for (const payload in mapping) {
const schemaNameOrRef = mapping[payload];
if (typeof schemaNameOrRef !== 'string') {
console.warn(`Ignoring ${schemaNameOrRef} for ${payload} in discriminator.mapping.`);
continue;
}
let schema;
if (/^[a-zA-Z0-9._-]+$/.test(schemaNameOrRef)) {
schema = this.resolveReference(this.spec, { $ref: `#/components/schemas/${schemaNameOrRef}` });
}
if (!schema) {
schema = this.resolveReference(this.spec, { $ref: schemaNameOrRef });
}
if (schema) {
schema['x-discriminator-value'] = payload;
schema['x-ms-discriminator-value'] = payload;
}
else {
console.warn(`Unable to resolve ${schemaNameOrRef} for ${payload} in discriminator.mapping.`);
}
}
}
convertSecurityDefinitions() {
this.spec.securityDefinitions = this.spec.components.securitySchemes;
for (const secKey in this.spec.securityDefinitions) {
const security = this.spec.securityDefinitions[secKey];
if (security.type === 'http' && security.scheme === 'basic') {
security.type = 'basic';
delete security.scheme;
}
else if (security.type === 'http' && security.scheme === 'bearer') {
security.type = 'apiKey';
security.name = 'Authorization';
security.in = 'header';
delete security.scheme;
delete security.bearerFormat;
}
else if (security.type === 'oauth2') {
const flowName = Object.keys(security.flows)[0];
const flow = security.flows[flowName];
security.flow = this.getOAuth2FlowName(flowName);
security.authorizationUrl = flow.authorizationUrl;
security.tokenUrl = flow.tokenUrl;
security.scopes = flow.scopes;
delete security.flows;
}
}
delete this.spec.components.securitySchemes;
}
getOAuth2FlowName(flowName) {
switch (flowName) {
case 'clientCredentials':
return 'application';
case 'authorizationCode':
return 'accessCode';
default:
return flowName;
}
}
getCollectionFormat(style, explode) {
switch (style) {
case 'matrix':
return explode ? undefined : 'csv';
case 'label':
return undefined;
case 'simple':
return 'csv';
case 'spaceDelimited':
return 'ssv';
case 'pipeDelimited':
return 'pipes';
case 'deepObject':
return 'multi';
case 'form':
return explode === false ? 'csv' : 'multi';
default:
return undefined;
}
}
}
exports.default = Converter;
function fixRef(ref) {
return ref
.replace('#/components/schemas/', '#/definitions/')
.replace('#/components/', '#/x-components/');
}
function fixRefs(obj) {
if (Array.isArray(obj)) {
obj.forEach(fixRefs);
}
else if (typeof obj === 'object' && obj !== null) {
for (const key in obj) {
if (key === '$ref') {
obj.$ref = fixRef(obj.$ref);
}
else {
fixRefs(obj[key]);
}
}
}
}
function isJsonMimeType(type) {
return APPLICATION_JSON_REGEX.test(type);
}
function getSupportedMimeTypes(content) {
const MIME_VALUES = Object.values(SUPPORTED_MIME_TYPES);
return Object.keys(content).filter(key => {
return MIME_VALUES.includes(key) || isJsonMimeType(key);
});
}