openapi-to-postmanv2
Version:
Convert a given OpenAPI specification to Postman Collection v2.0
1,387 lines (1,202 loc) • 91.7 kB
JavaScript
const generateAuthForCollectionFromOpenAPI = require('./helpers/collection/generateAuthForCollectionFromOpenAPI');
const utils = require('./utils');
const schemaFaker = require('../assets/json-schema-faker'),
_ = require('lodash'),
mergeAllOf = require('json-schema-merge-allof'),
xmlFaker = require('./xmlSchemaFaker.js'),
URLENCODED = 'application/x-www-form-urlencoded',
APP_JSON = 'application/json',
APP_JS = 'application/javascript',
TEXT_XML = 'text/xml',
APP_XML = 'application/xml',
TEXT_PLAIN = 'text/plain',
TEXT_HTML = 'text/html',
FORM_DATA = 'multipart/form-data',
HEADER_TYPE = {
JSON: 'json',
XML: 'xml',
INVALID: 'invalid'
},
HEADER_TYPE_PREVIEW_LANGUAGE_MAP = {
[HEADER_TYPE.JSON]: 'json',
[HEADER_TYPE.XML]: 'xml'
},
// These headers are to be validated explicitly
// As these are not defined under usual parameters object and need special handling
IMPLICIT_HEADERS = [
'content-type', // 'content-type' is defined based on content/media-type of req/res body,
'accept',
'authorization'
],
SCHEMA_PROPERTIES_TO_EXCLUDE = [
'default',
'enum',
'pattern'
],
// All formats supported by both ajv and json-schema-faker
SUPPORTED_FORMATS = [
'date', 'time', 'date-time',
'uri', 'uri-reference', 'uri-template',
'email',
'hostname',
'ipv4', 'ipv6',
'regex',
'uuid',
'binary',
'json-pointer',
'int64',
'float',
'double'
],
typesMap = {
integer: {
int32: '<integer>',
int64: '<long>'
},
number: {
float: '<float>',
double: '<double>'
},
string: {
byte: '<byte>',
binary: '<binary>',
date: '<date>',
'date-time': '<dateTime>',
password: '<password>'
},
boolean: '<boolean>',
array: '<array>',
object: '<object>'
},
// Maximum size of schema till whch we generate 2 elements per array (50 KB)
SCHEMA_SIZE_OPTIMIZATION_THRESHOLD = 50 * 1024,
PROPERTIES_TO_ASSIGN_ON_CASCADE = ['type', 'nullable', 'properties'],
crypto = require('crypto'),
/**
* @param {*} rootObject - the object from which you're trying to read a property
* @param {*} pathArray - each element in this array a property of the previous object
* @param {*} defValue - what to return if the required path is not found
* @returns {*} - required property value
* @description - this is similar to _.get(rootObject, pathArray.join('.')), but also works for cases where
* there's a . in the property name
*/
_getEscaped = (rootObject, pathArray, defValue) => {
if (!(pathArray instanceof Array)) {
return null;
}
if (rootObject === undefined) {
return defValue;
}
if (_.isEmpty(pathArray)) {
return rootObject;
}
return _getEscaped(rootObject[pathArray.shift()], pathArray, defValue);
},
getXmlVersionContent = (bodyContent) => {
const regExp = new RegExp('([<\\?xml]+[\\s{1,}]+[version="\\d.\\d"]+[\\sencoding="]+.{1,15}"\\?>)');
let xmlBody = bodyContent;
if (_.isFunction(bodyContent.match) && !bodyContent.match(regExp)) {
const versionContent = '<?xml version="1.0" encoding="UTF-8"?>\n';
xmlBody = versionContent + xmlBody;
}
return xmlBody;
};
// See https://github.com/json-schema-faker/json-schema-faker/tree/master/docs#available-options
schemaFaker.option({
requiredOnly: false,
optionalsProbability: 1.0, // always add optional fields
maxLength: 256,
minItems: 1, // for arrays
maxItems: 20, // limit on maximum number of items faked for (type: arrray)
useDefaultValue: true,
ignoreMissingRefs: true,
avoidExampleItemsLength: true, // option to avoid validating type array schema example's minItems and maxItems props.
failOnInvalidFormat: false
});
let QUERYPARAM = 'query',
CONVERSION = 'conversion',
HEADER = 'header',
PATHPARAM = 'path',
SCHEMA_TYPES = {
array: 'array',
boolean: 'boolean',
integer: 'integer',
number: 'number',
object: 'object',
string: 'string'
},
SCHEMA_FORMATS = {
DEFAULT: 'default', // used for non-request-body data and json
XML: 'xml' // used for request-body XMLs
},
REF_STACK_LIMIT = 30,
ERR_TOO_MANY_LEVELS = '<Error: Too many levels of nesting to fake this schema>',
/**
* Changes the {} around scheme and path variables to :variable
* @param {string} url - the url string
* @returns {string} string after replacing /{pet}/ with /:pet/
*/
sanitizeUrl = (url) => {
// URL should always be string so update value if non-string value is found
if (typeof url !== 'string') {
return '';
}
// This simply replaces all instances of {text} with {{text}}
// text cannot have any of these 3 chars: /{}
// {{text}} will not be converted
const replacer = function (match, p1, offset, string) {
if (string[offset - 1] === '{' && string[offset + match.length + 1] !== '}') {
return match;
}
return '{' + p1 + '}';
};
url = _.isString(url) ? url.replace(/(\{[^\/\{\}]+\})/g, replacer) : '';
// converts the following:
// /{{path}}/{{file}}.{{format}}/{{hello}} => /:path/{{file}}.{{format}}/:hello
let pathVariables = url.match(/(\/\{\{[^\/\{\}]+\}\})(?=\/|$)/g);
if (pathVariables) {
pathVariables.forEach((match) => {
const replaceWith = match.replace(/{{/g, ':').replace(/}}/g, '');
url = url.replace(match, replaceWith);
});
}
return url;
},
filterCollectionAndPathVariables = (url, pathVariables) => {
// URL should always be string so update value if non-string value is found
if (typeof url !== 'string') {
return '';
}
// /:path/{{file}}.{{format}}/:hello => only {{file}} and {{format}} will match
let variables = url.match(/(\{\{[^\/\{\}]+\}\})/g),
collectionVariables = [],
collectionVariableMap = {},
filteredPathVariables = [];
_.forEach(variables, (variable) => {
const collVar = variable.replace(/{{/g, '').replace(/}}/g, '');
collectionVariableMap[collVar] = true;
});
// Filter out variables that are to be added as collection variables
_.forEach(pathVariables, (pathVariable) => {
if (collectionVariableMap[pathVariable.key]) {
collectionVariables.push(_.pick(pathVariable, ['key', 'value']));
}
else {
filteredPathVariables.push(pathVariable);
}
});
return {
collectionVariables,
pathVariables: filteredPathVariables
};
},
resolveBaseUrlForPostmanRequest = (operationItem) => {
let serverObj = _.get(operationItem, 'servers.0'),
baseUrl = '{{baseUrl}}',
serverVariables = [],
pathVariables = [],
collectionVariables = [];
if (!serverObj) {
return { collectionVariables, pathVariables, baseUrl };
}
baseUrl = sanitizeUrl(serverObj.url);
_.forOwn(serverObj.variables, (value, key) => {
serverVariables.push({
key,
value: _.get(value, 'default') || ''
});
});
({ collectionVariables, pathVariables } = filterCollectionAndPathVariables(baseUrl, serverVariables));
return { collectionVariables, pathVariables, baseUrl };
},
/**
* Provides ref stack limit for current instance
* @param {*} stackLimit - Defined stackLimit in options
*
* @returns {Number} Returns the stackLimit to be used
*/
getRefStackLimit = (stackLimit) => {
if (typeof stackLimit === 'number' && stackLimit > REF_STACK_LIMIT) {
return stackLimit;
}
return REF_STACK_LIMIT;
},
/**
* Resets cache storing readOnly and writeOnly property map.
*
* @param {Object} context - Global context object
* @returns {void}
*/
resetReadWritePropCache = (context) => {
context.readOnlyPropCache = {};
context.writeOnlyPropCache = {};
},
/**
* Merges provided readOnly writeOnly properties cache with existing cache present in context
*
* @param {Object} context - Global context object
* @param {Object} readOnlyPropCache - readOnly properties cache to be merged
* @param {Object} writeOnlyPropCache - writeOnly properties cache to be merged
* @param {Object} currentPath - Current path (json-pointer) being resolved relative to original schema
* @returns {void}
*/
mergeReadWritePropCache = (context, readOnlyPropCache, writeOnlyPropCache, currentPath = '') => {
_.forOwn(readOnlyPropCache, (value, key) => {
context.readOnlyPropCache[utils.mergeJsonPath(currentPath, key)] = true;
});
_.forOwn(writeOnlyPropCache, (value, key) => {
context.writeOnlyPropCache[utils.mergeJsonPath(currentPath, key)] = true;
});
},
/**
* Resolve a given ref from the schema
* @param {Object} context - Global context object
* @param {Object} $ref - Ref that is to be resolved
* @param {Number} stackDepth - Depth of the current stack for Ref resolution
* @param {Object} seenRef - Seen Reference map
*
* @returns {Object} Returns the object that satisfies the schema
*/
resolveRefFromSchema = (context, $ref, stackDepth = 0, seenRef = {}) => {
const { specComponents } = context,
{ stackLimit } = context.computedOptions;
if (stackDepth >= getRefStackLimit(stackLimit)) {
return { value: ERR_TOO_MANY_LEVELS };
}
stackDepth++;
seenRef[$ref] = true;
if (context.schemaCache[$ref]) {
// Also merge readOnly and writeOnly prop cache from schemaCache to global context cache
mergeReadWritePropCache(context, context.schemaCache[$ref].readOnlyPropCache,
context.schemaCache[$ref].writeOnlyPropCache);
return context.schemaCache[$ref].schema;
}
if (!_.isFunction($ref.split)) {
return { value: `reference ${$ref} not found in the OpenAPI spec` };
}
let splitRef = $ref.split('/'),
resolvedSchema;
// .split should return [#, components, schemas, schemaName]
// So length should atleast be 4
if (splitRef.length < 4) {
// not throwing an error. We didn't find the reference - generate a dummy value
return { value: `reference ${$ref} not found in the OpenAPI spec` };
}
// something like #/components/schemas/PaginationEnvelope/properties/page
// will be resolved - we don't care about anything before the components part
// splitRef.slice(1) will return ['components', 'schemas', 'PaginationEnvelope', 'properties', 'page']
// not using _.get here because that fails if there's a . in the property name (Pagination.Envelope, for example)
splitRef = splitRef.slice(1).map((elem) => {
// https://swagger.io/docs/specification/using-ref#escape
// since / is the default delimiter, slashes are escaped with ~1
return decodeURIComponent(
elem
.replace(/~1/g, '/')
.replace(/~0/g, '~')
);
});
resolvedSchema = _getEscaped(specComponents, splitRef);
if (resolvedSchema === undefined) {
return { value: 'reference ' + $ref + ' not found in the OpenAPI spec' };
}
if (_.get(resolvedSchema, '$ref')) {
if (seenRef[resolvedSchema.$ref]) {
return {
value: '<Circular reference to ' + resolvedSchema.$ref + ' detected>'
};
}
return resolveRefFromSchema(context, resolvedSchema.$ref, stackDepth, _.cloneDeep(seenRef));
}
return resolvedSchema;
},
/**
* Resolve a given ref from an example
* @param {Object} context - Global context object
* @param {Object} $ref - Ref that is to be resolved
* @param {Number} stackDepth - Depth of the current stack for Ref resolution
* @param {Object} seenRef - Seen Reference map
*
* @returns {Object} Returns the object that satisfies the schema
*/
resolveRefForExamples = (context, $ref, stackDepth = 0, seenRef = {}) => {
const { specComponents } = context,
{ stackLimit } = context.computedOptions;
if (stackDepth >= getRefStackLimit(stackLimit)) {
return { value: ERR_TOO_MANY_LEVELS };
}
stackDepth++;
seenRef[$ref] = true;
if (context.schemaCache[$ref]) {
// Also merge readOnly and writeOnly prop cache from schemaCache to global context cache
mergeReadWritePropCache(context, context.schemaCache[$ref].readOnlyPropCache,
context.schemaCache[$ref].writeOnlyPropCache);
return context.schemaCache[$ref].schema;
}
if (!_.isFunction($ref.split)) {
return { value: `reference ${schema.$ref} not found in the OpenAPI spec` };
}
let splitRef = $ref.split('/'),
resolvedExample;
// .split should return [#, components, schemas, schemaName]
// So length should atleast be 4
if (splitRef.length < 4) {
// not throwing an error. We didn't find the reference - generate a dummy value
return { value: `reference ${$ref} not found in the OpenAPI spec` };
}
// something like #/components/schemas/PaginationEnvelope/properties/page
// will be resolved - we don't care about anything before the components part
// splitRef.slice(1) will return ['components', 'schemas', 'PaginationEnvelope', 'properties', 'page']
// not using _.get here because that fails if there's a . in the property name (Pagination.Envelope, for example)
splitRef = splitRef.slice(1).map((elem) => {
// https://swagger.io/docs/specification/using-ref#escape
// since / is the default delimiter, slashes are escaped with ~1
return decodeURIComponent(
elem
.replace(/~1/g, '/')
.replace(/~0/g, '~')
);
});
resolvedExample = _getEscaped(specComponents, splitRef);
if (resolvedExample === undefined) {
return { value: 'reference ' + $ref + ' not found in the OpenAPI spec' };
}
if (_.has(resolvedExample, '$ref')) {
if (seenRef[resolvedExample.$ref]) {
return {
value: `<Circular reference to ${resolvedExample.$ref} detected>`
};
}
return resolveRefFromSchema(context, resolvedExample.$ref, stackDepth, _.cloneDeep(seenRef));
}
// Add the resolved schema to the global schema cache
context.schemaCache[$ref] = {
schema: resolvedExample,
readOnlyPropCache: {},
writeOnlyPropCache: {}
};
return resolvedExample;
},
resolveExampleData = (context, exampleData) => {
if (_.has(exampleData, '$ref')) {
const resolvedRef = resolveRefForExamples(context, exampleData.$ref);
exampleData = resolveExampleData(context, resolvedRef);
}
else if (typeof exampleData === 'object') {
_.forOwn(exampleData, (data, key) => {
exampleData[key] = resolveExampleData(context, data);
});
}
return exampleData;
},
/**
* returns first example in the input map
* @param {Object} context - Global context object
* @param {Object} exampleObj - Object defined in the schema
* @returns {*} first example in the input map type
*/
getExampleData = (context, exampleObj) => {
let example = {},
exampleKey;
if (!exampleObj || typeof exampleObj !== 'object') {
return '';
}
exampleKey = Object.keys(exampleObj)[0];
example = exampleObj[exampleKey];
if (example && example.$ref) {
example = resolveExampleData(context, example);
}
if (_.get(example, 'value')) {
example = resolveExampleData(context, example.value);
}
return example;
},
/**
* Handle resolution of allOf property of schema
*
* @param {Object} context - Global context object
* @param {Object} schema - Schema to be resolved
* @param {Number} [stack] - Current recursion depth
* @param {*} resolveFor - resolve refs for flow validation/conversion (value to be one of VALIDATION/CONVERSION)
* @param {Object} seenRef - Map of all the references that have been resolved
* @param {String} currentPath - Current path (json-pointer) being resolved relative to original schema
*
* @returns {Object} Resolved schema
*/
resolveAllOfSchema = (context, schema, stack = 0, resolveFor = CONVERSION, seenRef = {}, currentPath = '') => {
try {
return mergeAllOf(_.assign(schema, {
allOf: _.map(schema.allOf, (schema) => {
// eslint-disable-next-line no-use-before-define
return _resolveSchema(context, schema, stack, resolveFor, _.cloneDeep(seenRef), currentPath);
})
}), {
// below option is required to make sure schemas with additionalProperties set to false are resolved correctly
ignoreAdditionalProperties: true,
resolvers: {
// for keywords in OpenAPI schema that are not standard defined JSON schema keywords, use default resolver
defaultResolver: (compacted) => { return compacted[0]; },
enum: (values) => {
return _.uniq(_.concat(...values));
}
}
});
}
catch (e) {
console.warn('Error while resolving allOf schema: ', e);
return { value: '<Error: Could not resolve allOf schema' };
}
},
/**
* Resolve a given ref from the schema
*
* @param {Object} context - Global context
* @param {Object} schema - Schema that is to be resolved
* @param {Number} [stack] - Current recursion depth
* @param {String} resolveFor - For which action this resolution is to be done
* @param {Object} seenRef - Map of all the references that have been resolved
* @param {String} currentPath - Current path (json-pointer) being resolved relative to original schema
* @todo: Explore using a directed graph/tree for maintaining seen ref
*
* @returns {Object} Returns the object that satisfies the schema
*/
_resolveSchema = (context, schema, stack = 0, resolveFor = CONVERSION, seenRef = {}, currentPath = '') => {
if (!schema) {
return new Error('Schema is empty');
}
const { stackLimit } = context.computedOptions;
if (stack >= getRefStackLimit(stackLimit)) {
return { value: ERR_TOO_MANY_LEVELS };
}
stack++;
// eslint-disable-next-line one-var
const compositeKeyword = schema.anyOf ? 'anyOf' : 'oneOf',
{ concreteUtils } = context;
let compositeSchema = schema.anyOf || schema.oneOf;
if (compositeSchema) {
compositeSchema = _.map(compositeSchema, (schemaElement) => {
const isSchemaFullyResolved = _.get(schemaElement, 'value') === ERR_TOO_MANY_LEVELS &&
!_.startsWith(_.get(schemaElement, 'value', ''), '<Circular reference to ');
/**
* elements of composite schema may not have resolved fully,
* we want to avoid assigning these properties to schema element in such cases
*/
PROPERTIES_TO_ASSIGN_ON_CASCADE.forEach((prop) => {
if (_.isNil(schemaElement[prop]) && !_.isNil(schema[prop]) && isSchemaFullyResolved) {
schemaElement[prop] = schema[prop];
}
});
return schemaElement;
});
if (resolveFor === CONVERSION) {
return _resolveSchema(context, compositeSchema[0], stack, resolveFor, _.cloneDeep(seenRef), currentPath);
}
return { [compositeKeyword]: _.map(compositeSchema, (schemaElement, index) => {
return _resolveSchema(context, schemaElement, stack, resolveFor, _.cloneDeep(seenRef),
utils.addToJsonPath(currentPath, [compositeKeyword, index]));
}) };
}
if (schema.allOf) {
return resolveAllOfSchema(context, schema, stack, resolveFor, _.cloneDeep(seenRef), currentPath);
}
if (schema.$ref) {
const schemaRef = schema.$ref;
if (seenRef[schemaRef]) {
return {
value: '<Circular reference to ' + schemaRef + ' detected>'
};
}
seenRef[schemaRef] = true;
if (context.schemaCache[schemaRef]) {
// Also merge readOnly and writeOnly prop cache from schemaCache to global context cache
mergeReadWritePropCache(context, context.schemaCache[schemaRef].readOnlyPropCache,
context.schemaCache[schemaRef].writeOnlyPropCache, currentPath);
schema = context.schemaCache[schemaRef].schema;
}
else {
const existingReadPropCache = context.readOnlyPropCache,
existingWritePropCache = context.writeOnlyPropCache;
schema = resolveRefFromSchema(context, schemaRef, stack, _.cloneDeep(seenRef));
/**
* Reset readOnly and writeOnly prop cache before resolving schema to make sure
* we have fresh cache for $ref resolution which will be stored as part of schemaCache
*/
resetReadWritePropCache(context);
schema = _resolveSchema(context, schema, stack, resolveFor, _.cloneDeep(seenRef), '');
// Add the resolved schema to the global schema cache
context.schemaCache[schemaRef] = {
schema,
readOnlyPropCache: context.readOnlyPropCache,
writeOnlyPropCache: context.writeOnlyPropCache
};
// eslint-disable-next-line one-var
const newReadPropCache = context.readOnlyPropCache,
newWritePropCache = context.writeOnlyPropCache;
// Assign existing readOnly and writeOnly prop cache back to global context cache
context.readOnlyPropCache = existingReadPropCache;
context.writeOnlyPropCache = existingWritePropCache;
// Merge existing and current cache to make sure we have all the properties in cache
mergeReadWritePropCache(context, newReadPropCache, newWritePropCache, currentPath);
}
return schema;
}
// Discard format if not supported by both json-schema-faker and ajv or pattern is also defined
if (!_.includes(SUPPORTED_FORMATS, schema.format) || (schema.pattern && schema.format)) {
schema.format && (delete schema.format);
}
if (
concreteUtils.compareTypes(schema.type, SCHEMA_TYPES.object) ||
schema.hasOwnProperty('properties')
) {
let resolvedSchemaProps = {},
{ includeDeprecated } = context.computedOptions;
_.forOwn(schema.properties, (property, propertyName) => {
// Skip property resolution if it's not schema object
if (!_.isObject(property)) {
return;
}
if (
property.format === 'decimal' ||
property.format === 'byte' ||
property.format === 'password' ||
property.format === 'unix-time'
) {
delete property.format;
}
// Skip addition of deprecated properties based on provided options
if (!includeDeprecated && property.deprecated) {
return;
}
const currentPropPath = utils.addToJsonPath(currentPath, ['properties', propertyName]);
resolvedSchemaProps[propertyName] = _resolveSchema(context, property, stack, resolveFor,
_.cloneDeep(seenRef), currentPropPath);
});
schema.properties = resolvedSchemaProps;
schema.type = schema.type || SCHEMA_TYPES.object;
}
// If schema is of type array
else if (concreteUtils.compareTypes(schema.type, SCHEMA_TYPES.array) && schema.items) {
schema.items = _resolveSchema(context, schema.items, stack, resolveFor, _.cloneDeep(seenRef),
utils.addToJsonPath(currentPath, ['items']));
}
// Any properties to ignored should not be available in schema
else if (_.every(SCHEMA_PROPERTIES_TO_EXCLUDE, (schemaKey) => { return !schema.hasOwnProperty(schemaKey); })) {
if (schema.hasOwnProperty('type')) {
let { parametersResolution } = context.computedOptions;
// Override default value to schema for CONVERSION only for parmeter resolution set to schema
if (resolveFor === CONVERSION && parametersResolution === 'schema') {
if (!schema.hasOwnProperty('format')) {
schema.default = '<' + schema.type + '>';
}
else if (typesMap.hasOwnProperty(schema.type)) {
schema.default = typesMap[schema.type][schema.format];
// in case the format is a custom format (email, hostname etc.)
// https://swagger.io/docs/specification/data-models/data-types/#string
// eslint-disable-next-line max-depth
if (!schema.default && schema.format) {
// Use non defined format only for schema of type string
schema.default = '<' + (schema.type === SCHEMA_TYPES.string ? schema.format : schema.type) + '>';
}
}
else {
schema.default = '<' + schema.type + (schema.format ? ('-' + schema.format) : '') + '>';
}
}
}
}
if (schema.hasOwnProperty('additionalProperties')) {
schema.additionalProperties = _.isBoolean(schema.additionalProperties) ? schema.additionalProperties :
_resolveSchema(context, schema.additionalProperties, stack, resolveFor, _.cloneDeep(seenRef),
utils.addToJsonPath(currentPath, ['additionalProperties']));
schema.type = schema.type || SCHEMA_TYPES.object;
}
// Resolve refs inside enums to value
if (schema.hasOwnProperty('enum')) {
_.forEach(schema.enum, (item, index) => {
if (item && item.hasOwnProperty('$ref')) {
schema.enum[index] = resolveRefFromSchema(
context, item.$ref, stack, _.cloneDeep(seenRef)
);
}
});
}
// Keep track of readOnly and writeOnly properties to resolve request and responses accordingly later.
if (schema.readOnly) {
context.readOnlyPropCache[currentPath] = true;
}
if (schema.writeOnly) {
context.writeOnlyPropCache[currentPath] = true;
}
return schema;
},
/**
* Processes and resolves types from Nested JSON schema structure.
*
* @param {Object} resolvedSchema - The resolved JSON schema to process for type extraction.
* @returns {Object} The processed schema details.
*/
processSchema = (resolvedSchema) => {
if (resolvedSchema.type === 'object' && resolvedSchema.properties) {
const schemaDetails = {
type: resolvedSchema.type,
properties: {},
required: []
},
requiredProperties = new Set(resolvedSchema.required || []);
for (let [propName, propValue] of Object.entries(resolvedSchema.properties)) {
if (!propValue.type) {
continue;
}
const propertyDetails = {
type: propValue.type,
deprecated: propValue.deprecated,
enum: propValue.enum || undefined,
minLength: propValue.minLength,
maxLength: propValue.maxLength,
minimum: propValue.minimum,
maximum: propValue.maximum,
pattern: propValue.pattern,
example: propValue.example,
description: propValue.description,
format: propValue.format
};
if (requiredProperties.has(propName)) {
schemaDetails.required.push(propName);
}
if (propValue.properties) {
let processedProperties = processSchema(propValue);
propertyDetails.properties = processedProperties.properties;
if (processedProperties.required) {
propertyDetails.required = processedProperties.required;
}
}
else if (propValue.type === 'array' && propValue.items) {
propertyDetails.items = processSchema(propValue.items);
}
schemaDetails.properties[propName] = propertyDetails;
}
if (schemaDetails.required && schemaDetails.required.length === 0) {
schemaDetails.required = undefined;
}
return schemaDetails;
}
else if (resolvedSchema.type === 'array' && resolvedSchema.items) {
const arrayDetails = {
type: resolvedSchema.type,
items: processSchema(resolvedSchema.items)
};
if (resolvedSchema.minItems !== undefined) { arrayDetails.minItems = resolvedSchema.minItems; }
if (resolvedSchema.maxItems !== undefined) { arrayDetails.maxItems = resolvedSchema.maxItems; }
return arrayDetails;
}
return {
type: resolvedSchema.type
};
},
/**
* Wrapper around _resolveSchema which resolves a given schema
*
* @param {Object} context - Global context
* @param {Object} schema - Schema that is to be resolved
* @param {Object} resolutionMeta - Metadata of resolution taking place
* @param {Number} resolutionMeta.stack - Current recursion depth
* @param {String} resolutionMeta.resolveFor - For which action this resolution is to be done
* @param {Object} resolutionMeta.seenRef - Map of all the references that have been resolved
* @param {Boolean} resolutionMeta.isResponseSchema - Whether schema is from response or not
*
* @returns {Object} Returns the object that satisfies the schema
*/
resolveSchema = (context, schema,
{ stack = 0, resolveFor = CONVERSION, seenRef = {}, isResponseSchema = false } = {}
) => {
// reset readOnly and writeOnly prop cache before resolving schema to make sure we have fresh cache
resetReadWritePropCache(context);
let resolvedSchema = _resolveSchema(context, schema, stack, resolveFor, seenRef);
/**
* If readOnly or writeOnly properties are present in the schema, we need to clone original schema first.
* Because we modify original resolved schema and delete readOnly or writeOnly properties from it
* depending upon if schema belongs to Request or Response.
* This is done to avoid modifying original schema object and to keep it intact for future use.
*/
if (!_.isEmpty(context.readOnlyPropCache) || !_.isEmpty(context.writeOnlyPropCache)) {
resolvedSchema = _.cloneDeep(resolvedSchema);
}
if (isResponseSchema) {
_.forOwn(context.writeOnlyPropCache, (value, key) => {
// We need to make sure to remove empty strings via _.compact that are added while forming json-pointer
_.unset(resolvedSchema, utils.getJsonPathArray(key));
});
}
else {
_.forOwn(context.readOnlyPropCache, (value, key) => {
// We need to make sure to remove empty strings via _.compact that are added while forming json-pointer
_.unset(resolvedSchema, utils.getJsonPathArray(key));
});
}
return resolvedSchema;
},
/**
* Provides information regarding serialisation of param
*
* @param {Object} context - Required context from related SchemaPack function
* @param {Object} param - OpenAPI Parameter object
* @param {Object} options - Options object
* @param {Boolean} options.isResponseSchema - Whether schema is from response or not
* @returns {Object} - Information regarding parameter serialisation. Contains following properties.
* {
* style - style property defined/inferred from schema
* explode - explode property defined/inferred from schema
* startValue - starting value that is prepended to serialised value
* propSeparator - Character that separates two properties or values in serialised string of respective param
* keyValueSeparator - Character that separates key from values in serialised string of respective param
* isExplodable - whether params can be exploded (serialised value can contain key and value)
* }
*/
getParamSerialisationInfo = (context, param, { isResponseSchema = false } = {}) => {
let paramName = _.get(param, 'name'),
paramSchema,
style, // style property defined/inferred from schema
explode, // explode property defined/inferred from schema
propSeparator, // separates two properties or values
keyValueSeparator, // separats key from value
startValue = '', // starting value that is unique to each style
// following prop represents whether param can be truly exploded, as for some style even when explode is true,
// serialisation doesn't separate key-value
isExplodable;
// for invalid param object return null
if (!_.isObject(param)) {
return {};
}
// Resolve the ref and composite schemas
paramSchema = resolveSchema(context, param.schema, { isResponseSchema });
isExplodable = paramSchema.type === 'object';
// decide allowed / default style for respective param location
switch (param.in) {
case 'path':
style = _.includes(['matrix', 'label', 'simple'], param.style) ? param.style : 'simple';
break;
case 'query':
style = _.includes(['form', 'spaceDelimited', 'pipeDelimited', 'deepObject'], param.style) ?
param.style : 'form';
break;
case 'header':
style = 'simple';
break;
default:
style = 'simple';
break;
}
// decide allowed / default explode property for respective param location
explode = (_.isBoolean(param.explode) ? param.explode : (_.includes(['form', 'deepObject'], style)));
// decide explodable params, starting value and separators between key-value and properties for serialisation
switch (style) {
case 'matrix':
isExplodable = paramSchema.type === 'object' || explode;
startValue = ';' + ((paramSchema.type === 'object' && explode) ? '' : (paramName + '='));
propSeparator = explode ? ';' : ',';
keyValueSeparator = explode ? '=' : ',';
break;
case 'label':
startValue = '.';
propSeparator = '.';
keyValueSeparator = explode ? '=' : '.';
break;
case 'form':
// for 'form' when explode is true, query is devided into different key-value pairs
propSeparator = keyValueSeparator = ',';
break;
case 'simple':
propSeparator = ',';
keyValueSeparator = explode ? '=' : ',';
break;
case 'spaceDelimited':
explode = false;
propSeparator = keyValueSeparator = '%20';
break;
case 'pipeDelimited':
explode = false;
propSeparator = keyValueSeparator = '|';
break;
case 'deepObject':
// for 'deepObject' query is devided into different key-value pairs
explode = true;
break;
default:
break;
}
return { style, explode, startValue, propSeparator, keyValueSeparator, isExplodable };
},
/**
*
* @param {*} input - input string that needs to be hashed
* @returns {*} sha1 hash of the string
*/
hash = (input) => {
return crypto.createHash('sha1').update(input).digest('base64');
},
fakeSchema = (context, schema, shouldGenerateFromExample = true) => {
try {
let stringifiedSchema = typeof schema === 'object' && (JSON.stringify(schema)),
key = hash(stringifiedSchema),
restrictArrayItems = typeof stringifiedSchema === 'string' &&
(stringifiedSchema.length > SCHEMA_SIZE_OPTIMIZATION_THRESHOLD),
fakedSchema;
// unassign potentially larger string data after calculation as not required
stringifiedSchema = null;
if (context.schemaFakerCache[key]) {
return context.schemaFakerCache[key];
}
schemaFaker.option({
useExamplesValue: shouldGenerateFromExample,
defaultMinItems: restrictArrayItems ? 1 : 2,
defaultMaxItems: restrictArrayItems ? 1 : 2
});
fakedSchema = schemaFaker(schema, null, context.schemaValidationCache || {});
context.schemaFakerCache[key] = fakedSchema;
return fakedSchema;
}
catch (error) {
console.warn(
'Error faking a schema. Not faking this schema. Schema:', schema,
'Error', error
);
return null;
}
},
/**
* Resolve value of a given parameter
*
* @param {Object} context - Required context from related SchemaPack function
* @param {Object} param - Parameter that is to be resolved from schema
* @param {Object} options - Addition options
* @param {String} options.schemaFormat - Corresponding schema format (can be one of xml/default)
* @param {Boolean} options.isResponseSchema - Whether schema is from response or not
* @returns {*} Value of the parameter
*/
resolveValueOfParameter = (context, param,
{ schemaFormat = SCHEMA_FORMATS.DEFAULT, isResponseSchema = false } = {}
) => {
if (!param || !param.hasOwnProperty('schema')) {
return '';
}
const { indentCharacter } = context.computedOptions,
resolvedSchema = resolveSchema(context, param.schema, { isResponseSchema }),
{ parametersResolution } = context.computedOptions,
shouldGenerateFromExample = parametersResolution === 'example',
hasExample = param.example !== undefined ||
param.schema.example !== undefined ||
param.examples !== undefined ||
param.schema.examples !== undefined;
if (shouldGenerateFromExample && hasExample) {
/**
* Here it could be example or examples (plural)
* For examples, we'll pick the first example
*/
let example;
if (param.example !== undefined) {
example = param.example;
}
else if (param.schema.example !== undefined) {
example = _.has(param.schema.example, 'value') ? param.schema.example.value : param.schema.example;
}
else {
example = getExampleData(context, param.examples || param.schema.examples);
}
return example;
}
schemaFaker.option({
useExamplesValue: true
});
if (resolvedSchema.properties) {
// If any property exists with format:binary (and type: string) schemaFaker crashes
// we just delete based on format=binary
for (const prop in resolvedSchema.properties) {
if (resolvedSchema.properties.hasOwnProperty(prop)) {
if (
resolvedSchema.properties[prop].format === 'byte' ||
resolvedSchema.properties[prop].format === 'decimal'
) {
delete resolvedSchema.properties[prop].format;
}
}
}
}
try {
if (schemaFormat === SCHEMA_FORMATS.XML) {
return xmlFaker(null, resolvedSchema, indentCharacter, parametersResolution);
}
// for JSON, the indentCharacter will be applied in the JSON.stringify step later on
return fakeSchema(context, resolvedSchema, shouldGenerateFromExample);
}
catch (e) {
console.warn(
'Error faking a schema. Not faking this schema. Schema:', resolvedSchema,
'Error', e
);
return '';
}
},
/**
* Resolve the url of the Postman request from the operation item
* @param {Object} operationPath - Exact path of the operation defined in the schema
* @returns {String} Url of the request
*/
resolveUrlForPostmanRequest = (operationPath) => {
return sanitizeUrl(operationPath);
},
/**
* Recursively extracts key-value pair from deep objects.
*
* @param {Object} deepObject - Deep object
* @param {String} objectKey - key associated with deep object
* @returns {Array} array of param key-value pairs
*/
extractDeepObjectParams = (deepObject, objectKey) => {
let extractedParams = [];
_.keys(deepObject).forEach((key) => {
let value = deepObject[key];
if (value && typeof value === 'object') {
extractedParams = _.concat(extractedParams, extractDeepObjectParams(value, objectKey + '[' + key + ']'));
}
else {
extractedParams.push({ key: objectKey + '[' + key + ']', value });
}
});
return extractedParams;
},
/**
* Gets the description of the parameter.
* If the parameter is required, it prepends a `(Requried)` before the parameter description
* If the parameter type is enum, it appends the possible enum values
* @param {object} parameter - input param for which description needs to be returned
* @returns {string} description of the parameters
*/
getParameterDescription = (parameter) => {
if (!_.isObject(parameter)) {
return '';
}
return (parameter.required ? '(Required) ' : '') + (parameter.description || '') +
(parameter.enum ? ' (This can only be one of ' + parameter.enum + ')' : '');
},
/**
* Serialise Param based on mentioned style field in schema object
*
* @param {Object} context - Global context object
* @param {Object} param - OpenAPI Parameter object
* @param {*} paramValue - Value of the parameter
* @param {Object} options - Additional options for serialisation
* @param {Boolean} options.isResponseSchema - Whether schema is from response or not
* @returns {Array} - Array of key-value pairs for the parameter
*/
serialiseParamsBasedOnStyle = (context, param, paramValue, { isResponseSchema = false } = {}) => {
const { style, explode, startValue, propSeparator, keyValueSeparator, isExplodable } =
getParamSerialisationInfo(context, param, { isResponseSchema }),
{ enableOptionalParameters } = context.computedOptions;
let serialisedValue = '',
description = getParameterDescription(param),
paramName = _.get(param, 'name'),
disabled = !enableOptionalParameters && _.get(param, 'required') !== true,
pmParams = [],
isNotSerializable = false;
// decide explodable params, starting value and separators between key-value and properties for serialisation
// Ref: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#style-examples
switch (style) {
case 'form':
if (explode && _.isObject(paramValue)) {
const isArrayValue = _.isArray(paramValue);
_.forEach(paramValue, (value, key) => {
pmParams.push({
key: isArrayValue ? paramName : key,
value: (value === undefined ? '' : _.toString(value)),
description,
disabled
});
});
return pmParams;
}
break;
case 'deepObject':
if (_.isObject(paramValue) && !_.isArray(paramValue)) {
let extractedParams = extractDeepObjectParams(paramValue, paramName);
_.forEach(extractedParams, (extractedParam) => {
pmParams.push({
key: extractedParam.key,
value: _.toString(extractedParam.value) || '',
description,
disabled
});
});
return pmParams;
}
else if (_.isArray(paramValue)) {
isNotSerializable = true;
pmParams.push({
key: paramName,
value: '<Error: Not supported in OAS>',
disabled
});
}
break;
default:
break;
}
if (isNotSerializable) {
return pmParams;
}
if (_.isObject(paramValue)) {
_.forEach(paramValue, (value, key) => {
// add property separator for all index/keys except first
!_.isEmpty(serialisedValue) && (serialisedValue += propSeparator);
// append key for param that can be exploded
isExplodable && (serialisedValue += (key + keyValueSeparator));
serialisedValue += (value === undefined ? '' : _.toString(value));
});
}
// for non-object and non-empty value append value as is to string
else if (!_.isNil(paramValue)) {
serialisedValue += paramValue;
}
// prepend starting value to serialised value (valid for empty value also)
serialisedValue = startValue + serialisedValue;
pmParams.push({
key: paramName,
value: _.toString(serialisedValue),
description,
disabled
});
return pmParams;
},
getTypeOfContent = (content) => {
if (_.isArray(content)) {
return SCHEMA_TYPES.array;
}
return typeof content;
},
/**
* Parses media type from given content-type header or media type
* from content object into type and subtype
*
* @param {String} str - string to be parsed
* @returns {Object} - Parsed media type into type and subtype
*/
parseMediaType = (str) => {
let simpleMediaTypeRegExp = /^\s*([^\s\/;]+)\/([^;\s]+)\s*(?:;(.*))?$/,
match = simpleMediaTypeRegExp.exec(str),
type = '',
subtype = '';
if (match) {
// as mediatype name are case-insensitive keep it in lower case for uniformity
type = _.toLower(match[1]);
subtype = _.toLower(match[2]);
}
return { type, subtype };
},
/**
* Get the format of content type header
* @param {string} cTypeHeader - the content type header string
* @returns {string} type of content type header
*/
getHeaderFamily = (cTypeHeader) => {
let mediaType = parseMediaType(cTypeHeader);
if (mediaType.type === 'application' &&
(mediaType.subtype === 'json' || _.endsWith(mediaType.subtype, '+json'))) {
return HEADER_TYPE.JSON;
}
if ((mediaType.type === 'application' || mediaType.type === 'text') &&
(mediaType.subtype === 'xml' || _.endsWith(mediaType.subtype, '+xml'))) {
return HEADER_TYPE.XML;
}
return HEADER_TYPE.INVALID;
},
/**
* Gets XML Example data in correct format based on schema
*
* @param {Object} context - Global context object
* @param {Object} exampleData - Example data to be used
* @param {Object} requestBodySchema - Schema of the request body
* @returns {String} XML Example data
*/
getXMLExampleData = (context, exampleData, requestBodySchema) => {
const { parametersResolution, indentCharacter } = context.computedOptions;
let reqBodySchemaWithExample = requestBodySchema;
// Assign example at schema level to be faked by xmlSchemaFaker
if (typeof requestBodySchema === 'object') {
reqBodySchemaWithExample = Object.assign({}, requestBodySchema, { example: exampleData });
}
return xmlFaker(null, reqBodySchemaWithExample, indentCharacter, parametersResolution);
},
/**
* Generates postman equivalent examples which contains request and response mappings of
* each example based on examples mentioned in definition
*
* This matching between request bodies and response bodies are done in following order.
* 1. Try matching keys from request and response examples
*
* (We'll also be considering any request body example with response code as key
* that's matching default response body example if present
* See fro example - test/data/valid_openapi/multiExampleResponseCodeMatching.json)
*
* 2. If any key matching is found, we'll generate example from it and ignore non-matching keys
*
* 3. If no matching key is found, we'll generate examples based on positional matching.
*
* Positional matching means first example in request body will be matched with first example
* in response body and so on. Any left over request or response body for which
* positional matching is not found, we'll use first req/res example.
*
* @param {Object} context - Global context object
* @param {Object} responseExamples - Examples defined in the response
* @param {Object} requestBodyExamples - Examples defined in the request body
* @param {Object} responseBodySchema - Schema of the response body
* @param {Boolean} isXMLExample - Whether the example is XML example
* @returns {Array} Examples for corresponding operation
*/
generateExamples = (context, responseExamples, requestBodyExamples, responseBodySchema, isXMLExample) => {
const pmExamples = [],
responseExampleKeys = _.map(responseExamples, 'key'),
requestBodyExampleKeys = _.map(requestBodyExamples, 'key'),
usedRequestExamples = _.fill(Array(requestBodyExamples.length), false),
exampleKeyComparator = (example, key) => {
return _.toLower(example.key) === _.toLower(key);
};
let matchedKeys = _.intersectionBy(responseExampleKeys, requestBodyExampleKeys, _.toLower),
isResponseCodeMatching = false;
// Only match in case of default response example matching with any request body example
if (!matchedKeys.length && responseExamples.length === 1 && responseExamples[0].key === '_default') {
const responseCodes = _.map(responseExamples, 'responseCode');
matchedKeys = _.intersectionBy(responseCodes, requestBodyExampleKeys, _.toLower);
isResponseCodeMatching = matchedKeys.length > 0;
}
// Do keys matching first and ignore any leftover req/res body for which matching is not found
if (matchedKeys.length) {
_.forEach(matchedKeys, (key) => {
const matchedRequestExamples = _.filter(requestBodyExamples, (example) => {
return exampleKeyComparator(example, key);
}),
responseExample = _.find(responseExamples, (example) => {
// If there is a response code key-matching, then only match with keys based on response code
if (isResponseCodeMatching) {
return example.responseCode === key;
}
return exampleKeyComparator(example, key);
});
let requestExample = _.find(matchedRequestExamples, ['contentType', _.get(responseExample, 'contentType')]),
responseExampleData;
if (!requestExample) {
requestExample = _.head(matchedRequestExamples);
}
responseExampleData = getExampleData(context, { [responseExample.key]: responseExample.value });
if (isXMLExample) {
responseExampleData = getXMLExampleData(context, responseExampleData, responseBodySchema);
}
pmExamples.push({
request: getExampleData(context, { [requestExample.key]: requestExample.value }),
response: responseExampleData,
name: _.get(responseExample, 'value.summary') ||
(responseExample.key !== '_default' && responseExample.key) ||
_.get(requestExample, 'value.summary') || requestExample.key || 'Example'
});
});
return pmExamples;
}
// No key matching between req and res were found, so perform positional matching now
_.forEach(responseExamples, (responseExample, index) =