UNPKG

swagger2openapi

Version:

Convert Swagger 2.0 definitions to OpenApi 3.0 and validate

1,156 lines (1,064 loc) 68.6 kB
// @ts-check 'use strict'; const fs = require('fs'); const url = require('url'); const pathlib = require('path'); const maybe = require('call-me-maybe'); const fetch = require('node-fetch-h2'); const yaml = require('yaml'); const jptr = require('reftools/lib/jptr.js'); const resolveInternal = jptr.jptr; const isRef = require('reftools/lib/isref.js').isRef; const clone = require('reftools/lib/clone.js').clone; const cclone = require('reftools/lib/clone.js').circularClone; const recurse = require('reftools/lib/recurse.js').recurse; const resolver = require('oas-resolver'); const sw = require('oas-schema-walker'); const common = require('oas-kit-common'); const statusCodes = require('./lib/statusCodes.js').statusCodes; const ourVersion = require('./package.json').version; // TODO handle specification-extensions with plugins? const targetVersion = '3.0.0'; let componentNames; // initialised in main class S2OError extends Error { constructor(message) { super(message); this.name = 'S2OError'; } } function throwError(message, options) { let err = new S2OError(message); err.options = options; if (options.promise) { options.promise.reject(err); } else { throw err; } } function throwOrWarn(message, container, options) { if (options.warnOnly) { container[options.warnProperty||'x-s2o-warning'] = message; } else { throwError(message, options); } } function fixUpSubSchema(schema,parent,options) { if (schema.nullable) options.patches++; if (schema.discriminator && typeof schema.discriminator === 'string') { schema.discriminator = { propertyName: schema.discriminator }; } if (schema.items && Array.isArray(schema.items)) { if (schema.items.length === 0) { schema.items = {}; } else if (schema.items.length === 1) { schema.items = schema.items[0]; } else schema.items = { anyOf: schema.items }; } if (schema.type && Array.isArray(schema.type)) { if (options.patch) { options.patches++; if (schema.type.length === 0) { delete schema.type; } else { if (!schema.oneOf) schema.oneOf = []; for (let type of schema.type) { let newSchema = {}; if (type === 'null') { schema.nullable = true; } else { newSchema.type = type; for (let prop of common.arrayProperties) { if (typeof schema.prop !== 'undefined') { newSchema[prop] = schema[prop]; delete schema[prop]; } } } if (newSchema.type) { schema.oneOf.push(newSchema); } } delete schema.type; if (schema.oneOf.length === 0) { delete schema.oneOf; // means was just null => nullable } else if (schema.oneOf.length < 2) { schema.type = schema.oneOf[0].type; if (Object.keys(schema.oneOf[0]).length > 1) { throwOrWarn('Lost properties from oneOf',schema,options); } delete schema.oneOf; } } // do not else this if (schema.type && Array.isArray(schema.type) && schema.type.length === 1) { schema.type = schema.type[0]; } } else { throwError('(Patchable) schema type must not be an array', options); } } if (schema.type && schema.type === 'null') { delete schema.type; schema.nullable = true; } if ((schema.type === 'array') && (!schema.items)) { schema.items = {}; } if (schema.type === 'file') { schema.type = 'string'; schema.format = 'binary'; } if (typeof schema.required === 'boolean') { if (schema.required && schema.name) { if (typeof parent.required === 'undefined') { parent.required = []; } if (Array.isArray(parent.required)) parent.required.push(schema.name); } delete schema.required; } // TODO if we have a nested properties (object inside an object) and the // *parent* type is not set, force it to object // TODO if default is set but type is not set, force type to typeof default if (schema.xml && typeof schema.xml.namespace === 'string') { if (!schema.xml.namespace) delete schema.xml.namespace; } if (typeof schema.allowEmptyValue !== 'undefined') { options.patches++; delete schema.allowEmptyValue; } } function fixUpSubSchemaExtensions(schema,parent) { if (schema["x-required"] && Array.isArray(schema["x-required"])) { if (!schema.required) schema.required = []; schema.required = schema.required.concat(schema["x-required"]); delete schema["x-required"]; } if (schema["x-anyOf"]) { schema.anyOf = schema["x-anyOf"]; delete schema["x-anyOf"]; } if (schema["x-oneOf"]) { schema.oneOf = schema["x-oneOf"]; delete schema["x-oneOf"]; } if (schema["x-not"]) { schema.not = schema["x-not"]; delete schema["x-not"]; } if (typeof schema["x-nullable"] === 'boolean') { schema.nullable = schema["x-nullable"]; delete schema["x-nullable"]; } if ((typeof schema["x-discriminator"] === 'object') && (typeof schema["x-discriminator"].propertyName === 'string')) { schema.discriminator = schema["x-discriminator"]; delete schema["x-discriminator"]; for (let entry in schema.discriminator.mapping) { let schemaOrRef = schema.discriminator.mapping[entry]; if (schemaOrRef.startsWith('#/definitions/')) { schema.discriminator.mapping[entry] = schemaOrRef.replace('#/definitions/','#/components/schemas/'); } } } } function fixUpSchema(schema,options) { sw.walkSchema(schema,{},{},function(schema,parent,state){ fixUpSubSchemaExtensions(schema,parent); fixUpSubSchema(schema,parent,options); }); } function getMiroComponentName(ref) { if (ref.indexOf('#')>=0) { ref = ref.split('#')[1].split('/').pop(); } else { ref = ref.split('/').pop().split('.')[0]; } return encodeURIComponent(common.sanitise(ref)); } function fixupRefs(obj, key, state) { let options = state.payload.options; if (isRef(obj,key)) { if (obj[key].startsWith('#/components/')) { // no-op } else if (obj[key] === '#/consumes') { // people are *so* creative delete obj[key]; state.parent[state.pkey] = clone(options.openapi.consumes); } else if (obj[key] === '#/produces') { // and by creative, I mean devious delete obj[key]; state.parent[state.pkey] = clone(options.openapi.produces); } else if (obj[key].startsWith('#/definitions/')) { //only the first part of a schema component name must be sanitised let keys = obj[key].replace('#/definitions/', '').split('/'); const ref = jptr.jpunescape(keys[0]); let newKey = componentNames.schemas[decodeURIComponent(ref)]; // lookup, resolves a $ref if (newKey) { keys[0] = newKey; } else { throwOrWarn('Could not resolve reference '+obj[key],obj,options); } obj[key] = '#/components/schemas/' + keys.join('/'); } else if (obj[key].startsWith('#/parameters/')) { // for extensions like Apigee's x-templates obj[key] = '#/components/parameters/' + common.sanitise(obj[key].replace('#/parameters/', '')); } else if (obj[key].startsWith('#/responses/')) { // for extensions like Apigee's x-templates obj[key] = '#/components/responses/' + common.sanitise(obj[key].replace('#/responses/', '')); } else if (obj[key].startsWith('#')) { // fixes up direct $refs or those created by resolvers let target = clone(jptr.jptr(options.openapi,obj[key])); if (target === false) throwOrWarn('direct $ref not found '+obj[key],obj,options) else if (options.refmap[obj[key]]) { obj[key] = options.refmap[obj[key]]; } else { // we use a heuristic to determine what kind of thing is being referenced let oldRef = obj[key]; oldRef = oldRef.replace('/properties/headers/',''); oldRef = oldRef.replace('/properties/responses/',''); oldRef = oldRef.replace('/properties/parameters/',''); oldRef = oldRef.replace('/properties/schemas/',''); let type = 'schemas'; let schemaIndex = oldRef.lastIndexOf('/schema'); type = (oldRef.indexOf('/headers/')>schemaIndex) ? 'headers' : ((oldRef.indexOf('/responses/')>schemaIndex) ? 'responses' : ((oldRef.indexOf('/example')>schemaIndex) ? 'examples' : ((oldRef.indexOf('/x-')>schemaIndex) ? 'extensions' : ((oldRef.indexOf('/parameters/')>schemaIndex) ? 'parameters' : 'schemas')))); // non-body/form parameters have not moved in the overall structure (like responses) // but extracting the requestBodies can cause the *number* of parameters to change if (type === 'schemas') { fixUpSchema(target,options); } if ((type !== 'responses') && (type !== 'extensions')) { let prefix = type.substr(0,type.length-1); if ((prefix === 'parameter') && target.name && (target.name === common.sanitise(target.name))) { prefix = encodeURIComponent(target.name); } let suffix = 1; if (obj['x-miro']) { prefix = getMiroComponentName(obj['x-miro']); suffix = ''; } while (jptr.jptr(options.openapi,'#/components/'+type+'/'+prefix+suffix)) { suffix = (suffix === '' ? 2 : ++suffix); } let newRef = '#/components/'+type+'/'+prefix+suffix; let refSuffix = ''; if (type === 'examples') { target = { value: target }; refSuffix = '/value'; } jptr.jptr(options.openapi,newRef,target); options.refmap[obj[key]] = newRef+refSuffix; obj[key] = newRef+refSuffix; } } } delete obj['x-miro']; // do this last - rework cases where $ref object has sibling properties if (Object.keys(obj).length > 1) { const tmpRef = obj[key]; const inSchema = state.path.indexOf('/schema') >= 0; // not perfect, but in the absence of a reasonably-sized and complete OAS 2.0 parser... if (options.refSiblings === 'preserve') { // no-op } else if (inSchema && (options.refSiblings === 'allOf')) { delete obj.$ref; state.parent[state.pkey] = { allOf: [ { $ref: tmpRef }, obj ]}; } else { // remove, or not 'preserve' and not in a schema state.parent[state.pkey] = { $ref: tmpRef }; } } } if ((key === 'x-ms-odata') && (typeof obj[key] === 'string') && (obj[key].startsWith('#/'))) { let keys = obj[key].replace('#/definitions/', '').replace('#/components/schemas/','').split('/'); let newKey = componentNames.schemas[decodeURIComponent(keys[0])]; // lookup, resolves a $ref if (newKey) { keys[0] = newKey; } else { throwOrWarn('Could not resolve reference '+obj[key],obj,options); } obj[key] = '#/components/schemas/' + keys.join('/'); } } /* * This has to happen as a separate pass because multiple $refs may point * through elements of the same path */ function dedupeRefs(openapi, options) { for (let ref in options.refmap) { jptr.jptr(openapi,ref,{ $ref: options.refmap[ref] }); } } function processSecurity(securityObject) { for (let s in securityObject) { for (let k in securityObject[s]) { let sname = common.sanitise(k); if (k !== sname) { securityObject[s][sname] = securityObject[s][k]; delete securityObject[s][k]; } } } } function processSecurityScheme(scheme, options) { if (scheme.type === 'basic') { scheme.type = 'http'; scheme.scheme = 'basic'; } if (scheme.type === 'oauth2') { let flow = {}; let flowName = scheme.flow; if (scheme.flow === 'application') flowName = 'clientCredentials'; if (scheme.flow === 'accessCode') flowName = 'authorizationCode'; if (typeof scheme.authorizationUrl !== 'undefined') flow.authorizationUrl = scheme.authorizationUrl.split('?')[0].trim() || '/'; if (typeof scheme.tokenUrl === 'string') flow.tokenUrl = scheme.tokenUrl.split('?')[0].trim() || '/'; flow.scopes = scheme.scopes || {}; scheme.flows = {}; scheme.flows[flowName] = flow; delete scheme.flow; delete scheme.authorizationUrl; delete scheme.tokenUrl; delete scheme.scopes; if (typeof scheme.name !== 'undefined') { if (options.patch) { options.patches++; delete scheme.name; } else { throwError('(Patchable) oauth2 securitySchemes should not have name property', options); } } } } function keepParameters(value) { return (value && !value["x-s2o-delete"]); } function processHeader(header, options) { if (header.$ref) { header.$ref = header.$ref.replace('#/responses/', '#/components/responses/'); } else { if (header.type && !header.schema) { header.schema = {}; } if (header.type) header.schema.type = header.type; if (header.items && header.items.type !== 'array') { if (header.items.collectionFormat !== header.collectionFormat) { throwOrWarn('Nested collectionFormats are not supported', header, options); } delete header.items.collectionFormat; } if (header.type === 'array') { if (header.collectionFormat === 'ssv') { throwOrWarn('collectionFormat:ssv is no longer supported for headers', header, options); // not lossless } else if (header.collectionFormat === 'pipes') { throwOrWarn('collectionFormat:pipes is no longer supported for headers', header, options); // not lossless } else if (header.collectionFormat === 'multi') { header.explode = true; } else if (header.collectionFormat === 'tsv') { throwOrWarn('collectionFormat:tsv is no longer supported', header, options); // not lossless header["x-collectionFormat"] = 'tsv'; } else { // 'csv' header.style = 'simple'; } delete header.collectionFormat; } else if (header.collectionFormat) { if (options.patch) { options.patches++; delete header.collectionFormat; } else { throwError('(Patchable) collectionFormat is only applicable to header.type array', options); } } delete header.type; for (let prop of common.parameterTypeProperties) { if (typeof header[prop] !== 'undefined') { header.schema[prop] = header[prop]; delete header[prop]; } } for (let prop of common.arrayProperties) { if (typeof header[prop] !== 'undefined') { header.schema[prop] = header[prop]; delete header[prop]; } } } } function fixParamRef(param, options) { if (param.$ref.indexOf('#/parameters/') >= 0) { let refComponents = param.$ref.split('#/parameters/'); param.$ref = refComponents[0] + '#/components/parameters/' + common.sanitise(refComponents[1]); } if (param.$ref.indexOf('#/definitions/') >= 0) { throwOrWarn('Definition used as parameter', param, options); } } function attachRequestBody(op,options) { let newOp = {}; for (let key of Object.keys(op)) { newOp[key] = op[key]; if (key === 'parameters') { newOp.requestBody = {}; if (options.rbname) newOp[options.rbname] = ''; } } newOp.requestBody = {}; // just in case there are no parameters return newOp; } /** * @returns op, as it may have changed */ function processParameter(param, op, path, method, index, openapi, options) { let result = {}; let singularRequestBody = true; let originalType; if (op && op.consumes && (typeof op.consumes === 'string')) { if (options.patch) { options.patches++; op.consumes = [op.consumes]; } else { return throwError('(Patchable) operation.consumes must be an array', options); } } if (!Array.isArray(openapi.consumes)) delete openapi.consumes; let consumes = ((op ? op.consumes : null) || (openapi.consumes || [])).filter(common.uniqueOnly); if (param && param.$ref && (typeof param.$ref === 'string')) { // if we still have a ref here, it must be an internal one fixParamRef(param, options); let ptr = decodeURIComponent(param.$ref.replace('#/components/parameters/', '')); let rbody = false; let target = openapi.components.parameters[ptr]; // resolves a $ref, must have been sanitised already if (((!target) || (target["x-s2o-delete"])) && param.$ref.startsWith('#/')) { // if it's gone, chances are it's a requestBody component now unless spec was broken param["x-s2o-delete"] = true; rbody = true; } // shared formData parameters from swagger or path level could be used in any combination. // we dereference all op.requestBody's then hash them and pull out common ones later if (rbody) { let ref = param.$ref; let newParam = resolveInternal(openapi, param.$ref); if (!newParam && ref.startsWith('#/')) { throwOrWarn('Could not resolve reference ' + ref, param, options); } else { if (newParam) param = newParam; // preserve reference } } } if (param && (param.name || param.in)) { // if it's a real parameter OR we've dereferenced it if (typeof param['x-deprecated'] === 'boolean') { param.deprecated = param['x-deprecated']; delete param['x-deprecated']; } if (typeof param['x-example'] !== 'undefined') { param.example = param['x-example']; delete param['x-example']; } if ((param.in !== 'body') && (!param.type)) { if (options.patch) { options.patches++; param.type = 'string'; } else { throwError('(Patchable) parameter.type is mandatory for non-body parameters', options); } } if (param.type && typeof param.type === 'object' && param.type.$ref) { // $ref anywhere sensibility param.type = resolveInternal(openapi, param.type.$ref); } if (param.type === 'file') { param['x-s2o-originalType'] = param.type; originalType = param.type; } if (param.description && typeof param.description === 'object' && param.description.$ref) { // $ref anywhere sensibility param.description = resolveInternal(openapi, param.description.$ref); } if (param.description === null) delete param.description; let oldCollectionFormat = param.collectionFormat; if ((param.type === 'array') && !oldCollectionFormat) { oldCollectionFormat = 'csv'; } if (oldCollectionFormat) { if (param.type !== 'array') { if (options.patch) { options.patches++; delete param.collectionFormat; } else { throwError('(Patchable) collectionFormat is only applicable to param.type array', options); } } if ((oldCollectionFormat === 'csv') && ((param.in === 'query') || (param.in === 'cookie'))) { param.style = 'form'; param.explode = false; } if ((oldCollectionFormat === 'csv') && ((param.in === 'path') || (param.in === 'header'))) { param.style = 'simple'; } if (oldCollectionFormat === 'ssv') { if (param.in === 'query') { param.style = 'spaceDelimited'; } else { throwOrWarn('collectionFormat:ssv is no longer supported except for in:query parameters', param, options); // not lossless } } if (oldCollectionFormat === 'pipes') { if (param.in === 'query') { param.style = 'pipeDelimited'; } else { throwOrWarn('collectionFormat:pipes is no longer supported except for in:query parameters', param, options); // not lossless } } if (oldCollectionFormat === 'multi') { param.explode = true; } if (oldCollectionFormat === 'tsv') { throwOrWarn('collectionFormat:tsv is no longer supported', param, options); // not lossless param["x-collectionFormat"] = 'tsv'; } delete param.collectionFormat; } if (param.type && (param.type !== 'body') && (param.in !== 'formData')) { if (param.items && param.schema) { throwOrWarn('parameter has array,items and schema', param, options); } else { if (param.schema) options.patches++; // already present if ((!param.schema) || (typeof param.schema !== 'object')) param.schema = {}; param.schema.type = param.type; if (param.items) { param.schema.items = param.items; delete param.items; recurse(param.schema.items, null, function (obj, key, state) { if ((key === 'collectionFormat') && (typeof obj[key] === 'string')) { if (oldCollectionFormat && obj[key] !== oldCollectionFormat) { throwOrWarn('Nested collectionFormats are not supported', param, options); } delete obj[key]; // not lossless } // items in 2.0 was a subset of the JSON-Schema items // object, it gets fixed up below }); } for (let prop of common.parameterTypeProperties) { if (typeof param[prop] !== 'undefined') param.schema[prop] = param[prop]; delete param[prop]; } } } if (param.schema) { fixUpSchema(param.schema,options); } if (param["x-ms-skip-url-encoding"]) { if (param.in === 'query') { // might be in:path, not allowed in OAS3 param.allowReserved = true; delete param["x-ms-skip-url-encoding"]; } } } if (param && param.in === 'formData') { // convert to requestBody component singularRequestBody = false; result.content = {}; let contentType = 'application/x-www-form-urlencoded'; if ((consumes.length) && (consumes.indexOf('multipart/form-data') >= 0)) { contentType = 'multipart/form-data'; } result.content[contentType] = {}; if (param.schema) { result.content[contentType].schema = param.schema; if (param.schema.$ref) { result['x-s2o-name'] = decodeURIComponent(param.schema.$ref.replace('#/components/schemas/', '')); } } else { result.content[contentType].schema = {}; result.content[contentType].schema.type = 'object'; result.content[contentType].schema.properties = {}; result.content[contentType].schema.properties[param.name] = {}; let schema = result.content[contentType].schema; let target = result.content[contentType].schema.properties[param.name]; if (param.description) target.description = param.description; if (param.example) target.example = param.example; if (param.type) target.type = param.type; for (let prop of common.parameterTypeProperties) { if (typeof param[prop] !== 'undefined') target[prop] = param[prop]; } if (param.required === true) { if (!schema.required) schema.required = []; schema.required.push(param.name); result.required = true; } if (typeof param.default !== 'undefined') target.default = param.default; if (target.properties) target.properties = param.properties; if (param.allOf) target.allOf = param.allOf; // new are anyOf, oneOf, not if ((param.type === 'array') && (param.items)) { target.items = param.items; if (target.items.collectionFormat) delete target.items.collectionFormat; } if ((originalType === 'file') || (param['x-s2o-originalType'] === 'file')) { target.type = 'string'; target.format = 'binary'; } // Copy any extensions on the form param to the target schema property. copyExtensions(param, target); } } else if (param && (param.type === 'file')) { // convert to requestBody if (param.required) result.required = param.required; result.content = {}; result.content["application/octet-stream"] = {}; result.content["application/octet-stream"].schema = {}; result.content["application/octet-stream"].schema.type = 'string'; result.content["application/octet-stream"].schema.format = 'binary'; copyExtensions(param, result); } if (param && param.in === 'body') { result.content = {}; if (param.name) result['x-s2o-name'] = (op && op.operationId ? common.sanitiseAll(op.operationId) : '') + ('_' + param.name).toCamelCase(); if (param.description) result.description = param.description; if (param.required) result.required = param.required; // Set the "request body name" extension on the operation if requested. if (op && options.rbname && param.name) { op[options.rbname] = param.name; } if (param.schema && param.schema.$ref) { result['x-s2o-name'] = decodeURIComponent(param.schema.$ref.replace('#/components/schemas/', '')); } else if (param.schema && (param.schema.type === 'array') && param.schema.items && param.schema.items.$ref) { result['x-s2o-name'] = decodeURIComponent(param.schema.items.$ref.replace('#/components/schemas/', '')) + 'Array'; } if (!consumes.length) { consumes.push('application/json'); // TODO verify default } for (let mimetype of consumes) { result.content[mimetype] = {}; result.content[mimetype].schema = clone(param.schema || {}); fixUpSchema(result.content[mimetype].schema,options); } // Copy any extensions from the original parameter to the new requestBody copyExtensions(param, result); } if (Object.keys(result).length > 0) { param["x-s2o-delete"] = true; // work out where to attach the requestBody if (op) { if (op.requestBody && singularRequestBody) { op.requestBody["x-s2o-overloaded"] = true; let opId = op.operationId || index; throwOrWarn('Operation ' + opId + ' has multiple requestBodies', op, options); } else { if (!op.requestBody) { op = path[method] = attachRequestBody(op,options); // make sure we have one } if ((op.requestBody.content && op.requestBody.content["multipart/form-data"]) && (op.requestBody.content["multipart/form-data"].schema) && (op.requestBody.content["multipart/form-data"].schema.properties) && (result.content["multipart/form-data"]) && (result.content["multipart/form-data"].schema) && (result.content["multipart/form-data"].schema.properties)) { op.requestBody.content["multipart/form-data"].schema.properties = Object.assign(op.requestBody.content["multipart/form-data"].schema.properties, result.content["multipart/form-data"].schema.properties); op.requestBody.content["multipart/form-data"].schema.required = (op.requestBody.content["multipart/form-data"].schema.required || []).concat(result.content["multipart/form-data"].schema.required||[]); if (!op.requestBody.content["multipart/form-data"].schema.required.length) { delete op.requestBody.content["multipart/form-data"].schema.required; } } else if ((op.requestBody.content && op.requestBody.content["application/x-www-form-urlencoded"] && op.requestBody.content["application/x-www-form-urlencoded"].schema && op.requestBody.content["application/x-www-form-urlencoded"].schema.properties) && result.content["application/x-www-form-urlencoded"] && result.content["application/x-www-form-urlencoded"].schema && result.content["application/x-www-form-urlencoded"].schema.properties) { op.requestBody.content["application/x-www-form-urlencoded"].schema.properties = Object.assign(op.requestBody.content["application/x-www-form-urlencoded"].schema.properties, result.content["application/x-www-form-urlencoded"].schema.properties); op.requestBody.content["application/x-www-form-urlencoded"].schema.required = (op.requestBody.content["application/x-www-form-urlencoded"].schema.required || []).concat(result.content["application/x-www-form-urlencoded"].schema.required||[]); if (!op.requestBody.content["application/x-www-form-urlencoded"].schema.required.length) { delete op.requestBody.content["application/x-www-form-urlencoded"].schema.required; } } else { op.requestBody = Object.assign(op.requestBody, result); if (!op.requestBody['x-s2o-name']) { if (op.requestBody.schema && op.requestBody.schema.$ref) { op.requestBody['x-s2o-name'] = decodeURIComponent(op.requestBody.schema.$ref.replace('#/components/schemas/', '')).split('/').join(''); } else if (op.operationId) { op.requestBody['x-s2o-name'] = common.sanitiseAll(op.operationId); } } } } } } // tidy up if (param && !param['x-s2o-delete']) { delete param.type; for (let prop of common.parameterTypeProperties) { delete param[prop]; } if ((param.in === 'path') && ((typeof param.required === 'undefined') || (param.required !== true))) { if (options.patch) { options.patches++; param.required = true; } else { throwError('(Patchable) path parameters must be required:true ['+param.name+' in '+index+']', options); } } } return op; } function copyExtensions(src, tgt) { for (let prop in src) { if (prop.startsWith('x-') && !prop.startsWith('x-s2o')) { tgt[prop] = src[prop]; } } } function processResponse(response, name, op, openapi, options) { if (!response) return false; if (response.$ref && (typeof response.$ref === 'string')) { if (response.$ref.indexOf('#/definitions/') >= 0) { //response.$ref = '#/components/schemas/'+common.sanitise(response.$ref.replace('#/definitions/','')); throwOrWarn('definition used as response: ' + response.$ref, response, options); } else { if (response.$ref.startsWith('#/responses/')) { response.$ref = '#/components/responses/' + common.sanitise(decodeURIComponent(response.$ref.replace('#/responses/', ''))); } } } else { if ((typeof response.description === 'undefined') || (response.description === null) || ((response.description === '') && options.patch)) { if (options.patch) { if ((typeof response === 'object') && (!Array.isArray(response))) { options.patches++; response.description = (statusCodes[response] || ''); } } else { throwError('(Patchable) response.description is mandatory', options); } } if (typeof response.schema !== 'undefined') { fixUpSchema(response.schema,options); if (response.schema.$ref && (typeof response.schema.$ref === 'string') && response.schema.$ref.startsWith('#/responses/')) { response.schema.$ref = '#/components/responses/' + common.sanitise(decodeURIComponent(response.schema.$ref.replace('#/responses/', ''))); } if (op && op.produces && (typeof op.produces === 'string')) { if (options.patch) { options.patches++; op.produces = [op.produces]; } else { return throwError('(Patchable) operation.produces must be an array', options); } } if (openapi.produces && !Array.isArray(openapi.produces)) delete openapi.produces; let produces = ((op ? op.produces : null) || (openapi.produces || [])).filter(common.uniqueOnly); if (!produces.length) produces.push('*/*'); // TODO verify default response.content = {}; for (let mimetype of produces) { response.content[mimetype] = {}; response.content[mimetype].schema = clone(response.schema); if (response.examples && response.examples[mimetype]) { let example = {}; example.value = response.examples[mimetype]; response.content[mimetype].examples = {}; response.content[mimetype].examples.response = example; delete response.examples[mimetype]; } if (response.content[mimetype].schema.type === 'file') { response.content[mimetype].schema = { type: 'string', format: 'binary' }; } } delete response.schema; } // examples for content-types not listed in produces for (let mimetype in response.examples) { if (!response.content) response.content = {}; if (!response.content[mimetype]) response.content[mimetype] = {}; response.content[mimetype].examples = {}; response.content[mimetype].examples.response = {}; response.content[mimetype].examples.response.value = response.examples[mimetype]; } delete response.examples; if (response.headers) { for (let h in response.headers) { if (h.toLowerCase() === 'status code') { if (options.patch) { options.patches++; delete response.headers[h]; } else { throwError('(Patchable) "Status Code" is not a valid header', options); } } else { processHeader(response.headers[h], options); } } } } } function processPaths(container, containerName, options, requestBodyCache, openapi) { for (let p in container) { let path = container[p]; // path.$ref is external only if (path && (path['x-trace']) && (typeof path['x-trace'] === 'object')) { path.trace = path['x-trace']; delete path['x-trace']; } if (path && (path['x-summary']) && (typeof path['x-summary'] === 'string')) { path.summary = path['x-summary']; delete path['x-summary']; } if (path && (path['x-description']) && (typeof path['x-description'] === 'string')) { path.description = path['x-description']; delete path['x-description']; } if (path && (path['x-servers']) && (Array.isArray(path['x-servers']))) { path.servers = path['x-servers']; delete path['x-servers']; } for (let method in path) { if ((common.httpMethods.indexOf(method) >= 0) || (method === 'x-amazon-apigateway-any-method')) { let op = path[method]; if (op && op.parameters && Array.isArray(op.parameters)) { if (path.parameters) { for (let param of path.parameters) { if (typeof param.$ref === 'string') { fixParamRef(param, options); param = resolveInternal(openapi, param.$ref); } let match = op.parameters.find(function (e, i, a) { return ((e.name === param.name) && (e.in === param.in)); }); if (!match && ((param.in === 'formData') || (param.in === 'body') || (param.type === 'file'))) { op = processParameter(param, op, path, method, p, openapi, options); if (options.rbname && op[options.rbname] === '') { delete op[options.rbname]; } } } } for (let param of op.parameters) { op = processParameter(param, op, path, method, method + ':' + p, openapi, options); } if (options.rbname && op[options.rbname] === '') { delete op[options.rbname]; } if (!options.debug) { if (op.parameters) op.parameters = op.parameters.filter(keepParameters); } } if (op && op.security) processSecurity(op.security); //don't need to remove requestBody for non-supported ops as they "SHALL be ignored" // responses if (typeof op === 'object') { if (!op.responses) { let defaultResp = {}; defaultResp.description = 'Default response'; op.responses = { default: defaultResp }; } for (let r in op.responses) { let response = op.responses[r]; processResponse(response, r, op, openapi, options); } } if (op && (op['x-servers']) && (Array.isArray(op['x-servers']))) { op.servers = op['x-servers']; delete op['x-servers']; } else if (op && op.schemes && op.schemes.length) { for (let scheme of op.schemes) { if ((!openapi.schemes) || (openapi.schemes.indexOf(scheme) < 0)) { if (!op.servers) { op.servers = []; } if (Array.isArray(openapi.servers)) { for (let server of openapi.servers) { let newServer = clone(server); let serverUrl = url.parse(newServer.url); serverUrl.protocol = scheme; newServer.url = serverUrl.format(); op.servers.push(newServer); } } } } } if (options.debug) { op["x-s2o-consumes"] = op.consumes || []; op["x-s2o-produces"] = op.produces || []; } if (op) { delete op.consumes; delete op.produces; delete op.schemes; if (op["x-ms-examples"]) { for (let e in op["x-ms-examples"]) { let example = op["x-ms-examples"][e]; let se = common.sanitiseAll(e); if (example.parameters) { for (let p in example.parameters) { let value = example.parameters[p]; for (let param of (op.parameters||[]).concat(path.parameters||[])) { if (param.$ref) { param = jptr.jptr(openapi,param.$ref); } if ((param.name === p) && (!param.example)) { if (!param.examples) { param.examples = {}; } param.examples[e] = {value: value}; } } } } if (example.responses) { for (let r in example.responses) { if (example.responses[r].headers) { for (let h in example.responses[r].headers) { let value = example.responses[r].headers[h]; for (let rh in op.responses[r].headers) { if (rh === h) { let header = op.responses[r].headers[rh]; header.example = value; } } } } if (example.responses[r].body) { openapi.components.examples[se] = { value: clone(example.responses[r].body) }; if (op.responses[r] && op.responses[r].content) { for (let ct in op.responses[r].content) { let contentType = op.responses[r].content[ct]; if (!contentType.examples) { contentType.examples = {}; } contentType.examples[e] = { $ref: '#/components/examples/'+se }; } } } } } } delete op["x-ms-examples"]; } if (op.parameters && op.parameters.length === 0) delete op.parameters; if (op.requestBody) { let effectiveOperationId = op.operationId ? common.sanitiseAll(op.operationId) : common.sanitiseAll(method + p).toCamelCase(); let rbName = common.sanitise(op.requestBody['x-s2o-name'] || effectiveOperationId || ''); delete op.requestBody['x-s2o-name']; let rbStr = JSON.stringify(op.requestBody); let rbHash = common.hash(rbStr); if (!requestBodyCache[rbHash]) { let entry = {}; entry.name = rbName; entry.body = op.requestBody; entry.refs = []; requestBodyCache[rbHash] = entry; } let ptr = '#/'+containerName+'/'+encodeURIComponent(jptr.jpescape(p))+'/'+method+'/requestBody'; requestBodyCache[rbHash].refs.push(ptr); } } } } if (path && path.parameters) { for (let p2 in path.parameters) { let param = path.parameters[p2]; processParameter(param, null, path, null, p, openapi, options); // index here is the path string } if (!options.debug && Array.isArray(path.parameters)) { path.parameters = path.parameters.filter(keepParameters); } } } } function main(openapi, options) { let requestBodyCache = {}; componentNames = { schemas: {} }; if (openapi.security) processSecurity(openapi.security); for (let s in openapi.components.securitySchemes) { let sname = common.sanitise(s); if (s !== sname) { if (openapi.components.securitySchemes[sname]) { throwError('Duplicate sanitised securityScheme name ' + sname, options); } openapi.components.securitySchemes[sname] = openapi.components.securitySchemes[s]; delete openapi.components.securitySchemes[s]; } processSecurityScheme(openapi.components.securitySchemes[sname], options); } for (let s in openapi.components.schemas) { let sname = common.sanitiseAll(s); let suffix = ''; if (s !== sname) { while (openapi.components.schemas[sname + suffix]) { // @ts-ignore suffix = (suffix ? ++suffix : 2); } openapi.components.schemas[sname + suffix] = openapi.components.schemas[s]; delete openapi.components.schemas[s]; } componentNames.schemas[s] = sname + suffix; fixUpSchema(openapi.components.schemas[sname+suffix],options) } // fix all $refs to their new locations (and potentially new names) options.refmap = {}; recurse(openapi, { payload: { options: options } }, fixupRefs); dedupeRefs(openapi,options); for (let p in openapi.components.parameters) { let sname = common.sanitise(p); if (p !== sname) { if (openapi.components.parameters[sname]) { throwError('Duplicate sanitised parameter name ' + sname, options); } openapi.components.parameters[sname] = openapi.components.parameters[p]; delete openapi.components.parameters[p]; } let param = openapi.components.parameters[sname]; processParameter(param, null, null, null, sname, openapi, options); } for (let r in openapi.components.responses) { let sname = common.sanitise(r); if (r !== sname) { if (openapi.components.responses[sname]) { throwError('Duplicate sanitised response name ' + sname, options); } openapi.components.responses[sname] = openapi.components.responses[r]; delete openapi.components.responses[r]; } let response = openapi.components.responses[sname]; proc