openapi-to-postmanv2
Version:
Convert a given OpenAPI specification to Postman Collection v2.0
1,354 lines (1,211 loc) • 105 kB
JavaScript
/* eslint-disable require-jsdoc */
// TODO: REMOVE THIS ☝🏻
const _ = require('lodash'),
{ Header } = require('postman-collection/lib/collection/header'),
{ QueryParam } = require('postman-collection/lib/collection/query-param'),
{ Url } = require('postman-collection/lib/collection/url'),
{ Variable } = require('postman-collection/lib/collection/variable'),
async = require('async'),
crypto = require('crypto'),
schemaFaker = require('../assets/json-schema-faker.js'),
xmlFaker = require('./xmlSchemaFaker.js'),
utils = require('./utils'),
{
resolveSchema,
resolveRefFromSchema,
resolvePostmanRequest,
resolveResponseForPostmanRequest
} = require('./schemaUtils'),
concreteUtils = require('../lib/30XUtils/schemaUtils30X'),
ajvValidationError = require('../lib/ajValidation/ajvValidationError'),
{ validateSchema } = require('../lib/ajValidation/ajvValidation'),
{ formatDataPath, checkIsCorrectType, isKnownType,
getServersPathVars } = require('../lib/common/schemaUtilsCommon.js'),
{ findMatchingRequestFromSchema, isPmVariable } = require('./requestMatchingUtils'),
// common global constants
SCHEMA_FORMATS = {
DEFAULT: 'default', // used for non-request-body data and json
XML: 'xml' // used for request-body XMLs
},
URLENCODED = 'application/x-www-form-urlencoded',
TEXT_PLAIN = 'text/plain',
PARAMETER_SOURCE = {
REQUEST: 'REQUEST',
RESPONSE: 'RESPONSE'
},
HEADER_TYPE = {
JSON: 'json',
XML: 'xml',
INVALID: 'invalid'
},
propNames = {
QUERYPARAM: 'query parameter',
PATHVARIABLE: 'path variable',
HEADER: 'header',
BODY: 'request body',
RESPONSE_HEADER: 'response header',
RESPONSE_BODY: 'response body'
},
// Specifies types of processing Refs
PROCESSING_TYPE = {
VALIDATION: 'VALIDATION',
CONVERSION: 'CONVERSION'
},
// These are the methods supported in the PathItem schema
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject
METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'],
// 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'
],
OAS_NOT_SUPPORTED = '<Error: Not supported in OAS>',
/**
* @sujay: this needs to be a better global level setting
* before we start using the v2 validations everywhere.
*/
VALIDATE_OPTIONAL_PARAMS = true;
// 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: array)
useDefaultValue: true,
ignoreMissingRefs: true,
avoidExampleItemsLength: true, // option to avoid validating type array schema example's minItems and maxItems props.
failOnInvalidFormat: false
});
/**
*
* @param {*} input - input string that needs to be hashed
* @returns {*} sha1 hash of the string
*/
function hash(input) {
return crypto.createHash('sha1').update(input).digest('base64');
}
/**
* Provides context that's needed for V2 resolveSchema() interface
*
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
* @param {object} components - components defined in the OAS spec. These are used to
* resolve references while generating params
* @returns {object} - Provides default context
*/
function getDefaultContext (options, components = {}) {
return {
concreteUtils,
schemaCache: {},
computedOptions: options,
schemaValidationCache: new Map(),
specComponents: { components: components.components }
};
}
/**
* Verifies if the deprecated operations should be added
*
* @param {object} operation - openAPI operation object
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
* @returns {boolean} whether to add or not the deprecated operation
*/
function shouldAddDeprecatedOperation (operation, options) {
if (typeof operation === 'object') {
return !operation.deprecated ||
(operation.deprecated === true && options.includeDeprecated === true);
}
return false;
}
/**
* Safe wrapper for schemaFaker that resolves references and
* removes things that might make schemaFaker crash
* @param {Object} context - Required context from related SchemaPack function
* @param {*} oldSchema the schema to fake
* generate a fake object, example: use specified examples as-is). Default: schema
* @param {*} resolveFor - resolve refs for flow validation/conversion (value to be one of VALIDATION/CONVERSION)
* @param {string} parameterSourceOption Specifies whether the schema being faked is from a request or response.
* @param {*} components list of predefined components (with schemas)
* @param {string} schemaFormat default or xml
* @param {object} schemaCache - object storing schemaFaker and schemaResolution caches
* @returns {object} fakedObject
*/
function safeSchemaFaker (context, oldSchema, resolveFor, parameterSourceOption, components,
schemaFormat, schemaCache) {
let prop, key, resolvedSchema, fakedSchema,
schemaFakerCache = _.get(schemaCache, 'schemaFakerCache', {}),
concreteUtils = context.concreteUtils;
const options = context.computedOptions,
resolveTo = _.get(options, 'parametersResolution', 'example'),
indentCharacter = options.indentCharacter;
/**
* Schema is cloned here as resolveSchema() when called for CONVERSION use cases, will mutate schema in certain way.
* i.e. For array it'll add maxItems = 2. This should be avoided as we'll again be needing non-mutated schema
* in further VALIDATION use cases as needed.
*/
resolvedSchema = resolveSchema(context, _.cloneDeep(oldSchema), {
resolveFor: _.toLower(PROCESSING_TYPE.CONVERSION),
isResponseSchema: parameterSourceOption === PARAMETER_SOURCE.RESPONSE
});
resolvedSchema = concreteUtils.fixExamplesByVersion(resolvedSchema);
key = JSON.stringify(resolvedSchema);
if (resolveTo === 'schema') {
key = 'resolveToSchema ' + key;
schemaFaker.option({
useExamplesValue: false,
useDefaultValue: true
});
}
else if (resolveTo === 'example') {
key = 'resolveToExample ' + key;
schemaFaker.option({
useExamplesValue: true
});
}
if (resolveFor === PROCESSING_TYPE.VALIDATION) {
schemaFaker.option({
avoidExampleItemsLength: false
});
}
if (schemaFormat === 'xml') {
key += ' schemaFormatXML';
}
else {
key += ' schemaFormatDEFAULT';
}
key = hash(key);
if (schemaFakerCache[key]) {
return schemaFakerCache[key];
}
if (resolvedSchema.properties) {
// If any property exists with format:binary (and type: string) schemaFaker crashes
// we just delete based on format=binary
for (prop in resolvedSchema.properties) {
if (resolvedSchema.properties.hasOwnProperty(prop)) {
if (resolvedSchema.properties[prop].format === 'binary') {
delete resolvedSchema.properties[prop].format;
}
}
}
}
try {
if (schemaFormat === SCHEMA_FORMATS.XML) {
fakedSchema = xmlFaker(null, resolvedSchema, indentCharacter);
schemaFakerCache[key] = fakedSchema;
return fakedSchema;
}
// for JSON, the indentCharacter will be applied in the JSON.stringify step later on
fakedSchema = schemaFaker(resolvedSchema, null, _.get(schemaCache, 'schemaValidationCache'));
schemaFakerCache[key] = fakedSchema;
return fakedSchema;
}
catch (e) {
console.warn(
'Error faking a schema. Not faking this schema. Schema:', resolvedSchema,
'Error', e
);
return null;
}
}
/** Separates out collection and path variables from the reqUrl
*
* @param {string} reqUrl Request Url
* @param {Array} pathVars Path variables
*
* @returns {Object} reqUrl, updated path Variables array and collection Variables.
*/
function sanitizeUrlPathParams (reqUrl, pathVars) {
var matches,
collectionVars = [];
// converts all the of the following:
// /{{path}}/{{file}}.{{format}}/{{hello}} => /:path/{{file}}.{{format}}/:hello
matches = utils.findPathVariablesFromPath(reqUrl);
if (matches) {
matches.forEach((match) => {
const replaceWith = match.replace(/{{/g, ':').replace(/}}/g, '');
reqUrl = reqUrl.replace(match, replaceWith);
});
}
// Separates pathVars array and collectionVars.
matches = utils.findCollectionVariablesFromPath(reqUrl);
if (matches) {
matches.forEach((match) => {
const collVar = match.replace(/{{/g, '').replace(/}}/g, '');
pathVars = pathVars.filter((item) => {
if (item.name === collVar) {
collectionVars.push(item);
}
return !(item.name === collVar);
});
});
}
return { url: reqUrl, pathVars, collectionVars };
}
/**
*
* @param {*} transaction Transaction with which to compare
* @param {*} transactionPathPrefix the jsonpath for this validation (will be prepended to all identified mismatches)
* @param {*} schemaPath the applicable pathItem defined at the schema level
* @param {*} pathRoute Route to applicable pathItem (i.e. 'GET /users/{userID}')
* @param {*} options OAS options
* @param {*} callback Callback
* @returns {array} mismatches (in the callback)
*/
function checkMetadata (transaction, transactionPathPrefix, schemaPath, pathRoute, options, callback) {
let expectedReqName,
reqNameMismatch,
actualReqName = _.get(transaction, 'name'),
trimmedReqName,
mismatches = [],
mismatchObj,
reqUrl;
if (!options.validateMetadata) {
return callback(null, []);
}
// only validate string upto 255 character as longer name results in issues while updation
trimmedReqName = utils.trimRequestName(actualReqName);
// handling path templating in request url if any
// convert all {anything} to {{anything}}
reqUrl = utils.fixPathVariablesInUrl(pathRoute.slice(pathRoute.indexOf('/')));
// convert all /{{one}}/{{two}} to /:one/:two
// Doesn't touch /{{file}}.{{format}}
reqUrl = sanitizeUrlPathParams(reqUrl, []).url;
switch (options.requestNameSource) {
case 'fallback' : {
// operationId is usually camelcase or snake case
expectedReqName = schemaPath.summary || utils.insertSpacesInName(schemaPath.operationId) ||
schemaPath.description || reqUrl;
expectedReqName = utils.trimRequestName(expectedReqName);
reqNameMismatch = (trimmedReqName !== expectedReqName);
break;
}
case 'url' : {
// actual value may differ in conversion as it uses local/global servers info to generate it
// for now suggest actual path as request name
expectedReqName = reqUrl;
expectedReqName = utils.trimRequestName(expectedReqName);
reqNameMismatch = !_.endsWith(actualReqName, reqUrl);
break;
}
default : {
expectedReqName = schemaPath[options.requestNameSource];
expectedReqName = utils.trimRequestName(expectedReqName);
reqNameMismatch = (trimmedReqName !== expectedReqName);
break;
}
}
if (reqNameMismatch) {
mismatchObj = {
property: 'REQUEST_NAME',
transactionJsonPath: transactionPathPrefix + '.name',
schemaJsonPath: null,
reasonCode: 'INVALID_VALUE',
reason: 'The request name didn\'t match with specified schema'
};
options.suggestAvailableFixes && (mismatchObj.suggestedFix = {
key: 'name',
actualValue: actualReqName || null,
suggestedValue: expectedReqName
});
mismatches.push(mismatchObj);
}
/**
* Note: Request Description validation/syncing is removed with v2 interface
*/
return callback(null, mismatches);
}
/**
* Given parameter objects, it assigns example/examples of parameter object as schema example.
*
* @param {Object} parameter - parameter object
* @returns {null} - null
*/
function assignParameterExamples (parameter) {
let example = _.get(parameter, 'example'),
examples = _.values(_.get(parameter, 'examples'));
if (example !== undefined) {
_.set(parameter, 'schema.example', example);
}
else if (examples) {
let exampleToUse = _.get(examples, '[0].value');
!_.isUndefined(exampleToUse) && (_.set(parameter, 'schema.example', exampleToUse));
}
}
/**
* 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
*/
function getParameterDescription (parameter) {
if (!_.isObject(parameter)) {
return '';
}
return (parameter.required ? '(Required) ' : '') + (parameter.description || '') +
(parameter.enum ? ' (This can only be one of ' + parameter.enum + ')' : '');
}
/**
* Provides information regarding serialisation of param
*
* @param {*} param - OpenAPI Parameter object
* @param {String} parameterSource - Specifies whether the schema being faked is from a request or response.
* @param {Object} components - OpenAPI components defined in the OAS spec. These are used to
* resolve references while generating params.
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
* @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)
* }
*/
function getParamSerialisationInfo (param, parameterSource, components, options) {
var paramName = _.get(param, 'name'),
paramSchema = resolveSchema(getDefaultContext(options, components), _.cloneDeep(param.schema), {
resolveFor: PROCESSING_TYPE.VALIDATION,
isResponseSchema: parameterSource === PARAMETER_SOURCE.RESPONSE
}),
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 = paramSchema.type === 'object';
// for invalid param object return null
if (!_.isObject(param)) {
return null;
}
// 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 divided 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 divided into different key-value pairs
explode = true;
break;
default:
break;
}
return { style, explode, startValue, propSeparator, keyValueSeparator, isExplodable };
}
/**
* This function deserialises parameter value based on param schema
*
* @param {*} param - OpenAPI Parameter object
* @param {String} paramValue - Parameter value to be deserialised
* @param {String} parameterSource - Specifies whether the schema being faked is from a request or response.
* @param {Object} components - OpenAPI components defined in the OAS spec. These are used to
* resolve references while generating params.
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
* @param {Object} schemaCache - object storing schemaFaker and schemaResolution caches
* @returns {*} - deserialises parameter value
*/
function deserialiseParamValue (param, paramValue, parameterSource, components, options) {
var constructedValue,
paramSchema = resolveSchema(getDefaultContext(options, components), _.cloneDeep(param.schema), {
resolveFor: PROCESSING_TYPE.VALIDATION,
isResponseSchema: parameterSource === PARAMETER_SOURCE.RESPONSE
}),
isEvenNumber = (num) => {
return (num % 2 === 0);
},
convertToDataType = (value) => {
try {
return JSON.parse(value);
}
catch (e) {
return value;
}
};
// for invalid param object return null
if (!_.isObject(param) || !_.isString(paramValue)) {
return null;
}
let { startValue, propSeparator, keyValueSeparator, isExplodable } =
getParamSerialisationInfo(param, parameterSource, components, options);
// as query params are constructed from url, during conversion we use decodeURI which converts ('%20' into ' ')
(keyValueSeparator === '%20') && (keyValueSeparator = ' ');
(propSeparator === '%20') && (propSeparator = ' ');
// remove start value from serialised value
paramValue = paramValue.slice(paramValue.indexOf(startValue) === 0 ? startValue.length : 0);
// define value to constructed according to type
paramSchema.type === 'object' && (constructedValue = {});
paramSchema.type === 'array' && (constructedValue = []);
if (constructedValue) {
let allProps = paramValue.split(propSeparator);
_.forEach(allProps, (element, index) => {
let keyValArray;
if (propSeparator === keyValueSeparator && isExplodable) {
if (isEvenNumber(index)) {
keyValArray = _.slice(allProps, index, index + 2);
}
else {
return;
}
}
else if (isExplodable) {
keyValArray = element.split(keyValueSeparator);
}
if (paramSchema.type === 'object') {
_.set(constructedValue, keyValArray[0], convertToDataType(keyValArray[1]));
}
else if (paramSchema.type === 'array') {
constructedValue.push(convertToDataType(_.get(keyValArray, '[1]', element)));
}
});
}
else {
constructedValue = paramValue;
}
return constructedValue;
}
/**
* This function is little modified version of lodash _.get()
* where if path is empty it will return source object instead undefined/fallback value
*
* @param {Object} sourceValue - source from where value is to be extracted
* @param {String} dataPath - json path to value that is to be extracted
* @param {*} fallback - fallback value if sourceValue doesn't contain value at dataPath
* @returns {*} extracted value
*/
function getPathValue (sourceValue, dataPath, fallback) {
return (dataPath === '' ? sourceValue : _.get(sourceValue, dataPath, fallback));
}
/**
* This function extracts suggested value from faked value at Ajv mismatch path (dataPath)
*
* @param {*} fakedValue Faked value by jsf
* @param {*} actualValue Actual value in transaction
* @param {*} ajvValidationErrorObj Ajv error for which fix is suggested
* @returns {*} Suggested Value
*/
function getSuggestedValue (fakedValue, actualValue, ajvValidationErrorObj) {
var suggestedValue,
tempSuggestedValue,
dataPath = formatDataPath(ajvValidationErrorObj.instancePath || ''),
targetActualValue,
targetFakedValue;
// discard the leading '.' if it exists
if (dataPath[0] === '.') {
dataPath = dataPath.slice(1);
}
targetActualValue = getPathValue(actualValue, dataPath, {});
targetFakedValue = getPathValue(fakedValue, dataPath, {});
switch (ajvValidationErrorObj.keyword) {
// to do: check for minItems, maxItems
case 'minProperties':
suggestedValue = _.assign({}, targetActualValue,
_.pick(targetFakedValue, _.difference(_.keys(targetFakedValue), _.keys(targetActualValue))));
break;
case 'maxProperties':
suggestedValue = _.pick(targetActualValue, _.intersection(_.keys(targetActualValue), _.keys(targetFakedValue)));
break;
case 'required':
suggestedValue = _.assign({}, targetActualValue,
_.pick(targetFakedValue, ajvValidationErrorObj.params.missingProperty));
break;
case 'minItems':
suggestedValue = _.concat(targetActualValue, _.slice(targetFakedValue, targetActualValue.length));
break;
case 'maxItems':
suggestedValue = _.slice(targetActualValue, 0, ajvValidationErrorObj.params.limit);
break;
case 'uniqueItems':
tempSuggestedValue = _.cloneDeep(targetActualValue);
tempSuggestedValue[ajvValidationErrorObj.params.j] = _.last(targetFakedValue);
suggestedValue = tempSuggestedValue;
break;
// Keywords: minLength, maxLength, format, minimum, maximum, type, multipleOf, pattern
default:
suggestedValue = getPathValue(fakedValue, dataPath, null);
break;
}
return suggestedValue;
}
/**
* Tests whether given parameter is of complex array type from param key
*
* @param {*} paramKey - Parmaeter key that is to be tested
* @returns {Boolean} - result
*/
function isParamComplexArray (paramKey) {
// this checks if parameter key numbered element (i.e. itemArray[1] is complex array param)
let regex = /\[[\d]+\]/gm;
return regex.test(paramKey);
}
/**
* 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
*/
function 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
*/
function 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;
}
/**
* Finds valid JSON media type object from content object
*
* @param {*} contentObj - Content Object from schema
* @returns {*} - valid JSON media type if exists
*/
function getJsonContentType (contentObj) {
let jsonContentType = _.find(_.keys(contentObj), (contentType) => {
let mediaType = parseMediaType(contentType);
return mediaType.type === 'application' && (
mediaType.subtype === 'json' || _.endsWith(mediaType.subtype, '+json')
);
});
return jsonContentType;
}
/**
* Gives mismtach for content type header for request/response
*
* @param {Array} headers - Transaction Headers
* @param {String} transactionPathPrefix - Transaction Path to headers
* @param {String} schemaPathPrefix - Schema path to content object
* @param {Object} contentObj - Corresponding Schema content object
* @param {String} mismatchProperty - Mismatch property (HEADER / RESPONSE_HEADER)
* @param {*} options - OAS options, check lib/options.js for more
* @returns {Array} found mismatch objects
*/
function checkContentTypeHeader (headers, transactionPathPrefix, schemaPathPrefix, contentObj,
mismatchProperty, options) {
let mediaTypes = [],
contentHeader,
contentHeaderIndex,
contentHeaderMediaType,
suggestedContentHeader,
hasComputedType,
humanPropName = mismatchProperty === 'HEADER' ? 'header' : 'response header',
mismatches = [];
// get all media types present in content object
_.forEach(_.keys(contentObj), (contentType) => {
let contentMediaType = parseMediaType(contentType);
mediaTypes.push({
type: contentMediaType.type,
subtype: contentMediaType.subtype,
contentType: contentMediaType.type + '/' + contentMediaType.subtype
});
});
// prefer JSON > XML > Other media types for suggested header.
_.forEach(mediaTypes, (mediaType) => {
let headerFamily = getHeaderFamily(mediaType.contentType);
if (headerFamily !== HEADER_TYPE.INVALID) {
suggestedContentHeader = mediaType.contentType;
hasComputedType = true;
if (headerFamily === HEADER_TYPE.JSON) {
return false;
}
}
});
// if no JSON or XML, take whatever we have
if (!hasComputedType && mediaTypes.length > 0) {
suggestedContentHeader = mediaTypes[0].contentType;
hasComputedType = true;
}
// get content-type header and info
_.forEach(headers, (header, index) => {
if (_.toLower(header.key) === 'content-type') {
let mediaType = parseMediaType(header.value);
contentHeader = header;
contentHeaderIndex = index;
contentHeaderMediaType = mediaType.type + '/' + mediaType.subtype;
return false;
}
});
// Schema body content has no media type objects
if (!_.isEmpty(contentHeader) && _.isEmpty(mediaTypes)) {
// ignore mismatch for default header (text/plain) added by conversion
if (options.showMissingInSchemaErrors && _.toLower(contentHeaderMediaType) !== TEXT_PLAIN) {
mismatches.push({
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix + `[${contentHeaderIndex}]`,
schemaJsonPath: null,
reasonCode: 'MISSING_IN_SCHEMA',
// Reason for missing in schema suggests that certain media type in req/res body is not present
reason: `The ${mismatchProperty === 'HEADER' ? 'request' : 'response'} body should have media type` +
` "${contentHeaderMediaType}"`
});
}
}
// No request/response content-type header
else if (_.isEmpty(contentHeader) && !_.isEmpty(mediaTypes)) {
let mismatchObj = {
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix,
schemaJsonPath: schemaPathPrefix,
reasonCode: 'MISSING_IN_REQUEST',
reason: `The ${humanPropName} "Content-Type" was not found in the transaction`
};
if (options.suggestAvailableFixes) {
mismatchObj.suggestedFix = {
key: 'Content-Type',
actualValue: null,
suggestedValue: {
key: 'Content-Type',
value: suggestedContentHeader
}
};
}
mismatches.push(mismatchObj);
}
// Invalid type of header found
else if (!_.isEmpty(contentHeader)) {
let mismatchObj,
matched = false;
// wildcard header matching
_.forEach(mediaTypes, (mediaType) => {
let transactionHeader = _.split(contentHeaderMediaType, '/'),
headerTypeMatched = (mediaType.type === '*' || mediaType.type === transactionHeader[0]),
headerSubtypeMatched = (mediaType.subtype === '*' || mediaType.subtype === transactionHeader[1]);
if (headerTypeMatched && headerSubtypeMatched) {
matched = true;
}
});
if (!matched) {
mismatchObj = {
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix + `[${contentHeaderIndex}].value`,
schemaJsonPath: schemaPathPrefix,
reasonCode: 'INVALID_TYPE',
reason: `The ${humanPropName} "Content-Type" needs to be "${suggestedContentHeader}",` +
` but we found "${contentHeaderMediaType}" instead`
};
if (options.suggestAvailableFixes) {
mismatchObj.suggestedFix = {
key: 'Content-Type',
actualValue: contentHeader.value,
suggestedValue: suggestedContentHeader
};
}
mismatches.push(mismatchObj);
}
}
return mismatches;
}
/**
* Generates appropriate collection element based on parameter location
*
* @param {Object} param - Parameter object habing key, value and description (optional)
* @param {String} location - Parameter location ("in" property of OAS defined parameter object)
* @returns {Object} - SDK element
*/
function generateSdkParam (param, location) {
const sdkElementMap = {
'query': QueryParam,
'header': Header,
'path': Variable
};
let generatedParam = {
key: param.key,
value: param.value
};
_.has(param, 'disabled') && (generatedParam.disabled = param.disabled);
// use appropriate sdk element based on location parmaeter is in for param generation
if (sdkElementMap[location]) {
generatedParam = new sdkElementMap[location](generatedParam);
}
param.description && (generatedParam.description = param.description);
return generatedParam;
}
/**
* Recursively extracts key-value pair from deep objects.
*
* @param {*} deepObject - Deep object
* @param {*} objectKey - key associated with deep object
* @returns {Array} array of param key-value pairs
*/
function extractDeepObjectParams (deepObject, objectKey) {
let extractedParams = [];
Object.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;
}
/**
* Returns an array of parameters
* Handles array/object/string param types
* @param {*} param - the param object, as defined in
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
* @param {any} paramValue - the value to use (from schema or example) for the given param.
* This will be exploded/parsed according to the param type
* @param {*} parameterSource — Specifies whether the schema being faked is from a request or response.
* @param {object} components - components defined in the OAS spec. These are used to
* resolve references while generating params.
* @param {object} schemaCache - object storing schemaFaker and schemaResolution caches
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
* @returns {array} parameters. One param with type=array might lead to multiple params
* in the return value
* The styles are documented at
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values
*/
function convertParamsWithStyle (param, paramValue, parameterSource, components, schemaCache, options) {
var paramName = _.get(param, 'name'),
pmParams = [],
serialisedValue = '',
description = getParameterDescription(param),
disabled = false;
// for invalid param object return null
if (!_.isObject(param)) {
return null;
}
let { style, explode, startValue, propSeparator, keyValueSeparator, isExplodable } =
getParamSerialisationInfo(param, parameterSource, components, options);
if (options && !options.enableOptionalParameters) {
disabled = !param.required;
}
// decide explodable params, starting value and separators between key-value and properties for serialisation
switch (style) {
case 'form':
if (explode && _.isObject(paramValue)) {
_.forEach(paramValue, (value, key) => {
pmParams.push(generateSdkParam({
key: _.isArray(paramValue) ? paramName : key,
value: (value === undefined ? '' : value),
description,
disabled
}, _.get(param, 'in')));
});
return pmParams;
}
// handle free-form parameter correctly
if (explode && (_.get(param, 'schema.type') === 'object') && _.isEmpty(_.get(param, 'schema.properties'))) {
return pmParams;
}
break;
case 'deepObject':
if (_.isObject(paramValue)) {
let extractedParams = extractDeepObjectParams(paramValue, paramName);
_.forEach(extractedParams, (extractedParam) => {
pmParams.push(generateSdkParam({
key: extractedParam.key,
value: extractedParam.value || '',
description,
disabled
}, _.get(param, 'in')));
});
return pmParams;
}
break;
default:
break;
}
// for array and object, serialize value
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 ? '' : 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(generateSdkParam({
key: paramName,
value: serialisedValue,
description,
disabled
}, _.get(param, 'in')));
return pmParams;
}
/**
* Converts the necessary server variables to the
* something that can be added to the collection
* TODO: Figure out better description
* @param {object} serverVariables - Object containing the server variables at the root/path-item level
* @param {string} keyName - an additional key to add the serverUrl to the variable list
* @param {string} serverUrl - URL from the server object
* @returns {object} modified collection after the addition of the server variables
*/
function convertToPmCollectionVariables (serverVariables, keyName, serverUrl = '') {
var variables = [];
if (serverVariables) {
_.forOwn(serverVariables, (value, key) => {
let description = getParameterDescription(value);
variables.push(new Variable({
key: key,
value: value.default || '',
description: description
}));
});
}
if (keyName) {
variables.push(new Variable({
key: keyName,
value: serverUrl,
type: 'string'
}));
}
return variables;
}
/**
* Returns params applied to specific operation with resolved references. Params from parent
* blocks (collection/folder) are merged, so that the request has a flattened list of params needed.
* OperationParams take precedence over pathParams
*
* @param {Object} context - Required context from related SchemaPack function
* @param {array} operationParam operation (Postman request)-level params.
* @param {array} pathParam are path parent-level params.
* @returns {*} combined requestParams from operation and path params.
*/
function getRequestParams (context, operationParam, pathParam) {
if (!Array.isArray(operationParam)) {
operationParam = [];
}
if (!Array.isArray(pathParam)) {
pathParam = [];
}
pathParam.forEach((param, index, arr) => {
if (_.has(param, '$ref')) {
arr[index] = resolveRefFromSchema(context, param.$ref);
}
});
operationParam.forEach((param, index, arr) => {
if (_.has(param, '$ref')) {
arr[index] = resolveRefFromSchema(context, param.$ref);
}
});
if (_.isEmpty(pathParam)) {
return operationParam;
}
else if (_.isEmpty(operationParam)) {
return pathParam;
}
// If both path and operation params exist,
// we need to de-duplicate
// A param with the same name and 'in' value from operationParam
// will get precedence
var reqParam = operationParam.slice();
pathParam.forEach((param) => {
var dupParam = operationParam.find(function(element) {
return element.name === param.name && element.in === param.in &&
// the below two conditions because undefined === undefined returns true
element.name && param.name &&
element.in && param.in;
});
if (!dupParam) {
// if there's no duplicate param in operationParam,
// use the one from the common pathParam list
// this ensures that operationParam is given precedence
reqParam.push(param);
}
});
return reqParam;
}
// TODO: document / comment properly all cases
/**
* Resolves schema for form params such that each individual request body param can be validated
* to corresponding resolved schema params
*
* @param {*} schema - Schema object for corresponding form params
* @param {*} schemaKey - Key for corresponding Schema object to be resolved
* @param {*} encodingObj - OAS Encoding object
* @param {*} requestParams - collection request parameters
* @param {*} metaInfo - meta information of param (i.e. required)
* @param {object} components - components defined in the OAS spec. These are used to
* resolve references while generating params.
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
* @param {Boolean} shouldIterateChildren - Defines whether to iterate over children further for type object children
* @return {Array} Resolved form schema params
*/
function resolveFormParamSchema (schema, schemaKey, encodingObj, requestParams, metaInfo, components, options,
shouldIterateChildren) {
let resolvedSchemaParams = [],
resolvedProp,
encodingValue,
pSerialisationInfo,
isPropSeparable;
if (_.isArray(schema.anyOf) || _.isArray(schema.oneOf)) {
_.forEach(schema.anyOf || schema.oneOf, (schemaElement) => {
// As for such schemas there can be multiple choices, keep them as non required
resolvedSchemaParams = _.concat(resolvedSchemaParams, resolveFormParamSchema(schemaElement, schemaKey,
encodingObj, requestParams, _.assign(metaInfo, { required: false, isComposite: true }),
components, options, shouldIterateChildren));
});
return resolvedSchemaParams;
}
resolvedProp = {
name: schemaKey,
schema: schema,
required: _.get(metaInfo, 'required'),
in: 'query', // serialization follows same behaviour as query params
description: _.get(schema, 'description', _.get(metaInfo, 'description', '')),
pathPrefix: _.get(metaInfo, 'pathPrefix'),
isComposite: _.get(metaInfo, 'isComposite', false),
deprecated: _.get(schema, 'deprecated', _.get(metaInfo, 'deprecated'))
};
encodingValue = _.get(encodingObj, schemaKey);
if (_.isObject(encodingValue)) {
_.has(encodingValue, 'style') && (resolvedProp.style = encodingValue.style);
_.has(encodingValue, 'explode') && (resolvedProp.explode = encodingValue.explode);
}
pSerialisationInfo = getParamSerialisationInfo(resolvedProp, PARAMETER_SOURCE.REQUEST,
components, options);
isPropSeparable = _.includes(['form', 'deepObject'], pSerialisationInfo.style);
/**
* schema apart from type object should be only resolved if corresponding schema key is present.
* i.e. As URL encoded body requires key-value pair, non key schemas should not be resolved.
*/
if (!isPropSeparable || (!_.includes(['object', 'array'], _.get(schema, 'type')) && !_.isEmpty(schemaKey)) ||
(_.isEmpty(schemaKey) && _.get(schema, 'type') === 'array') || !pSerialisationInfo.explode) {
resolvedSchemaParams.push(resolvedProp);
}
else if (_.get(schema, 'type') === 'array' && pSerialisationInfo.style === 'form' &&
pSerialisationInfo.explode) {
resolvedProp.schema = _.get(schema, 'items', {});
resolvedSchemaParams.push(resolvedProp);
}
else {
// resolve each property as separate param similar to query params
_.forEach(_.get(schema, 'properties'), (propSchema, propName) => {
let resolvedPropName = _.isEmpty(schemaKey) ? propName : `${schemaKey}[${propName}]`,
resolvedProp = {
name: resolvedPropName,
schema: propSchema,
in: 'query', // serialization follows same behaviour as query params
description: _.get(propSchema, 'description') || _.get(metaInfo, 'description') || '',
required: _.get(metaInfo, 'required'),
isComposite: _.get(metaInfo, 'isComposite', false),
deprecated: _.get(propSchema, 'deprecated') || _.get(metaInfo, 'deprecated')
},
parentPropName = resolvedPropName.indexOf('[') === -1 ? resolvedPropName :
resolvedPropName.slice(0, resolvedPropName.indexOf('[')),
encodingValue = _.get(encodingObj, parentPropName),
pSerialisationInfo,
isPropSeparable;
if (_.isObject(encodingValue)) {
_.has(encodingValue, 'style') && (resolvedProp.style = encodingValue.style);
_.has(encodingValue, 'explode') && (resolvedProp.explode = encodingValue.explode);
}
if (_.isUndefined(metaInfo.required) && _.includes(_.get(schema, 'required'), propName)) {
resolvedProp.required = true;
}
pSerialisationInfo = getParamSerialisationInfo(resolvedProp, PARAMETER_SOURCE.REQUEST,
components, options);
isPropSeparable = _.includes(['form', 'deepObject'], pSerialisationInfo.style);
if (_.isArray(propSchema.anyOf) || _.isArray(propSchema.oneOf)) {
_.forEach(propSchema.anyOf || propSchema.oneOf, (schemaElement) => {
let nextSchemaKey = _.isEmpty(schemaKey) ? propName : `${schemaKey}[${propName}]`;
resolvedSchemaParams = _.concat(resolvedSchemaParams, resolveFormParamSchema(schemaElement, nextSchemaKey,
encodingObj, requestParams, _.assign(metaInfo, { required: false, isComposite: true }),
components, options, pSerialisationInfo.style === 'deepObject'));
});
return resolvedSchemaParams;
}
if (isPropSeparable && propSchema.type === 'array' && pSerialisationInfo.explode) {
/**
* avoid validation of complex array type param as OAS doesn't define serialisation
* of Array with deepObject style
*/
if (pSerialisationInfo.style !== 'deepObject' &&
!_.includes(['array', 'object'], _.get(propSchema, 'items.type'))) {
// add schema of corresponding items instead array
resolvedSchemaParams.push(_.assign({}, resolvedProp, {
schema: _.get(propSchema, 'items'),
isResolvedParam: true
}));
}
}
else if (isPropSeparable && propSchema.type === 'object' && pSerialisationInfo.explode) {
let localMetaInfo = _.isEmpty(metaInfo) ? (metaInfo = {
required: resolvedProp.required,
description: resolvedProp.description,
deprecated: _.get(resolvedProp, 'deprecated')
}) : metaInfo,
nextSchemaKey = _.isEmpty(schemaKey) ? propName : `${schemaKey}[${propName}]`;
// resolve all child params of parent param with deepObject style
if (pSerialisationInfo.style === 'deepObject') {
resolvedSchemaParams = _.concat(resolvedSchemaParams, resolveFormParamSchema(propSchema, nextSchemaKey,
encodingObj, requestParams, localMetaInfo, components, options, true));
}
else {
// add schema of all properties instead entire object
_.forEach(_.get(propSchema, 'properties', {}), (value, key) => {
resolvedSchemaParams.push({
name: key,
schema: value,
isResolvedParam: true,
required: resolvedProp.required,
description: resolvedProp.description,
isComposite: _.get(metaInfo, 'isComposite', false),
deprecated: _.get(resolvedProp, 'deprecated') || _.get(metaInfo, 'deprecated')
});
});
}
}
else {
resolvedSchemaParams.push(resolvedProp);
}
});
// Resolve additionalProperties via first finding additionalProper
if (_.isObject(_.get(schema, 'additionalProperties'))) {
const additionalPropSchema = _.get(schema, 'additionalProperties'),
matchingRequestParamKeys = [];
/**
* Find matching keys from request param as additional props can be unknown keys.
* and these unknown key names are not mentioned in schema
*/
_.forEach(requestParams, ({ key }) => {
if (_.isString(key) && schemaKey === '') {
const isParamResolved = _.some(resolvedSchemaParams, (param) => {
return key === param.key;
});
!isParamResolved && (matchingRequestParamKeys.push(key));
}
else if (_.isString(key) && _.startsWith(key, schemaKey + '[') && _.endsWith(key, ']')) {
const childKey = key.substring(key.indexOf(schemaKey + '[') + schemaKey.length + 1, key.length - 1);
if (!_.includes(childKey, '[')) {
matchingRequestParamKeys.push(key);
}
else {
matchingRequestParamKeys.push(schemaKey + '[' + childKey.slice(0, childKey.indexOf('[')));
}
}
});
// For every matched request param key add a child param schema that can be validated further
_.forEach(matchingRequestParamKeys, (matchedRequestParamKey) => {
if (_.get(additionalPropSchema, 'type') === 'object' && shouldIterateChildren) {
resolvedSchemaParams = _.concat(resolvedSchemaParams, resolveFormParamSchema(additionalPropSchema,
matchedRequestParamKey, encodingObj, requestParams, metaInfo, components, options, shouldIterateChildren));
}
// Avoid adding invalid array child params, As deepObject style should only contain object or simple types
else if (_.get(additionalPropSchema, 'type') !== 'array') {
resolvedSchemaParams.push({
name: matchedRequestParamKey,
schema: additionalPropSchema,
description: _.get(additionalPropSchema, 'description') || _.get(metaInfo, 'description') || '',
required: false,
isResolvedParam: true,
isComposite: true,
deprecated: _.get(additionalPropSchema, 'deprecated') || _.get(metaInfo, 'deprecated')
});
}
});
}
}
return resolvedSchemaParams;
}
/**
*
* @param {Object} context - Required context from related SchemaPack function
* @param {String} property - one of QUERYPARAM, PATHVARIABLE, HEADER, BODY, RESPONSE_HEADER, RESPONSE_BODY
* @param {String} jsonPathPrefix - this will be prepended to all JSON schema paths on the request
* @param {String} txnParamName - Optional - The name of the param being validated (useful for query params,
* req headers, res headers)
* @param {*} value - the value of the property in the request
* @param {String} schemaPathPrefix - this will be prepended to all JSON schema paths on the schema
* @param {Object} openApiSchemaObj - The OpenAPI schema object against which to validate
* @param {String} parameterSourceOption tells that the schema object is of request or response
* @param {Object} components - Components in the spec that the schema might refer to
* @param {Object} options - Global options
* @param {Object} schemaCache object storing schemaFaker and schemaResolution caches
* @param {string} jsonSchemaDialect The schema dialect defined in the OAS object
* @param {Function} callback - For return
* @returns {Array} array of mismatches
*/
function checkValueAgainstSchema (context, property, jsonPathPrefix, txnParamName, value, schemaPathPrefix,
openApiSchemaObj, parameterSourceOption, components, options, schemaCache, jsonSchemaDialect, callback) {
let mismatches = [],
jsonValue,
humanPropName = propNames[property],
needJsonMatching = (property === 'BODY' || property === 'RESPONSE_BODY'),
invalidJson = false,
valueToUse = value,
schema = resolveSchema(context, openApiSchemaObj, {
resolveFor: PROCESSING_TYPE.VALIDATION,
isResponseSchema: parameterSourceOption === PARAMETER_SOURCE.RESPONSE
}),
compositeSchema = schema.oneOf || schema.anyOf,
compareTypes = _.get(context, 'concreteUtils.compareTypes') || concreteUtils.compareTypes;
if (needJsonMatching) {
try {
j