UNPKG

openapi-alchemist

Version:

Transform OpenAPI 3 to Swagger 2 with alchemical precision

558 lines (557 loc) 22.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.OpenApi3ToSwagger2Converter = void 0; const fs = __importStar(require("node:fs")); const path = __importStar(require("node:path")); const url = __importStar(require("node:url")); const YAML = __importStar(require("js-yaml")); function deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof Array) { return obj.map(item => deepClone(item)); } if (typeof obj === 'object') { const cloned = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { cloned[key] = deepClone(obj[key]); } } return cloned; } return obj; } 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', }; class OpenApi3ToSwagger2Converter { spec; directory; constructor(data) { this.spec = JSON.parse(JSON.stringify(data.spec)); if (data.source && !data.source.startsWith('http')) { this.directory = path.dirname(data.source); } } convert() { 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; this.fixRefs(this.spec); } return this.spec; } resolveReference(base, obj, shouldClone) { if (!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[k]; }); return shouldClone ? deepClone(cur) : cur; } else if (ref.startsWith('http') || !this.directory) { throw new Error('Remote $ref URLs are not currently supported for openapi_3'); } else { const res = ref.split('#/', 2); const content = fs.readFileSync(path.join(this.directory, res[0]), 'utf8'); let external = null; try { external = JSON.parse(content); } catch { try { external = YAML.load(content); } catch { throw new Error('Could not parse path of $ref ' + res[0] + ' as JSON or YAML'); } } if (res.length > 1) { const keys = res[1] .split('/') .map((k) => k.replace(/~1/g, '/').replace(/~0/g, '~')); keys.forEach((k) => { external = external[k]; }); } return external; } } fixRef(ref) { return ref .replace('#/components/schemas/', '#/definitions/') .replace('#/components/', '#/x-components/'); } fixRefs(obj) { if (Array.isArray(obj)) { obj.forEach((item) => this.fixRefs(item)); } else if (typeof obj === 'object') { for (const key in obj) { if (key === '$ref') { obj.$ref = this.fixRef(obj.$ref); } else { this.fixRefs(obj[key]); } } } } convertInfos() { const server = this.spec.servers && this.spec.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 = RegExp('{' + variable + '}', 'g'); serverUrl = serverUrl.replace(re, variableObject['default']); } } const urlObj = url.parse(serverUrl); if (urlObj.host == null) { delete this.spec.host; } else { this.spec.host = urlObj.host; } if (urlObj.protocol == null) { delete this.spec.schemes; } else { this.spec.schemes = [urlObj.protocol?.substring(0, urlObj.protocol.length - 1)]; } this.spec.basePath = urlObj.pathname; } delete this.spec.servers; delete this.spec.openapi; } convertOperations() { for (const path in this.spec.paths) { const pathObject = (this.spec.paths[path] = this.resolveReference(this.spec, this.spec.paths[path], true)); this.convertParameters(pathObject); for (const method in pathObject) { if (HTTP_METHODS.indexOf(method) >= 0) { const operation = (pathObject[method] = this.resolveReference(this.spec, pathObject[method], true)); this.convertOperationParameters(operation); this.convertResponses(operation); } } } } convertOperationParameters(operation) { let content, param, contentKey, mediaRanges, mediaTypes; operation.parameters = operation.parameters || []; if (operation.requestBody) { param = this.resolveReference(this.spec, operation.requestBody, true); if (operation.requestBody.content) { const type = this.getSupportedMimeTypes(operation.requestBody.content)[0]; const structuredObj = { content: {} }; const data = operation.requestBody.content[type]; if (data && data.schema && data.schema.$ref && !data.schema.$ref.startsWith('#')) { param = this.resolveReference(this.spec, data.schema, true); structuredObj['content'][`${type}`] = { schema: param }; param = structuredObj; } } param.name = 'body'; content = param.content; if (content && Object.keys(content).length) { mediaRanges = Object.keys(content).filter((mediaRange) => mediaRange.indexOf('/') > 0); mediaTypes = mediaRanges.filter((range) => range.indexOf('*') < 0); contentKey = this.getSupportedMimeTypes(content)[0]; delete param.content; if (contentKey === SUPPORTED_MIME_TYPES.APPLICATION_X_WWW_URLENCODED || contentKey === SUPPORTED_MIME_TYPES.MULTIPART_FORM_DATA) { operation.consumes = mediaTypes; param.in = 'formData'; param.schema = content[contentKey].schema; param.schema = this.resolveReference(this.spec, param.schema, true); if (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 = { name, in: 'formData', schema, }; if (required.indexOf(name) >= 0) { formDataParam.required = true; } operation.parameters.push(formDataParam); } } } else { operation.parameters.push(param); } } else if (contentKey) { operation.consumes = mediaTypes; param.in = 'body'; param.schema = content[contentKey].schema; operation.parameters.push(param); } else if (mediaRanges) { operation.consumes = mediaTypes || ['application/octet-stream']; param.in = 'body'; param.name = param.name || 'file'; delete param.type; param.schema = content[mediaRanges[0]].schema || { type: 'string', format: 'binary', }; operation.parameters.push(param); } if (param.schema) { this.convertSchema(param.schema, 'request'); } } delete operation.requestBody; } this.convertParameters(operation); } convertParameters(obj) { if (obj.parameters === undefined) { return; } obj.parameters = obj.parameters || []; (obj.parameters || []).forEach((param, i) => { param = obj.parameters[i] = this.resolveReference(this.spec, param, false); 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, false); 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 || (param.in === 'query' || param.in === 'cookie' ? 'form' : 'simple'); if (style === 'matrix') { param.collectionFormat = param.explode ? undefined : 'csv'; } else if (style === 'label') { param.collectionFormat = undefined; } else if (style === 'simple') { param.collectionFormat = 'csv'; } else if (style === 'spaceDelimited') { param.collectionFormat = 'ssv'; } else if (style === 'pipeDelimited') { param.collectionFormat = 'pipes'; } else if (style === 'deepOpbject') { param.collectionFormat = 'multi'; } else if (style === 'form') { param.collectionFormat = param.explode === false ? 'csv' : 'multi'; } } delete param.style; delete param.explode; }); } copySchemaProperties(obj, props) { const schema = this.resolveReference(this.spec, obj.schema, true); if (!schema) return; props.forEach((prop) => { const value = schema[prop]; switch (prop) { case 'additionalProperties': if (typeof value === 'boolean') return; } if (value !== undefined) { obj[prop] = value; } }); } copySchemaXProperties(obj) { const schema = this.resolveReference(this.spec, obj.schema, true); if (!schema) return; for (const propName in schema) { if (Object.prototype.hasOwnProperty.call(schema, propName) && !Object.prototype.hasOwnProperty.call(obj, propName) && propName.startsWith('x-')) { obj[propName] = schema[propName]; } } } convertResponses(operation) { for (const code in operation.responses) { const response = (operation.responses[code] = this.resolveReference(this.spec, operation.responses[code], true)); if (response.content) { let anySchema = null, jsonSchema = null; for (const mediaRange in response.content) { const mediaType = mediaRange.indexOf('*') < 0 ? mediaRange : 'application/octet-stream'; if (!operation.produces) { operation.produces = [mediaType]; } else if (operation.produces.indexOf(mediaType) < 0) { operation.produces.push(mediaType); } const content = response.content[mediaRange]; anySchema = anySchema || content.schema; if (!jsonSchema && this.isJsonMimeType(mediaType)) { jsonSchema = content.schema; } if (content.example) { response.examples = response.examples || {}; response.examples[mediaType] = content.example; } } if (anySchema) { response.schema = jsonSchema || anySchema; const resolved = this.resolveReference(this.spec, response.schema, true); if (resolved && response.schema.$ref && !response.schema.$ref.startsWith('#')) { response.schema = resolved; } this.convertSchema(response.schema, 'response'); } } const headers = response.headers; if (headers) { for (const header in headers) { const resolved = this.resolveReference(this.spec, headers[header], true); if (resolved.schema) { resolved.type = resolved.schema.type; resolved.format = resolved.schema.format; delete resolved.schema; } headers[header] = resolved; } } delete response.content; } } convertSchema(def, operationDirection) { if (def.oneOf) { delete def.oneOf; if (def.discriminator) { delete def.discriminator; } } if (def.anyOf) { delete def.anyOf; if (def.discriminator) { delete def.discriminator; } } if (def.allOf) { for (const i in def.allOf) { this.convertSchema(def.allOf[i], 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 === true && operationDirection === 'response') { delete def.properties[propName]; } else { this.convertSchema(def.properties[propName], operationDirection); delete def.properties[propName].writeOnly; } } } break; case 'array': if (def.items) { this.convertSchema(def.items, operationDirection); } } if (def.nullable) { def['x-nullable'] = true; delete def.nullable; } if (def['deprecated'] !== undefined) { if (def['x-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], 'response'); } 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)) { try { schema = this.resolveReference(this.spec, { $ref: `#/components/schemas/${schemaNameOrRef}` }, false); } catch (err) { console.debug(`Error resolving ${schemaNameOrRef} for ${payload} as schema name in discriminator.mapping: ${err}`); } } if (!schema) { try { schema = this.resolveReference(this.spec, { $ref: schemaNameOrRef }, false); } catch (err) { console.debug(`Error resolving ${schemaNameOrRef} for ${payload} in discriminator.mapping: ${err}`); } } 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]; if (flowName === 'clientCredentials') { security.flow = 'application'; } else if (flowName === 'authorizationCode') { security.flow = 'accessCode'; } else { security.flow = flowName; } security.authorizationUrl = flow.authorizationUrl; security.tokenUrl = flow.tokenUrl; security.scopes = flow.scopes; delete security.flows; } } delete this.spec.components.securitySchemes; } isJsonMimeType(type) { return APPLICATION_JSON_REGEX.test(type); } getSupportedMimeTypes(content) { const MIME_VALUES = Object.keys(SUPPORTED_MIME_TYPES).map(key => { return SUPPORTED_MIME_TYPES[key]; }); return Object.keys(content).filter((key) => { return MIME_VALUES.indexOf(key) > -1 || this.isJsonMimeType(key); }); } } exports.OpenApi3ToSwagger2Converter = OpenApi3ToSwagger2Converter;