swagger2openapi
Version:
Convert Swagger 2.0 definitions to OpenApi 3.0 and validate
1,156 lines (1,064 loc) • 68.6 kB
JavaScript
// @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