Files
fastapi-openapi-to-postman/lib/schemaUtils.js
Luis Tejeda ad78625271 Add support for url with fragment and servers
Add support for url with fragment and serversAdd support for url with fragment and serversAdd support for url with fragment and servers
2022-07-13 14:39:23 -05:00

5059 lines
190 KiB
JavaScript

/**
* This file contains util functions that need OAS-awareness
* utils.js contains other util functions
*/
const { ParseError } = require('./common/ParseError.js');
const { formatDataPath, checkIsCorrectType, isKnownType } = require('./common/schemaUtilsCommon.js'),
{ getConcreteSchemaUtils, isSwagger, validateSupportedVersion } = require('./common/versionUtils.js'),
async = require('async'),
sdk = require('postman-collection'),
schemaFaker = require('../assets/json-schema-faker.js'),
deref = require('./deref.js'),
_ = require('lodash'),
xmlFaker = require('./xmlSchemaFaker.js'),
openApiErr = require('./error.js'),
ajvValidationError = require('./ajValidation/ajvValidationError'),
utils = require('./utils.js'),
defaultOptions = require('../lib/options.js').getOptions('use'),
{ Node, Trie } = require('./trie.js'),
{ validateSchema } = require('./ajValidation/ajvValidation'),
inputValidation = require('./30XUtils/inputValidation'),
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',
APP_JSON = 'application/json',
APP_JS = 'application/javascript',
TEXT_XML = 'text/xml',
TEXT_PLAIN = 'text/plain',
TEXT_HTML = 'text/html',
FORM_DATA = 'multipart/form-data',
REQUEST_TYPE = {
EXAMPLE: 'EXAMPLE',
ROOT: 'ROOT'
},
PARAMETER_SOURCE = {
REQUEST: 'REQUEST',
RESPONSE: 'RESPONSE'
},
HEADER_TYPE = {
JSON: 'json',
XML: 'xml',
INVALID: 'invalid'
},
PREVIEW_LANGUAGE = {
JSON: 'json',
XML: 'xml',
TEXT: 'text',
HTML: 'html'
},
authMap = {
basicAuth: 'basic',
bearerAuth: 'bearer',
digestAuth: 'digest',
hawkAuth: 'hawk',
oAuth1: 'oauth1',
oAuth2: 'oauth2',
ntlmAuth: 'ntlm',
awsSigV4: 'awsv4',
normal: null
},
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'
],
crypto = require('crypto'),
DEFAULT_SCHEMA_UTILS = require('./30XUtils/schemaUtils30X'),
{ getRelatedFiles } = require('./relatedFiles'),
{ compareVersion } = require('./common/versionUtils.js'),
parse = require('./parse'),
{ getBundleContentAndComponents, parseFileOrThrow } = require('./bundle.js'),
MULTI_FILE_API_TYPE_ALLOWED_VALUE = 'multiFile';
/* eslint-enable */
// 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.
});
/**
*
* @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');
}
/**
* Safe wrapper for schemaFaker that resolves references and
* removes things that might make schemaFaker crash
* @param {*} oldSchema the schema to fake
* @param {string} resolveTo The desired JSON-generation mechanism (schema: prefer using the JSONschema to
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 {string} indentCharacter char for 1 unit of indentation
* @param {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @param {object} stackLimit - The depth to which schema resolution will happen for nested refs
* @returns {object} fakedObject
*/
function safeSchemaFaker(oldSchema, resolveTo, resolveFor, parameterSourceOption, components,
schemaFormat, indentCharacter, schemaCache, stackLimit) {
var prop, key, resolvedSchema, fakedSchema,
schemaResolutionCache = _.get(schemaCache, 'schemaResolutionCache', {}),
schemaFakerCache = _.get(schemaCache, 'schemaFakerCache', {});
let concreteUtils = components && components.hasOwnProperty('concreteUtils') ?
components.concreteUtils :
DEFAULT_SCHEMA_UTILS;
resolvedSchema = deref.resolveRefs(oldSchema, parameterSourceOption, components, schemaResolutionCache,
resolveFor, resolveTo, 0, {}, stackLimit);
resolvedSchema = concreteUtils.fixExamplesByVersion(resolvedSchema);
key = JSON.stringify(resolvedSchema);
if (resolveTo === 'schema') {
key = 'resolveToSchema ' + key;
schemaFaker.option({
useExamplesValue: false
});
}
else if (resolveTo === 'example') {
key = 'resolveToExample ' + key;
schemaFaker.option({
useExamplesValue: true
});
}
if (resolveFor === PROCESSING_TYPE.VALIDATION) {
schemaFaker.option({
useDefaultValue: false,
avoidExampleItemsLength: false
});
}
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);
schemaFakerCache[key] = fakedSchema;
return fakedSchema;
}
catch (e) {
console.warn(
'Error faking a schema. Not faking this schema. Schema:', resolvedSchema,
'Error', e
);
return null;
}
}
module.exports = {
safeSchemaFaker: safeSchemaFaker,
/** Analyzes the spec to determine the size of the spec,
* number of request that will be generated out this spec, and
* number or references present in the spec.
*
* @param {Object} spec JSON
* @return {Object} returns number of requests that will be generated,
* number of refs present and size of the spec.
*/
analyzeSpec: function (spec) {
var size,
numberOfRefs = 0,
specString,
numberOfRequests = 0;
// Stringify and add whitespaces as there would be in a normal file
// To get accurate disk size
specString = JSON.stringify(spec);
// Size in MB
size = Buffer.byteLength(specString, 'utf8') / (1024 * 1024);
// No need to check for number of requests or refs if the size is greater than 8 MB
// The complexity is 10.
if (size < 8) {
// Finds the number of requests that would be generated from this spec
if (spec.paths) {
Object.values(spec.paths).forEach((value) => {
Object.keys(value).forEach((key) => {
if (METHODS.includes(key)) {
numberOfRequests++;
}
});
});
}
// Number of times the term $ref is repeated in the spec.
numberOfRefs = (specString.match(/\$ref/g) || []).length;
}
return {
size,
numberOfRefs,
numberOfRequests
};
},
/** Determines the complexity score and stackLimit
*
* @param {Object} analysis the object returned by analyzeSpec function
* @param {Object} options Current options
*
* @returns {Object} computedOptions - contains two new options i.e. stackLimit and complexity score
*/
determineOptions: function (analysis, options) {
let size = analysis.size,
numberOfRefs = analysis.numberOfRefs,
numberOfRequests = analysis.numberOfRequests;
var computedOptions = _.clone(options);
computedOptions.stackLimit = 10;
// This is the score that is given to each spec on the basis of the
// number of references present in spec and the number of requests that will be generated.
// This ranges from 0-10.
computedOptions.complexityScore = 0;
// Anything above the size of 8MB will be considered a big spec and given the
// least stack limit and the highest complexity score.
if (size >= 8) {
console.warn('Complexity score = 10');
computedOptions.stackLimit = 2;
computedOptions.complexityScore = 10;
return computedOptions;
}
else if (size >= 5 || numberOfRequests > 1500 || numberOfRefs > 1500) {
computedOptions.stackLimit = 3;
computedOptions.complexityScore = 9;
return computedOptions;
}
else if (size >= 1 && (numberOfRequests > 1000 || numberOfRefs > 1000)) {
computedOptions.stackLimit = 5;
computedOptions.complexityScore = 8;
return computedOptions;
}
else if (numberOfRefs > 500 || numberOfRequests > 500) {
computedOptions.stackLimit = 6;
computedOptions.complexityScore = 6;
return computedOptions;
}
return computedOptions;
},
/**
* Changes the {} around scheme and path variables to :variable
* @param {string} url - the url string
* @returns {string} string after replacing /{pet}/ with /:pet/
*/
fixPathVariablesInUrl: function (url) {
// All complicated logic removed
// This simply replaces all instances of {text} with {{text}}
// text cannot have any of these 3 chars: /{}
// {{text}} will not be converted
let replacer = function (match, p1, offset, string) {
if (string[offset - 1] === '{' && string[offset + match.length + 1] !== '}') {
return match;
}
return '{' + p1 + '}';
};
return url.replace(/(\{[^\/\{\}]+\})/g, replacer);
},
/**
* Changes path structure that contains {var} to :var and '/' to '_'
* This is done so generated collection variable is in correct format
* i.e. variable '{{item/{itemId}}}' is considered separates variable in URL by collection sdk
* @param {string} path - path defined in openapi spec
* @returns {string} - string after replacing {itemId} with :itemId
*/
fixPathVariableName: function (path) {
// Replaces structure like 'item/{itemId}' into 'item-itemId-Url'
return path.replace(/\//g, '-').replace(/[{}]/g, '') + '-Url';
},
/**
* Returns a description that's usable at the collection-level
* Adds the collection description and uses any relevant contact info
* @param {*} openapi The JSON representation of the OAS spec
* @returns {string} description
*/
getCollectionDescription: function (openapi) {
let description = _.get(openapi, 'info.description', '');
if (_.get(openapi, 'info.contact')) {
let contact = [];
if (openapi.info.contact.name) {
contact.push(' Name: ' + openapi.info.contact.name);
}
if (openapi.info.contact.email) {
contact.push(' Email: ' + openapi.info.contact.email);
}
if (contact.length > 0) {
// why to add unnecessary lines if there is no description
if (description !== '') {
description += '\n\n';
}
description += 'Contact Support:\n' + contact.join('\n');
}
}
return description;
},
/**
* Get the format of content type header
* @param {string} cTypeHeader - the content type header string
* @returns {string} type of content type header
*/
getHeaderFamily: function(cTypeHeader) {
let mediaType = this.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 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: function(parameter) {
if (!_.isObject(parameter)) {
return '';
}
return (parameter.required ? '(Required) ' : '') + (parameter.description || '') +
(parameter.enum ? ' (This can only be one of ' + parameter.enum + ')' : '');
},
/**
* Given parameter objects, it assigns example/examples of parameter object as schema example.
*
* @param {Object} parameter - parameter object
* @returns {null} - null
*/
assignParameterExamples: function (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));
}
},
/**
* Converts the neccessary 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
*/
convertToPmCollectionVariables: function(serverVariables, keyName, serverUrl = '') {
var variables = [];
if (serverVariables) {
_.forOwn(serverVariables, (value, key) => {
let description = this.getParameterDescription(value);
variables.push(new sdk.Variable({
key: key,
value: value.default || '',
description: description
}));
});
}
if (keyName) {
variables.push(new sdk.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 {array} operationParam operation (Postman request)-level params.
* @param {array} pathParam are path parent-level params.
* @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.
* @returns {*} combined requestParams from operation and path params.
*/
getRequestParams: function(operationParam, pathParam, components, options) {
options = _.merge({}, defaultOptions, options);
if (!Array.isArray(operationParam)) {
operationParam = [];
}
if (!Array.isArray(pathParam)) {
pathParam = [];
}
pathParam.forEach((param, index, arr) => {
if (_.has(param, '$ref')) {
arr[index] = this.getRefObject(param.$ref, components, options);
}
});
operationParam.forEach((param, index, arr) => {
if (_.has(param, '$ref')) {
arr[index] = this.getRefObject(param.$ref, components, options);
}
});
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;
},
/**
* Generates a Trie-like folder structure from the root path object of the OpenAPI specification.
* @param {Object} spec - specification in json format
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
* @param {boolean} fromWebhooks - true when we are creating the webhooks group trie - default: false
* @returns {Object} - The final object consists of the tree structure
*/
generateTrieFromPaths: function (spec, options, fromWebhooks = false) {
options = _.merge({}, defaultOptions, options);
let concreteUtils = getConcreteSchemaUtils({ type: 'json', data: spec }),
specComponentsAndUtils = {
concreteUtils
};
var paths = fromWebhooks ? spec.webhooks : spec.paths, // the first level of paths
currentPath = '',
currentPathObject = '',
commonParams = '',
collectionVariables = {},
operationItem,
pathLevelServers = '',
pathLength,
currentPathRequestCount,
currentNode,
i,
summary,
path,
pathMethods = [],
// creating a root node for the trie (serves as the root dir)
trie = new Trie(new Node({
name: '/'
})),
// returns a list of methods supported at each pathItem
// some pathItem props are not methods
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject
getPathMethods = function(pathKeys) {
var methods = [];
// TODO: Show warning for incorrect schema if !pathKeys
pathKeys && pathKeys.forEach(function(element) {
if (METHODS.includes(element)) {
methods.push(element);
}
});
return methods;
};
Object.assign(specComponentsAndUtils, concreteUtils.getRequiredData(spec));
for (path in paths) {
if (paths.hasOwnProperty(path)) {
currentPathObject = paths[path];
// discard the leading slash, if it exists
if (path[0] === '/') {
path = path.substring(1);
}
if (fromWebhooks) {
// When we work with webhooks the path value corresponds to the webhook's name
// and it won't be treated as path
currentPath = path === '' ? ['(root)'] : [path];
}
else {
// split the path into indiv. segments for trie generation
// unless path it the root endpoint
currentPath = path === '' ? ['(root)'] : path.split('/').filter((pathItem) => {
// remove any empty pathItems that might have cropped in
// due to trailing or double '/' characters
return pathItem !== '';
});
}
pathLength = currentPath.length;
// get method names available for this path
pathMethods = getPathMethods(Object.keys(currentPathObject));
// the number of requests under this node
currentPathRequestCount = pathMethods.length;
currentNode = trie.root;
// adding children for the nodes in the trie
// start at the top-level and do a DFS
for (i = 0; i < pathLength; i++) {
if (!currentNode.children[currentPath[i]]) {
// if the currentPath doesn't already exist at this node,
// add it as a folder
currentNode.addChildren(currentPath[i], new Node({
name: currentPath[i],
requestCount: 0,
requests: [],
children: {},
type: 'item-group',
childCount: 0
}));
// We are keeping the count children in a folder which can be a request or folder
// For ex- In case of /pets/a/b, pets has 1 childCount (i.e a)
currentNode.childCount += 1;
}
// requestCount increment for the node we just added
currentNode.children[currentPath[i]].requestCount += currentPathRequestCount;
currentNode = currentNode.children[currentPath[i]];
}
// extracting common parameters for all the methods in the current path item
if (currentPathObject.hasOwnProperty('parameters')) {
commonParams = currentPathObject.parameters;
}
// storing common path/collection vars from the server object at the path item level
if (currentPathObject.hasOwnProperty('servers')) {
pathLevelServers = currentPathObject.servers;
collectionVariables[this.fixPathVariableName(path)] = pathLevelServers[0];
delete currentPathObject.servers;
}
// add methods to node
// eslint-disable-next-line no-loop-func
_.each(pathMethods, (method) => {
// base operationItem
operationItem = currentPathObject[method] || {};
// params - these contain path/header/body params
operationItem.parameters = this.getRequestParams(operationItem.parameters, commonParams,
specComponentsAndUtils, options);
summary = operationItem.summary || operationItem.description;
currentNode.addMethod({
name: summary,
method: method,
path: path,
properties: operationItem,
type: 'item',
servers: pathLevelServers || undefined
});
currentNode.childCount += 1;
});
pathLevelServers = undefined;
commonParams = [];
}
}
return {
tree: trie,
variables: collectionVariables // server variables that are to be converted into collection variables.
};
},
addCollectionItemsFromWebhooks: function(spec, generatedStore, components, options, schemaCache) {
let webhooksObj = this.generateTrieFromPaths(spec, options, true),
webhooksTree = webhooksObj.tree,
webhooksFolder = new sdk.ItemGroup({ name: 'Webhooks' }),
variableStore = {},
webhooksVariables = [];
if (Object.keys(webhooksTree.root.children).length === 0) {
return;
}
for (let child in webhooksTree.root.children) {
if (
webhooksTree.root.children.hasOwnProperty(child) &&
webhooksTree.root.children[child].requestCount > 0
) {
webhooksVariables.push(new sdk.Variable({
key: this.cleanWebhookName(child),
value: '/',
type: 'string'
}));
webhooksFolder.items.add(
this.convertChildToItemGroup(
spec,
webhooksTree.root.children[child],
components,
options,
schemaCache,
variableStore,
true
),
);
}
}
generatedStore.collection.items.add(webhooksFolder);
webhooksVariables.forEach((variable) => {
generatedStore.collection.variables.add(variable);
});
},
/**
* Adds Postman Collection Items using paths.
* Folders are grouped based on trie that's generated using all paths.
*
* @param {object} spec - openAPI spec object
* @param {object} generatedStore - the store that holds the generated collection. Modified in-place
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {void} - generatedStore is modified in-place
*/
addCollectionItemsUsingPaths: function (spec, generatedStore, components, options, schemaCache) {
var folderTree,
folderObj,
child,
key,
variableStore = {};
/**
We need a trie because the decision of whether or not a node
is a folder or request can only be made once the whole trie is generated
This has a .trie and a .variables prop
*/
folderObj = this.generateTrieFromPaths(spec, options);
folderTree = folderObj.tree;
/*
* these are variables identified at the collection level
* they need to be added explicitly to collection variables
* deeper down in the trie, variables will be added directly to folders
* If the folderObj.variables have their own variables, we add
* them to the collectionVars
*/
if (folderObj.variables) {
_.forOwn(folderObj.variables, (server, key) => {
// TODO: Figure out what this does
this.convertToPmCollectionVariables(
server.variables, // these are path variables in the server block
key, // the name of the variable
this.fixPathVariablesInUrl(server.url)
).forEach((element) => {
generatedStore.collection.variables.add(element);
});
});
}
// Adds items from the trie into the collection that's in the store
for (child in folderTree.root.children) {
// A Postman request or folder is added if atleast one request is present in that sub-child's tree
// requestCount is a property added to each node (folder/request) while constructing the trie
if (folderTree.root.children.hasOwnProperty(child) && folderTree.root.children[child].requestCount > 0) {
generatedStore.collection.items.add(
this.convertChildToItemGroup(spec, folderTree.root.children[child],
components, options, schemaCache, variableStore)
);
}
}
for (key in variableStore) {
// variableStore contains all the kinds of variable created.
// Add only the variables with type 'collection' to generatedStore.collection.variables
if (variableStore[key].type === 'collection') {
const collectionVar = new sdk.Variable(variableStore[key]);
generatedStore.collection.variables.add(collectionVar);
}
}
},
/**
* Adds Postman Collection Items using tags.
* Each tag from OpenAPI tags object is mapped to a collection item-group (Folder), and all operation that has
* corresponding tag in operation object's tags array is included in mapped item-group.
*
* @param {object} spec - openAPI spec object
* @param {object} generatedStore - the store that holds the generated collection. Modified in-place
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {object} returns an object containing objects of tags and their requests
*/
addCollectionItemsUsingTags: function(spec, generatedStore, components, options, schemaCache) {
var globalTags = spec.tags || [],
paths = spec.paths || {},
pathMethods,
variableStore = {},
tagFolders = {};
// adding globalTags in the tagFolder object that are defined at the root level
_.forEach(globalTags, (globalTag) => {
tagFolders[globalTag.name] = {
description: _.get(globalTag, 'description', ''),
requests: []
};
});
_.forEach(paths, (currentPathObject, path) => {
var commonParams = [],
collectionVariables,
pathLevelServers = '';
// discard the leading slash, if it exists
if (path[0] === '/') {
path = path.substring(1);
}
// extracting common parameters for all the methods in the current path item
if (currentPathObject.hasOwnProperty('parameters')) {
commonParams = currentPathObject.parameters;
}
// storing common path/collection vars from the server object at the path item level
if (currentPathObject.hasOwnProperty('servers')) {
pathLevelServers = currentPathObject.servers;
// add path level server object's URL as collection variable
collectionVariables = this.convertToPmCollectionVariables(
pathLevelServers[0].variables, // these are path variables in the server block
this.fixPathVariableName(path), // the name of the variable
this.fixPathVariablesInUrl(pathLevelServers[0].url)
);
_.forEach(collectionVariables, (collectionVariable) => {
generatedStore.collection.variables.add(collectionVariable);
});
delete currentPathObject.servers;
}
// get available method names for this path (path item object can have keys apart from operations)
pathMethods = _.filter(_.keys(currentPathObject), (key) => {
return _.includes(METHODS, key);
});
_.forEach(pathMethods, (pathMethod) => {
var summary,
operationItem = currentPathObject[pathMethod] || {},
localTags = operationItem.tags;
// params - these contain path/header/body params
operationItem.parameters = this.getRequestParams(operationItem.parameters, commonParams,
components, options);
summary = operationItem.summary || operationItem.description;
// add the request which has not any tags
if (_.isEmpty(localTags)) {
let tempRequest = {
name: summary,
method: pathMethod,
path: path,
properties: operationItem,
type: 'item',
servers: pathLevelServers || undefined
};
generatedStore.collection.items.add(this.convertRequestToItem(
spec, tempRequest, components, options, schemaCache, variableStore));
}
else {
_.forEach(localTags, (localTag) => {
// add undefined tag object with empty description
if (!_.has(tagFolders, localTag)) {
tagFolders[localTag] = {
description: '',
requests: []
};
}
tagFolders[localTag].requests.push({
name: summary,
method: pathMethod,
path: path,
properties: operationItem,
type: 'item',
servers: pathLevelServers || undefined
});
});
}
});
});
// Add all folders created from tags and corresponding operations
// Iterate from bottom to top order to maintain tag order in spec
_.forEachRight(tagFolders, (tagFolder, tagName) => {
var itemGroup = new sdk.ItemGroup({
name: tagName,
description: tagFolder.description
});
_.forEach(tagFolder.requests, (request) => {
itemGroup.items.add(this.convertRequestToItem(spec, request, components, options, schemaCache, variableStore));
});
// Add folders first (before requests) in generated collection
generatedStore.collection.items.prepend(itemGroup);
});
// variableStore contains all the kinds of variable created.
// Add only the variables with type 'collection' to generatedStore.collection.variables
_.forEach(variableStore, (variable) => {
if (variable.type === 'collection') {
const collectionVar = new sdk.Variable(variable);
generatedStore.collection.variables.add(collectionVar);
}
});
},
/**
* Generates an array of SDK Variables from the common and provided path vars
* @param {string} type - Level at the tree root/path level. Can be method/root/param.
* method: request(operation)-level, root: spec-level, param: url-level
* @param {Array<object>} providedPathVars - Array of path variables
* @param {object|array} commonPathVars - Object of path variables taken from the specification
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {Array<object>} returns an array of sdk.Variable
*/
convertPathVariables: function(type, providedPathVars, commonPathVars, components, options, schemaCache) {
options = _.merge({}, defaultOptions, options);
var variables = [];
// converting the base uri path variables, if any
// commonPathVars is an object for type = root/method
// array otherwise
if (type === 'root' || type === 'method') {
_.forOwn(commonPathVars, (value, key) => {
let description = this.getParameterDescription(value);
variables.push({
key: key,
value: type === 'root' ? '{{' + key + '}}' : value.default,
description: description
});
});
}
else {
_.forEach(commonPathVars, (variable) => {
let fakedData,
convertedPathVar;
this.assignParameterExamples(variable);
fakedData = options.schemaFaker ?
safeSchemaFaker(variable.schema || {}, options.requestParametersResolution, PROCESSING_TYPE.CONVERSION,
PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache,
options.stackLimit) : '';
convertedPathVar = this.convertParamsWithStyle(variable, fakedData, PARAMETER_SOURCE.REQUEST,
components, schemaCache, options);
variables = _.concat(variables, convertedPathVar);
});
}
// keep already provided varables (server variables) at last
return _.concat(variables, providedPathVars);
},
/**
* convert childItem from OpenAPI to Postman itemGroup if requestCount(no of requests inside childitem)>1
* otherwise return postman request
* @param {*} openapi object with root-level data like pathVariables baseurl
* @param {*} child object is of type itemGroup or request
* resolve references while generating params.
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @param {object} variableStore - array for storing collection variables
* @param {boolean} fromWebhooks - true if we are processing the webhooks group, false by default
* @returns {*} Postman itemGroup or request
* @no-unit-test
*/
convertChildToItemGroup: function (openapi, child, components, options,
schemaCache, variableStore, fromWebhooks = false) {
options = _.merge({}, defaultOptions, options);
var resource = child,
itemGroup,
subChild,
i,
requestCount;
// 3 options:
// 1. folder with more than one request in its subtree
// (immediate children or otherwise)
if (resource.requestCount > 1) {
// only return a Postman folder if this folder has>1 children in its subtree
// otherwise we can end up with 10 levels of folders with 1 request in the end
itemGroup = new sdk.ItemGroup({
name: utils.insertSpacesInName(resource.name)
// TODO: have to add auth here (but first, auth to be put into the openapi tree)
});
// If a folder has only one child which is a folder then we collapsed the child folder
// with parent folder.
/* eslint-disable max-depth */
if (resource.childCount === 1 && options.collapseFolders) {
let subChild = Object.keys(resource.children)[0],
resourceSubChild = resource.children[subChild];
resourceSubChild.name = resource.name + '/' + resourceSubChild.name;
return this.convertChildToItemGroup(openapi, resourceSubChild, components, options,
schemaCache, variableStore, fromWebhooks);
}
/* eslint-enable */
// recurse over child leaf nodes
// and add as children to this folder
for (i = 0, requestCount = resource.requests.length; i < requestCount; i++) {
itemGroup.items.add(
this.convertRequestToItem(openapi, resource.requests[i], components, options,
schemaCache, variableStore, fromWebhooks)
);
}
// recurse over child folders
// and add as child folders to this folder
/* eslint-disable max-depth*/
for (subChild in resource.children) {
if (resource.children.hasOwnProperty(subChild) && resource.children[subChild].requestCount > 0) {
itemGroup.items.add(
this.convertChildToItemGroup(openapi, resource.children[subChild], components, options, schemaCache,
variableStore, fromWebhooks)
);
}
}
/* eslint-enable */
return itemGroup;
}
// 2. it has only 1 direct request of its own
if (resource.requests.length === 1) {
return this.convertRequestToItem(openapi, resource.requests[0], components, options,
schemaCache, variableStore, fromWebhooks);
}
// 3. it's a folder that has no child request
// but one request somewhere in its child folders
for (subChild in resource.children) {
if (resource.children.hasOwnProperty(subChild) && resource.children[subChild].requestCount === 1) {
return this.convertChildToItemGroup(openapi, resource.children[subChild], components, options, schemaCache,
variableStore, fromWebhooks);
}
}
},
/**
* Gets helper object based on the root spec and the operation.security object
* @param {*} openapi - the json object representing the OAS spec
* @param {Array<object>} securitySet - the security object at an operation level
* @returns {object} The authHelper to use while constructing the Postman Request. This is
* not directly supported in the SDK - the caller needs to determine the header/body based on the return
* value
* @no-unit-test
*/
getAuthHelper: function(openapi, securitySet) {
var securityDef,
helper;
// return false if security set is not defined
// or is an empty array
// this will set the request's auth to null - which is 'inherit from parent'
if (!securitySet || (Array.isArray(securitySet) && securitySet.length === 0)) {
return null;
}
_.forEach(securitySet, (security) => {
if (_.isObject(security) && _.isEmpty(security)) {
helper = {
type: 'noauth'
};
return false;
}
securityDef = _.get(openapi, ['securityDefs', Object.keys(security)[0]]);
if (!_.isObject(securityDef)) {
return;
}
else if (securityDef.type === 'http') {
if (_.toLower(securityDef.scheme) === 'basic') {
helper = {
type: 'basic',
basic: [
{ key: 'username', value: '<Basic Auth Username>' },
{ key: 'password', value: '<Basic Auth Password>' }
]
};
}
else if (_.toLower(securityDef.scheme) === 'bearer') {
helper = {
type: 'bearer',
bearer: [{ key: 'token', value: '<Bearer Token>' }]
};
}
else if (_.toLower(securityDef.scheme) === 'digest') {
helper = {
type: 'digest',
digest: [
{ key: 'username', value: '<Digest Auth Username>' },
{ key: 'password', value: '<Digest Auth Password>' },
{ key: 'realm', value: '<realm>' }
]
};
}
else if (_.toLower(securityDef.scheme) === 'oauth' || _.toLower(securityDef.scheme) === 'oauth1') {
helper = {
type: 'oauth1',
oauth1: [
{ key: 'consumerSecret', value: '<OAuth 1.0 Consumer Key>' },
{ key: 'consumerKey', value: '<OAuth 1.0 Consumer Secret>' },
{ key: 'addParamsToHeader', value: true }
]
};
}
}
else if (securityDef.type === 'oauth2') {
helper = {
type: 'oauth2'
};
}
else if (securityDef.type === 'apiKey') {
helper = {
type: 'apikey',
apikey: [
{
key: 'key',
value: _.isString(securityDef.name) ? securityDef.name : '<API Key Name>'
},
{ key: 'value', value: '<API Key>' },
{
key: 'in',
value: _.includes(['query', 'header'], securityDef.in) ? securityDef.in : 'header'
}
]
};
}
// stop searching for helper if valid auth scheme is found
if (!_.isEmpty(helper)) {
return false;
}
});
return helper;
},
/**
* 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
*/
generateSdkParam: function (param, location) {
const sdkElementMap = {
'query': sdk.QueryParam,
'header': sdk.Header,
'path': sdk.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;
},
/**
* Generates Auth helper for response, params (query, headers) in helper object is added in
* request (originalRequest) part of example.
*
* @param {*} requestAuthHelper - Auth helper object of corresponding request
* @returns {Object} - Response Auth helper object containing params to be added
*/
getResponseAuthHelper: function (requestAuthHelper) {
var responseAuthHelper = {
query: [],
header: []
},
getValueFromHelper = function (authParams, keyName) {
return _.find(authParams, { key: keyName }).value;
},
paramLocation,
description;
if (!_.isObject(requestAuthHelper)) {
return responseAuthHelper;
}
description = 'Added as a part of security scheme: ' + requestAuthHelper.type;
switch (requestAuthHelper.type) {
case 'apikey':
// find location of parameter from auth helper
paramLocation = getValueFromHelper(requestAuthHelper.apikey, 'in');
responseAuthHelper[paramLocation].push({
key: getValueFromHelper(requestAuthHelper.apikey, 'key'),
value: '<API Key>',
description
});
break;
case 'basic':
responseAuthHelper.header.push({
key: 'Authorization',
value: 'Basic <credentials>',
description
});
break;
case 'bearer':
responseAuthHelper.header.push({
key: 'Authorization',
value: 'Bearer <token>',
description
});
break;
case 'digest':
responseAuthHelper.header.push({
key: 'Authorization',
value: 'Digest <credentials>',
description
});
break;
case 'oauth1':
responseAuthHelper.header.push({
key: 'Authorization',
value: 'OAuth <credentials>',
description
});
break;
case 'oauth2':
responseAuthHelper.header.push({
key: 'Authorization',
value: '<token>',
description
});
break;
default:
break;
}
return responseAuthHelper;
},
/**
* Converts a 'content' object into Postman response body. Any content-type header determined
* from the body is returned as well
* @param {*} contentObj response content - this is the content property of the response body
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @return {object} responseBody, contentType header needed
*/
convertToPmResponseBody: function(contentObj, components, options, schemaCache) {
options = _.merge({}, defaultOptions, options);
var responseBody, cTypeHeader, hasComputedType, cTypes;
if (!contentObj) {
return {
contentTypeHeader: null,
responseBody: ''
};
}
let headers = Object.keys(contentObj);
for (let i = 0; i < headers.length; i++) {
let headerFamily = this.getHeaderFamily(headers[i]);
if (headerFamily !== HEADER_TYPE.INVALID) {
cTypeHeader = headers[i];
hasComputedType = true;
if (headerFamily === HEADER_TYPE.JSON) {
break;
}
}
}
// if no JSON or XML, take whatever we have
if (!hasComputedType) {
cTypes = Object.keys(contentObj);
if (cTypes.length > 0) {
cTypeHeader = cTypes[0];
hasComputedType = true;
}
else {
// just an empty object - can't convert anything
return {
contentTypeHeader: null,
responseBody: ''
};
}
}
responseBody = this.convertToPmBodyData(contentObj[cTypeHeader], REQUEST_TYPE.EXAMPLE, cTypeHeader,
PARAMETER_SOURCE.RESPONSE, options.indentCharacter, components, options, schemaCache);
if (this.getHeaderFamily(cTypeHeader) === HEADER_TYPE.JSON) {
responseBody = JSON.stringify(responseBody, null, options.indentCharacter);
}
else if (typeof responseBody !== 'string') {
// since the collection v2 schema only supports body being a string
responseBody = '';
}
return {
contentTypeHeader: cTypeHeader,
responseBody: responseBody
};
},
/**
* Create parameters specific for a request
* @param {*} localParams parameters array
* @returns {Object} with three arrays of query, header and path as keys.
* @no-unit-test
*/
getParametersForPathItem: function(localParams) {
var tempParam,
params = {
query: [],
header: [],
path: []
};
_.forEach(localParams, (param) => {
tempParam = param;
if (tempParam.in === 'query') {
params.query.push(tempParam);
}
else if (tempParam.in === 'header') {
params.header.push(tempParam);
}
else if (tempParam.in === 'path') {
params.path.push(tempParam);
}
});
return params;
},
/**
* returns first example in the input map
* @param {*} exampleObj map[string, exampleObject]
* @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.
* @returns {*} first example in the input map type
*/
getExampleData: function(exampleObj, components, options) {
options = _.merge({}, defaultOptions, options);
var example,
exampleKey;
if (typeof exampleObj !== 'object') {
return '';
}
exampleKey = Object.keys(exampleObj)[0];
example = exampleObj[exampleKey];
// return example value if present else example is returned
if (_.has(example, '$ref')) {
example = this.getRefObject(example.$ref, components, options);
}
if (_.has(example, 'value')) {
example = example.value;
}
return example;
},
/**
* converts one of the eamples or schema in Media Type object to postman data
* @param {*} bodyObj is MediaTypeObject
* @param {*} requestType - Specifies whether the request body is of example request or root request
* @param {*} contentType - content type header
* @param {string} parameterSourceOption tells that the schema object is of request or response
* @param {string} indentCharacter is needed for XML/JSON bodies only
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {*} postman body data
*/
// TODO: We also need to accept the content type
// and generate the body accordingly
// right now, even if the content-type was XML, we'll generate
// a JSON example/schema
convertToPmBodyData: function(bodyObj, requestType, contentType, parameterSourceOption,
indentCharacter, components, options, schemaCache) {
options = _.merge({}, defaultOptions, options);
var bodyData = '',
schemaFormat = SCHEMA_FORMATS.DEFAULT,
schemaType,
resolveTo = this.resolveToExampleOrSchema(requestType, options.requestParametersResolution,
options.exampleParametersResolution);
let concreteUtils = components && components.hasOwnProperty('concreteUtils') ?
components.concreteUtils :
DEFAULT_SCHEMA_UTILS;
if (_.isEmpty(bodyObj)) {
return bodyData;
}
if (bodyObj.example && (resolveTo === 'example' || !bodyObj.schema)) {
if (bodyObj.example.hasOwnProperty('$ref')) {
bodyObj.example = this.getRefObject(bodyObj.example.$ref, components, options);
if (this.getHeaderFamily(contentType) === HEADER_TYPE.JSON) {
// try to parse the example as JSON. OK if this fails
// eslint-disable-next-line max-depth
try {
bodyObj.example = JSON.parse(bodyObj.example);
}
// eslint-disable-next-line no-empty
catch (e) {}
}
}
bodyData = bodyObj.example;
}
else if (!_.isEmpty(bodyObj.examples) && (resolveTo === 'example' || !bodyObj.schema)) {
// take one of the examples as the body and not all
bodyData = this.getExampleData(bodyObj.examples, components, options);
}
else if (bodyObj.schema) {
if (bodyObj.schema.hasOwnProperty('$ref')) {
let outerProps = concreteUtils.getOuterPropsIfIsSupported(bodyObj.schema),
resolvedSchema;
// skip beforehand resolution for OAS 3.0
if (outerProps) {
resolvedSchema = this.getRefObject(bodyObj.schema.$ref, components, options);
bodyObj.schema = concreteUtils.addOuterPropsToRefSchemaIfIsSupported(resolvedSchema, outerProps);
}
}
if (options.schemaFaker) {
if (this.getHeaderFamily(contentType) === HEADER_TYPE.XML) {
schemaFormat = SCHEMA_FORMATS.XML;
}
// Do not fake schemas if the complexity score is 10
if (options.complexityScore === 10) {
schemaType = _.get(this.getRefObject(bodyObj.schema.$ref, components, options), 'type');
if (schemaType === 'object') {
return {
value: '<Error: Spec size too large, skipping faking of schemas>'
};
}
if (schemaType === 'array') {
return [
'<Error: Spec size too large, skipping faking of schemas>'
];
}
return '<Error: Spec size too large, skipping faking of schemas>';
}
// Do not fake the bodyData if the complexity is 10.
bodyData = safeSchemaFaker(bodyObj.schema || {}, resolveTo, PROCESSING_TYPE.CONVERSION, parameterSourceOption,
components, schemaFormat, indentCharacter, schemaCache, options.stackLimit);
}
else {
// do not fake if the option is false
bodyData = '';
}
}
return bodyData;
},
/**
* returns whether to resolve to example or schema
* @param {string} requestType - Specifies whether the request body is of example request or root request
* @param {string} requestParametersResolution - the option value of requestParametersResolution
* @param {string} exampleParametersResolution - the option value of exampleParametersResolution
* @returns {string} Whether to resolve to example or schema
*/
resolveToExampleOrSchema(requestType, requestParametersResolution, exampleParametersResolution) {
if (requestType === REQUEST_TYPE.ROOT) {
if (requestParametersResolution === 'example') {
return 'example';
}
else if (requestParametersResolution === 'schema') {
return 'schema';
}
}
if (requestType === REQUEST_TYPE.EXAMPLE) {
if (exampleParametersResolution === 'example') {
return 'example';
}
else if (exampleParametersResolution === 'schema') {
return 'schema';
}
}
return 'schema';
},
/**
* convert param with in='query' to string considering style and type
* @param {*} param with in='query'
* @param {*} requestType Specifies whether the request body is of example request or root request
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {array} converted queryparam
*/
convertToPmQueryParameters: function(param, requestType, components, options, schemaCache) {
options = _.merge({}, defaultOptions, options);
var pmParams = [],
paramValue,
resolveTo = this.resolveToExampleOrSchema(requestType, options.requestParametersResolution,
options.exampleParametersResolution);
if (!param) {
return [];
}
// check for existence of schema
if (param.hasOwnProperty('schema')) {
this.assignParameterExamples(param);
// fake data generated
paramValue = options.schemaFaker ?
safeSchemaFaker(param.schema, resolveTo, PROCESSING_TYPE.CONVERSION, PARAMETER_SOURCE.REQUEST,
components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit) : '';
// paramType = param.schema.type;
if (typeof paramValue === 'number' || typeof paramValue === 'boolean') {
// the SDK will keep the number-ness,
// which will be rejected by the collection v2 schema
// converting to string to prevent issues like
// https://github.com/postmanlabs/postman-app-support/issues/6500
paramValue = paramValue.toString();
}
return this.convertParamsWithStyle(param, paramValue, PARAMETER_SOURCE.REQUEST, components, schemaCache, options);
}
let description = this.getParameterDescription(param);
// since no schema present add the parameter with no value
pmParams.push({
key: param.name,
value: '',
description: description
});
return pmParams;
},
/**
* Recursicely 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
*/
extractDeepObjectParams: function (deepObject, objectKey) {
let extractedParams = [];
Object.keys(deepObject).forEach((key) => {
let value = deepObject[key];
if (typeof value === 'object') {
extractedParams = _.concat(extractedParams, this.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 schmeResolution 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
*/
convertParamsWithStyle: function(param, paramValue, parameterSource, components, schemaCache, options) {
var paramName = _.get(param, 'name'),
pmParams = [],
serialisedValue = '',
description = this.getParameterDescription(param),
disabled = false;
// for invalid param object return null
if (!_.isObject(param)) {
return null;
}
let { style, explode, startValue, propSeparator, keyValueSeparator, isExplodable } =
this.getParamSerialisationInfo(param, parameterSource, components, schemaCache);
if (options && options.disableOptionalParameters) {
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(this.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 = this.extractDeepObjectParams(paramValue, paramName);
_.forEach(extractedParams, (extractedParam) => {
pmParams.push(this.generateSdkParam({
key: extractedParam.key,
value: extractedParam.value || '',
description,
disabled
}, _.get(param, 'in')));
});
}
return pmParams;
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(this.generateSdkParam({
key: paramName,
value: serialisedValue,
description,
disabled
}, _.get(param, 'in')));
return pmParams;
},
/**
* converts params with in='header' to a Postman header object
* @param {*} header param with in='header'
* @param {*} requestType Specifies whether the request body is of example request or root request
* @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} options - a standard list of options that's globally passed around. Check options.js for more.
* @param {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {Object} instance of a Postman SDK Header
*/
convertToPmHeader: function(header, requestType, parameterSource, components, options, schemaCache) {
options = _.merge({}, defaultOptions, options);
var fakeData,
convertedHeader,
reqHeader,
resolveTo = this.resolveToExampleOrSchema(requestType, options.requestParametersResolution,
options.exampleParametersResolution);
if (header.hasOwnProperty('schema')) {
if (!options.schemaFaker) {
fakeData = '';
}
else {
this.assignParameterExamples(header);
fakeData = safeSchemaFaker(header.schema || {}, resolveTo, PROCESSING_TYPE.CONVERSION, parameterSource,
components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit);
}
}
else {
fakeData = '';
}
convertedHeader = _.get(this.convertParamsWithStyle(header, fakeData, parameterSource,
components, schemaCache, options), '[0]');
reqHeader = new sdk.Header(convertedHeader);
reqHeader.description = this.getParameterDescription(header);
return reqHeader;
},
/**
* converts operation item requestBody to a Postman request body
* @param {*} requestBody in operationItem
* @param {*} requestType - Specifies whether the request body is of example request or root request
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {Object} - Postman requestBody and Content-Type Header
*/
convertToPmBody: function(requestBody, requestType, components, options, schemaCache) {
options = _.merge({}, defaultOptions, options);
var contentObj, // content is required
bodyData,
param,
originalParam,
paramArray = [],
updateOptions = {},
reqBody = new sdk.RequestBody(),
contentHeader,
rDataMode,
params,
encoding,
cType,
description,
required,
enumValue,
formHeaders = [];
let concreteUtils = components && components.hasOwnProperty('concreteUtils') ?
components.concreteUtils :
DEFAULT_SCHEMA_UTILS;
// @TODO: how do we support multiple content types
contentObj = requestBody.content;
// to handle cases of malformed request body, where contentObj is null
if (!contentObj) {
return {
body: reqBody,
contentHeader: null,
formHeaders: null
};
}
// handling for the urlencoded media type
if (contentObj.hasOwnProperty(URLENCODED)) {
rDataMode = 'urlencoded';
bodyData = this.convertToPmBodyData(contentObj[URLENCODED], requestType, URLENCODED,
PARAMETER_SOURCE.REQUEST, options.indentCharacter, components, options, schemaCache);
encoding = contentObj[URLENCODED].encoding ? contentObj[URLENCODED].encoding : {};
if (contentObj[URLENCODED].hasOwnProperty('schema') && contentObj[URLENCODED].schema.hasOwnProperty('$ref')) {
contentObj[URLENCODED].schema = this.getRefObject(contentObj[URLENCODED].schema.$ref, components, options);
}
// create query parameters and add it to the request body object
_.forOwn(bodyData, (value, key) => {
if (_.get(contentObj[URLENCODED], 'schema.type') === 'object') {
description = _.get(contentObj[URLENCODED], ['schema', 'properties', key, 'description'], '');
required = _.includes(_.get(contentObj[URLENCODED], ['schema', 'required']), key);
enumValue = _.get(contentObj[URLENCODED], ['schema', 'properties', key, 'enum']);
}
!encoding[key] && (encoding[key] = {});
encoding[key].name = key;
encoding[key].schema = {
type: typeof value
};
// for urlencoded body serialisation is treated similar to query param
// reference https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-13
encoding[key].in = 'query';
_.isBoolean(required) && (encoding[key].required = required);
encoding[key].description = description;
params = this.convertParamsWithStyle(encoding[key], value, PARAMETER_SOURCE.REQUEST, components,
schemaCache, options);
// TODO: Show warning for incorrect schema if !params
params && params.forEach((element) => {
// Collection v2.1 schema allows urlencoded param value to be only string
if (typeof element.value !== 'string') {
try {
// convert other datatype to string (i.e. number, boolean etc)
element.value = JSON.stringify(element.value);
}
catch (e) {
// JSON.stringify can fail in few cases, suggest invalid type for such case
// eslint-disable-next-line max-len
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Exceptions
element.value = 'INVALID_URLENCODED_PARAM_TYPE';
}
}
});
paramArray.push(...params);
});
updateOptions = {
mode: rDataMode,
urlencoded: paramArray
};
// add a content type header for each media type for the request body
contentHeader = new sdk.Header({
key: 'Content-Type',
value: URLENCODED
});
// update the request body with the options
reqBody.update(updateOptions);
}
else if (contentObj.hasOwnProperty(FORM_DATA)) {
rDataMode = 'formdata';
bodyData = this.convertToPmBodyData(contentObj[FORM_DATA], requestType, FORM_DATA,
PARAMETER_SOURCE.REQUEST, options.indentCharacter, components, options, schemaCache);
encoding = contentObj[FORM_DATA].encoding ? contentObj[FORM_DATA].encoding : {};
// create the form parameters and add it to the request body object
_.forOwn(bodyData, (value, key) => {
if (_.get(contentObj[FORM_DATA], 'schema.type') === 'object') {
description = _.get(contentObj[FORM_DATA], ['schema', 'properties', key, 'description'], '');
required = _.includes(_.get(contentObj[FORM_DATA], ['schema', 'required']), key);
enumValue = _.get(contentObj[FORM_DATA], ['schema', 'properties', key, 'enum']);
}
description = (required ? '(Required) ' : '') + description +
(enumValue ? ' (This can only be one of ' + enumValue + ')' : '');
if (encoding.hasOwnProperty(key)) {
_.forOwn(encoding[key].headers, (value, key) => {
if (key !== 'Content-Type') {
if (encoding[key].headers[key].hasOwnProperty('$ref')) {
encoding[key].headers[key] = getRefObject(encoding[key].headers[key].$ref, components, options);
}
encoding[key].headers[key].name = key;
// this is only for ROOT request because we are adding the headers for example request later
formHeaders.push(this.convertToPmHeader(encoding[key].headers[key],
REQUEST_TYPE.ROOT, PARAMETER_SOURCE.REQUEST, components, options, schemaCache));
}
});
}
// Collection v2.1 schema allows form param value to be only string
if (typeof value !== 'string') {
try {
// convert other datatype to string (i.e. number, boolean etc)
value = JSON.stringify(value);
}
catch (e) {
// JSON.stringify can fail in few cases, suggest invalid type for such case
// eslint-disable-next-line max-len
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Exceptions
value = 'INVALID_FORM_PARAM_TYPE';
}
}
// Fetch the original param and if it is of type 'string' and format 'binary'
// then set the type of FormParam to 'file'
originalParam = _.get(contentObj[FORM_DATA], ['schema', 'properties', key]);
if (originalParam &&
originalParam.type === 'string' &&
originalParam.format === 'binary'
) {
param = new sdk.FormParam({
key: key,
value: '',
type: 'file'
});
}
else {
param = new sdk.FormParam({
key: key,
value: value,
type: 'text'
});
}
param.description = description;
paramArray.push(param);
});
updateOptions = {
mode: rDataMode,
formdata: paramArray
};
// add a content type header for the pertaining media type
contentHeader = new sdk.Header({
key: 'Content-Type',
value: FORM_DATA
});
// update the request body
reqBody.update(updateOptions);
}
else {
rDataMode = 'raw';
let bodyType, language;
// checking for all possible raw types
if (contentObj.hasOwnProperty(APP_JS)) { bodyType = APP_JS; }
else if (contentObj.hasOwnProperty(APP_JSON)) { bodyType = APP_JSON; }
else if (contentObj.hasOwnProperty(TEXT_HTML)) { bodyType = TEXT_HTML; }
else if (contentObj.hasOwnProperty(TEXT_PLAIN)) { bodyType = TEXT_PLAIN; }
else if (contentObj.hasOwnProperty(TEXT_XML)) { bodyType = TEXT_XML; }
else {
// take the first property it has
// types like image/png etc
for (cType in contentObj) {
if (contentObj.hasOwnProperty(cType)) {
bodyType = cType;
break;
}
}
}
if (
concreteUtils.isBinaryContentType(bodyType, contentObj)
) {
updateOptions = {
mode: 'file'
};
}
else {
bodyData = this.convertToPmBodyData(contentObj[bodyType], requestType, bodyType,
PARAMETER_SOURCE.REQUEST, options.indentCharacter, components, options, schemaCache);
updateOptions = {
mode: rDataMode,
raw: JSON.stringify(bodyData, null, options.indentCharacter)
};
}
language = this.getHeaderFamily(bodyType);
if (language !== HEADER_TYPE.INVALID) {
updateOptions.options = {
raw: {
language
}
};
}
contentHeader = new sdk.Header({
key: 'Content-Type',
value: bodyType
});
reqBody.update(updateOptions);
}
return {
body: reqBody,
contentHeader: contentHeader,
formHeaders: formHeaders
};
},
/**
* @param {*} response in operationItem responses
* @param {*} code - response Code
* @param {*} originalRequest - the request for the example
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {Object} postman response
*/
convertToPmResponse: function(response, code, originalRequest, components, options, schemaCache) {
options = _.merge({}, defaultOptions, options);
var responseHeaders = [],
previewLanguage = 'text',
responseBodyWrapper,
header,
sdkResponse;
if (!response) {
return null;
}
_.forOwn(response.headers, (value, key) => {
if (_.toLower(key) !== 'content-type') {
if (_.get(value, '$ref')) {
// the convert to PmHeader function handles the
// schema-faking
header = this.getRefObject(value.$ref, components, options);
}
else {
header = value;
}
header.name = key;
if (!_.includes(IMPLICIT_HEADERS, _.toLower(key))) {
responseHeaders.push(this.convertToPmHeader(header, REQUEST_TYPE.EXAMPLE,
PARAMETER_SOURCE.RESPONSE, components, options, schemaCache));
}
}
});
responseBodyWrapper = this.convertToPmResponseBody(response.content, components, options, schemaCache);
if (responseBodyWrapper.contentTypeHeader) {
// we could infer the content-type header from the body
responseHeaders.push({ key: 'Content-Type', value: responseBodyWrapper.contentTypeHeader });
if (this.getHeaderFamily(responseBodyWrapper.contentTypeHeader) === HEADER_TYPE.JSON) {
previewLanguage = PREVIEW_LANGUAGE.JSON;
}
else if (this.getHeaderFamily(responseBodyWrapper.contentTypeHeader) === HEADER_TYPE.XML) {
previewLanguage = PREVIEW_LANGUAGE.XML;
}
}
else if (response.content && Object.keys(response.content).length > 0) {
responseHeaders.push({ key: 'Content-Type', value: Object.keys(response.content)[0] });
if (this.getHeaderFamily(Object.keys(response.content)[0]) === HEADER_TYPE.JSON) {
previewLanguage = PREVIEW_LANGUAGE.JSON;
}
else if (this.getHeaderFamily(Object.keys(response.content)[0]) === HEADER_TYPE.XML) {
previewLanguage = PREVIEW_LANGUAGE.XML;
}
}
else {
responseHeaders.push({ key: 'Content-Type', value: TEXT_PLAIN });
}
// replace 'X' char with '0'
code = code.replace(/X/g, '0');
code = code === 'default' ? 500 : _.toSafeInteger(code);
sdkResponse = new sdk.Response({
name: response.description,
code: code || 500,
header: responseHeaders,
body: responseBodyWrapper.responseBody,
originalRequest: originalRequest
});
sdkResponse._postman_previewlanguage = previewLanguage;
return sdkResponse;
},
/**
* @param {*} $ref reference object
* @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.
* @returns {Object} reference object from the saved components
* @no-unit-tests
*/
getRefObject: function($ref, components, options) {
options = _.merge({}, defaultOptions, options);
var refObj, savedSchema;
if (typeof $ref !== 'string') {
return { value: `Invalid $ref: ${$ref} was found` };
}
savedSchema = $ref.split('/').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, '~')
);
});
// at this stage, savedSchema is [components, part1, parts]
// must have min. 2 segments after "#/components"
if (savedSchema.length < 3) {
console.warn(`ref ${$ref} not found.`);
return { value: `reference ${$ref} not found in the given specification` };
}
if (savedSchema[0] !== 'components' && savedSchema[0] !== 'paths') {
console.warn(`Error reading ${$ref}. Can only use references from components and paths`);
return { value: `Error reading ${$ref}. Can only use references from components and paths` };
}
// at this point, savedSchema is similar to ['components', 'schemas','Address']
// components is actually components and paths (an object with components + paths as 1st-level-props)
refObj = _.get(components, savedSchema);
if (!refObj) {
console.warn(`ref ${$ref} not found.`);
return { value: `reference ${$ref} not found in the given specification` };
}
if (refObj.$ref) {
return this.getRefObject(refObj.$ref, components, options);
}
return refObj;
},
/** Finds all the possible path variables in a given path string
* @param {string} path Path string : /pets/{petId}
* @returns {array} Array of path variables.
*/
findPathVariablesFromPath: function (path) {
// /{{path}}/{{file}}.{{format}}/{{hello}} return [ '{{path}}', '{{hello}}' ]
// https://regex101.com/r/XGL4Gh/1
return path.match(/(\/\{\{[^\/\{\}]+\}\})(?=\/|$)/g);
},
/** Finds all the possible collection variables in a given path string
* @param {string} path Path string : /pets/{petId}
* @returns {array} Array of collection variables.
*/
findCollectionVariablesFromPath: function (path) {
// /:path/{{file}}.{{format}}/:hello => only {{file}} and {{format}} will match
// https://regex101.com/r/XGL4Gh/2
return path.match(/(\{\{[^\/\{\}]+\}\})/g);
},
/**
* Finds all the possible path variables conversion from schema path,
* and excludes path variable that are converted to collection variable
* @param {string} path Path string
* @returns {array} Array of path variables.
*/
findPathVariablesFromSchemaPath: function (path) {
// /{path}/{file}.{format}/{hello} return [ '/{path}', '/{hello}' ]
// https://regex101.com/r/aFRWQD/4
let matches = path.match(/(\/\{[^\/\{\}]+\}(?=[\/\0]|$))/g);
// remove leading '/' and starting and ending curly braces
return _.map(matches, (match) => { return match.slice(2, -1); });
},
/**
* Finds fixed parts present in path segment of collection or schema.
*
* @param {String} segment - Path segment
* @param {String} pathType - Path type (one of 'collection' / 'schema')
* @returns {Array} - Array of strings where each element is fixed part in order of their occurence
*/
getFixedPartsFromPathSegment: function (segment, pathType = 'collection') {
var tempSegment = segment,
// collection is default
varMatches = segment.match(pathType === 'schema' ? /(\{[^\/\{\}]+\})/g : /(\{\{[^\/\{\}]+\}\})/g),
fixedParts = [];
_.forEach(varMatches, (match) => {
let matchedIndex = tempSegment.indexOf(match);
// push fixed part before collection variable if present
(matchedIndex !== 0) && (fixedParts.push(tempSegment.slice(0, matchedIndex)));
// substract starting fixed and variable part from tempSegment
tempSegment = tempSegment.substr(matchedIndex + match.length);
});
// add last fixed part if present
(tempSegment.length > 0) && (fixedParts.push(tempSegment));
return fixedParts;
},
/** 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.
*/
sanitizeUrlPathParams: function (reqUrl, pathVars) {
var matches,
collectionVars = [];
// converts all the of the following:
// /{{path}}/{{file}}.{{format}}/{{hello}} => /:path/{{file}}.{{format}}/:hello
matches = this.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 = this.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 };
},
cleanWebhookName: function(webhookName) {
return webhookName
.replace(/[:/]/g, '_')
.replace(/[{}]/g, '');
},
/**
* function to convert an openapi path item to postman item
* @param {*} openapi openapi object with root properties
* @param {*} operationItem path operationItem from tree structure
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @param {array} variableStore - array
* @param {boolean} fromWebhooks - true if we are procesing the webhooks group, false by default
* @returns {Object} postman request Item
* @no-unit-test
*/
convertRequestToItem: function(openapi, operationItem, components,
options, schemaCache, variableStore, fromWebhooks = false) {
options = _.merge({}, defaultOptions, options);
var reqName,
pathVariables = openapi.baseUrlVariables,
operation = operationItem.properties,
reqBody = operationItem.properties.requestBody,
itemParams = operationItem.properties.parameters,
reqParams = this.getParametersForPathItem(itemParams),
baseUrl = fromWebhooks ?
`{{${this.cleanWebhookName(operationItem.path)}}}` :
'{{baseUrl}}',
pathVarArray = [],
authHelper,
item,
serverObj,
displayUrl,
reqUrl = fromWebhooks ? '' : '/' + operationItem.path,
pmBody,
authMeta,
swagResponse,
localServers = fromWebhooks ? '' : _.get(operationItem, 'properties.servers'),
exampleRequestBody,
sanitizeResult,
globalServers = fromWebhooks ? '' : _.get(operationItem, 'servers'),
responseMediaTypes,
acceptHeader;
// handling path templating in request url if any
// convert all {anything} to {{anything}}
reqUrl = this.fixPathVariablesInUrl(reqUrl);
// convert all /{{one}}/{{two}} to /:one/:two
// Doesn't touch /{{file}}.{{format}}
sanitizeResult = this.sanitizeUrlPathParams(reqUrl, reqParams.path);
// Updated reqUrl
reqUrl = sanitizeResult.url;
// Updated reqParams.path
reqParams.path = sanitizeResult.pathVars;
// Add collection variables to the variableStore.
sanitizeResult.collectionVars.forEach((element) => {
if (!variableStore[element.name]) {
let fakedData = options.schemaFaker ?
safeSchemaFaker(element.schema || {}, options.requestParametersResolution, PROCESSING_TYPE.CONVERSION,
PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache,
options.stackLimit) : '',
convertedPathVar = _.get(this.convertParamsWithStyle(element, fakedData, PARAMETER_SOURCE.REQUEST,
components, schemaCache, options), '[0]', {});
variableStore[element.name] = _.assign(convertedPathVar, { key: element.name, type: 'collection' });
}
});
// accounting for the overriding of the root level and path level servers object if present at the operation level
if (Array.isArray(localServers) && localServers.length) {
serverObj = operationItem.properties.servers[0];
// convert all {anything} to {{anything}}
baseUrl = this.fixPathVariablesInUrl(serverObj.url);
// add serverObj variables to pathVarArray
if (serverObj.variables) {
_.forOwn(serverObj.variables, (value, key) => {
pathVarArray.push({
name: key,
value: value.default || '',
description: this.getParameterDescription(value)
});
});
// use pathVarAray to sanitize url for path params and collection variables.
sanitizeResult = this.sanitizeUrlPathParams(baseUrl, pathVarArray);
// update the base url with update url
baseUrl = sanitizeResult.url;
// Add new collection variables to the variableStore
sanitizeResult.collectionVars.forEach((element) => {
if (!variableStore[element.name]) {
variableStore[element.name] = {
key: element.name,
value: element.value || '',
description: element.description,
type: 'collection'
};
}
});
// remove all the collection variables from serverObj.variables
serverObj.pathVariables = {};
sanitizeResult.pathVars.forEach((element) => {
serverObj.pathVariables[element.name] = serverObj.variables[element.name];
});
// use this new filterd serverObj.pathVariables
// to generate pm path variables.
pathVarArray = this.convertPathVariables('method', [],
serverObj.pathVariables, components, options, schemaCache);
}
baseUrl += reqUrl;
}
else {
// accounting for the overriding of the root level servers object if present at the path level
if (Array.isArray(globalServers) && globalServers.length) {
// All the global servers present at the path level are taken care of in generateTrieFromPaths
// Just adding the same structure of the url as the display URL.
displayUrl = '{{' + this.fixPathVariableName(operationItem.path) + '}}' + reqUrl;
}
// In case there are no server available use the baseUrl
else {
baseUrl += reqUrl;
if (pathVariables || fromWebhooks) {
displayUrl = baseUrl;
}
else {
displayUrl = '{{baseUrl}}' + reqUrl;
}
}
pathVarArray = this.convertPathVariables('root', [], pathVariables, components, options, schemaCache);
}
switch (options.requestNameSource) {
case 'fallback' : {
// operationId is usually camelcase or snake case
if (fromWebhooks) {
reqName = utils.insertSpacesInName(operation.operationId) ||
operation.summary ||
operation.description ||
`${this.cleanWebhookName(operationItem.path)} - ${operationItem.method}`;
}
else {
reqName = operation.summary || utils.insertSpacesInName(operation.operationId) || reqUrl;
}
break;
}
case 'url' : {
reqName = displayUrl || baseUrl;
break;
}
default : {
reqName = operation[options.requestNameSource];
break;
}
}
if (!reqName) {
throw new openApiErr(`requestNameSource (${options.requestNameSource})` +
` in options is invalid or property does not exist in ${operationItem.path}`);
}
// handling authentication here (for http type only)
authHelper = this.getAuthHelper(openapi, operation.security);
// creating the request object
item = new sdk.Item({
name: reqName,
request: {
description: operation.description,
url: displayUrl || baseUrl,
name: reqName,
method: operationItem.method.toUpperCase()
}
});
// using the auth helper
authMeta = operation['x-postman-meta'];
if (authMeta && authMeta.currentHelper && authMap[authMeta.currentHelper]) {
let thisAuthObject = {
type: authMap[authMeta.currentHelper]
};
thisAuthObject[authMap[authMeta.currentHelper]] = authMeta.helperAttributes;
item.request.auth = new sdk.RequestAuth(thisAuthObject);
}
else {
item.request.auth = authHelper;
}
// adding query params to postman request url.
_.forEach(reqParams.query, (queryParam) => {
this.convertToPmQueryParameters(queryParam, REQUEST_TYPE.ROOT, components, options, schemaCache)
.forEach((pmParam) => {
item.request.url.addQueryParams(pmParam);
});
});
item.request.url.query.members.forEach((query) => {
// Collection v2.1 schema allows query param value to be string/null
if (typeof query.value !== 'string') {
try {
// convert other datatype to string (i.e. number, boolean etc)
query.value = JSON.stringify(query.value);
}
catch (e) {
// JSON.stringify can fail in few cases, suggest invalid type for such case
// eslint-disable-next-line max-len
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Exceptions
query.value = 'INVALID_QUERY_PARAM_TYPE';
}
}
});
item.request.url.variables.clear();
item.request.url.variables.assimilate(this.convertPathVariables('param', pathVarArray, reqParams.path,
components, options, schemaCache));
// Making sure description never goes out as an object
// App / Collection transformer fail with the object syntax
if (item.request.url.variables.members && item.request.url.variables.members.length > 0) {
item.request.url.variables.members = _.map(item.request.url.variables.members, (m) => {
if (typeof m.description === 'object' && m.description.content) {
m.description = m.description.content;
}
return m;
});
}
// adding headers to request from reqParam
_.forEach(reqParams.header, (header) => {
if (options.keepImplicitHeaders || !_.includes(IMPLICIT_HEADERS, _.toLower(_.get(header, 'name')))) {
item.request.addHeader(this.convertToPmHeader(header, REQUEST_TYPE.ROOT, PARAMETER_SOURCE.REQUEST,
components, options, schemaCache));
}
});
// adding Request Body and Content-Type header
if (reqBody) {
if (reqBody.$ref) {
reqBody = this.getRefObject(reqBody.$ref, components, options);
}
pmBody = this.convertToPmBody(reqBody, REQUEST_TYPE.ROOT, components, options, schemaCache);
item.request.body = pmBody.body;
// Following is added to make sure body pruning for request methods like GET, HEAD etc is disabled'.
item.protocolProfileBehavior = { disableBodyPruning: true };
if (!options.keepImplicitHeaders || (!_.find(reqParams.header, (h) => {
return _.toLower(_.get(h, 'name')) === 'content-type';
}))) {
// Add detected content-type header
item.request.addHeader(pmBody.contentHeader);
}
// extra form headers if encoding is present in request Body.
// TODO: Show warning for incorrect schema if !pmBody.formHeaders
pmBody.formHeaders && pmBody.formHeaders.forEach((element) => {
item.request.addHeader(element);
});
}
// adding responses to request item
if (operation.responses) {
let thisOriginalRequest = {},
responseAuthHelper,
authQueryParams,
convertedResponse;
if (options.includeAuthInfoInExample) {
responseAuthHelper = this.getResponseAuthHelper(authHelper);
// override auth helper with global security definition if no operation security definition found
if (_.isEmpty(authHelper)) {
responseAuthHelper = this.getResponseAuthHelper(this.getAuthHelper(openapi, openapi.security));
}
authQueryParams = _.map(responseAuthHelper.query, (queryParam) => {
// key-value pair will be added as transformed query string
return queryParam.key + '=' + queryParam.value;
});
}
_.forOwn(operation.responses, (response, code) => {
let originalRequestHeaders = [],
originalRequestQueryParams = this.convertToPmQueryArray(reqParams, REQUEST_TYPE.EXAMPLE,
components, options, schemaCache);
swagResponse = response;
if (_.get(response, '$ref')) {
swagResponse = this.getRefObject(response.$ref, components, options);
}
if (options.includeAuthInfoInExample) {
// add Authorization params if present
originalRequestQueryParams = _.concat(originalRequestQueryParams, authQueryParams);
originalRequestHeaders = _.concat(originalRequestHeaders, responseAuthHelper.header);
}
// Try and set fields for originalRequest (example.request)
thisOriginalRequest.method = item.request.method;
// setting URL
const clonedItemURL = _.cloneDeep(item.request.url);
thisOriginalRequest.url = clonedItemURL;
/**
* Setting variable
* overriding `thisOriginalRequest.url.variable` as the url defination expects
* 1. Field variable and not variables
* 2. Field variable to be array of objects which maps to `clonedItemURL.variables.members`
*/
thisOriginalRequest.url.variable = clonedItemURL.variables.members;
thisOriginalRequest.url.query = [];
// setting query params
if (originalRequestQueryParams.length) {
thisOriginalRequest.url.query = originalRequestQueryParams.join('&');
}
// setting headers
_.forEach(reqParams.header, (header) => {
originalRequestHeaders.push(this.convertToPmHeader(header, REQUEST_TYPE.EXAMPLE,
PARAMETER_SOURCE.REQUEST, components, options, schemaCache));
});
thisOriginalRequest.header = originalRequestHeaders;
// setting request body
try {
exampleRequestBody = this.convertToPmBody(operationItem.properties.requestBody,
REQUEST_TYPE.EXAMPLE, components, options, schemaCache);
thisOriginalRequest.body = exampleRequestBody.body ? exampleRequestBody.body.toJSON() : {};
}
catch (e) {
// console.warn('Exception thrown while trying to json-ify body for item.request.body:', item.request.body,
// 'Exception:', e);
thisOriginalRequest.body = {};
}
// set accept header value as first found response content's media type
if (_.isEmpty(acceptHeader)) {
responseMediaTypes = _.keys(_.get(swagResponse, 'content'));
if (responseMediaTypes.length > 0) {
acceptHeader = {
key: 'Accept',
value: responseMediaTypes[0]
};
}
}
convertedResponse = this.convertToPmResponse(swagResponse, code, thisOriginalRequest,
components, options, schemaCache);
convertedResponse && item.responses.add(convertedResponse);
});
}
// add accept header if found and not present already
if (!_.isEmpty(acceptHeader) && !item.request.headers.has('accept')) {
item.request.addHeader(acceptHeader);
}
return item;
},
/**
* function to convert an openapi query params object to array of query params
* @param {*} reqParams openapi query params object
* @param {*} requestType Specifies whether the request body is of example request or root request
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {*} array of all query params
*/
convertToPmQueryArray: function(reqParams, requestType, components, options, schemaCache) {
options = _.merge({}, defaultOptions, options);
let requestQueryParams = [];
_.forEach(reqParams.query, (queryParam) => {
this.convertToPmQueryParameters(queryParam, requestType, components, options, schemaCache).forEach((pmParam) => {
requestQueryParams.push(pmParam.key + '=' + pmParam.value);
});
});
return requestQueryParams;
},
// along with the path object, this also returns the values of the
// path variable's values
// also, any endpoint-level params are merged into the returned pathItemObject
findMatchingRequestFromSchema: function (method, url, schema, options) {
// first step - get array of requests from schema
let parsedUrl = require('url').parse(url),
retVal = [],
pathToMatch,
matchedPath,
matchedPathJsonPath,
schemaPathItems = schema.paths,
pathToMatchServer,
filteredPathItemsArray = [];
// Return no matches for invalid url (if unable to decode parsed url)
try {
pathToMatch = decodeURI(parsedUrl.pathname);
}
catch (e) {
console.warn(
'Error decoding request URI endpoint. URI: ', url,
'Error', e
);
return retVal;
}
// if pathToMatch starts with '/', we assume it's the correct path
// if not, we assume the segment till the first '/' is the host
// this is because a Postman URL like "{{url}}/a/b" will
// likely have {{url}} as the host segment
if (!pathToMatch.startsWith('/')) {
pathToMatch = pathToMatch.substring(pathToMatch.indexOf('/'));
}
// Here, only take pathItemObjects that have the right method
// of those that do, determine a score
// then just pick that key-value pair from schemaPathItems
_.forOwn(schemaPathItems, (pathItemObject, path) => {
if (!pathItemObject) {
// invalid schema. schema.paths had an invalid entry
return true;
}
if (!pathItemObject.hasOwnProperty(method.toLowerCase())) {
// the required method was not found at this path
return true;
}
// filter empty parameters
pathItemObject.parameters = _.reduce(pathItemObject.parameters, (accumulator, param) => {
if (!_.isEmpty(param)) {
accumulator.push(param);
}
return accumulator;
}, []);
let schemaMatchResult = { match: false };
// check if path and pathToMatch match (non-null)
// check in explicit (local defined) servers
if (pathItemObject[method.toLowerCase()].servers) {
pathToMatchServer = this.handleExplicitServersPathToMatch(pathToMatch, path);
schemaMatchResult = this.getPostmanUrlSchemaMatchScore(pathToMatchServer, path, options);
}
else {
schemaMatchResult = this.getPostmanUrlSchemaMatchScore(pathToMatch, path, options);
}
if (!schemaMatchResult.match) {
// there was no reasonable match b/w the postman path and this schema path
return true;
}
filteredPathItemsArray.push({
path,
pathItem: pathItemObject,
matchScore: schemaMatchResult.score,
pathVars: schemaMatchResult.pathVars,
// No. of fixed segment matches between schema and postman url path
// i.e. schema path /user/{userId} and request path /user/{{userId}} has 1 fixed segment match ('user')
fixedMatchedSegments: schemaMatchResult.fixedMatchedSegments,
// No. of variable segment matches between schema and postman url path
// i.e. schema path /user/{userId} and request path /user/{{userId}} has 1 variable segment match ('{userId}')
variableMatchedSegments: schemaMatchResult.variableMatchedSegments
});
});
// order endpoints with more fix matched segments and variable matched segments (for tie in former) first in result
filteredPathItemsArray = _.orderBy(filteredPathItemsArray, ['fixedMatchedSegments', 'variableMatchedSegments'],
['desc', 'desc']);
_.each(filteredPathItemsArray, (fp) => {
let path = fp.path,
pathItemObject = fp.pathItem,
score = fp.matchScore,
pathVars = fp.pathVars;
matchedPath = pathItemObject[method.toLowerCase()];
if (!matchedPath) {
// method existed at the path, but was a falsy value
return true;
}
matchedPathJsonPath = `$.paths[${path}]`;
// filter empty parameters
matchedPath.parameters = _.reduce(matchedPath.parameters, (accumulator, param) => {
if (!_.isEmpty(param)) {
accumulator.push(param);
}
return accumulator;
}, []);
// aggregate local + global parameters for this path
matchedPath.parameters = _.map(matchedPath.parameters, (commonParam) => {
// for path-specifix params that are added to the path, have a way to identify them
// when the schemaPath is required
// method is lowercased because OAS methods are always lowercase
commonParam.pathPrefix = `${matchedPathJsonPath}.${method.toLowerCase()}.parameters`;
return commonParam;
}).concat(
_.map(pathItemObject.parameters || [], (commonParam) => {
// for common params that are added to the path, have a way to identify them
// when the schemaPath is required
commonParam.pathPrefix = matchedPathJsonPath + '.parameters';
return commonParam;
})
);
retVal.push({
// using path instead of operationId / sumamry since it's widely understood
name: method + ' ' + path,
// assign path as schemaPathName property to use path in path object
path: _.assign(matchedPath, { schemaPathName: path }),
jsonPath: matchedPathJsonPath + '.' + method.toLowerCase(),
pathVariables: pathVars,
score: score
});
// code reaching here indicates the given method was not found
return true;
});
return retVal;
},
/**
* Checks if value is postman variable or not
*
* @param {*} value - Value to check for
* @returns {Boolean} postman variable or not
*/
isPmVariable: function (value) {
// collection/environment variables are in format - {{var}}
return _.isString(value) && _.startsWith(value, '{{') && _.endsWith(value, '}}');
},
/**
* 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
*/
getPathValue: function (sourceValue, dataPath, fallback) {
return (dataPath === '' ? sourceValue : _.get(sourceValue, dataPath, fallback));
},
/**
* Returns all security params that can be applied during converion.
*
* @param {Object} components - OpenAPI components
* @param {String} location - location for which we want to get security params (i.e. 'header' | 'query')
* @returns {Array} applicable security params
*/
getSecurityParams: function (components, location) {
let securityDefs = _.get(components, 'securitySchemes', {}),
securityParams = [];
_.forEach(securityDefs, (securityDef) => {
// Currently we only apply header and query for apiKey type of security param during conversion
if (_.get(securityDef, 'type') === 'apiKey' && _.get(securityDef, 'in') === location) {
securityParams.push(securityDef);
}
});
return securityParams;
},
/**
* 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} schemaCache - object storing schemaFaker and schmeResolution caches
* @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: function (param, parameterSource, components, schemaCache) {
var paramName = _.get(param, 'name'),
paramSchema = deref.resolveRefs(_.cloneDeep(param.schema), parameterSource, components,
_.get(schemaCache, 'schemaResolutionCache')),
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 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 };
},
/**
* This functiom 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} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {*} - deserialises parameter value
*/
deserialiseParamValue: function (param, paramValue, parameterSource, components, schemaCache) {
var constructedValue,
paramSchema = deref.resolveRefs(_.cloneDeep(param.schema), parameterSource, components,
_.get(schemaCache, 'schemaResolutionCache')),
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 } =
this.getParamSerialisationInfo(param, parameterSource, components, schemaCache);
// 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;
},
/**
* 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: function (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 };
},
/**
* Extracts all child parameters from explodable param
*
* @param {*} schema - Corresponding schema object of parent parameter to be devided into child params
* @param {*} paramKey - Parameter name of parent param object
* @param {*} metaInfo - meta information of param (i.e. required)
* @returns {Array} - Extracted child parameters
*/
extractChildParamSchema: function (schema, paramKey, metaInfo) {
let childParamSchemas = [];
_.forEach(_.get(schema, 'properties', {}), (value, key) => {
if (_.get(value, 'type') === 'object') {
childParamSchemas = _.concat(childParamSchemas, this.extractChildParamSchema(value,
`${paramKey}[${key}]`, metaInfo));
}
else {
let required = _.get(metaInfo, 'required') || false,
description = _.get(metaInfo, 'description') || '',
pathPrefix = _.get(metaInfo, 'pathPrefix');
childParamSchemas.push({
name: `${paramKey}[${key}]`,
schema: value,
description,
required,
isResolvedParam: true,
pathPrefix
});
}
});
return childParamSchemas;
},
/**
* Tests whether given parameter is of complex array type from param key
*
* @param {*} paramKey - Parmaeter key that is to be tested
* @returns {Boolean} - result
*/
isParamComplexArray: function (paramKey) {
// this checks if parameter key numbered element (i.e. itemArray[1] is complex array param)
let regex = /\[[\d]+\]/gm;
return regex.test(paramKey);
},
/**
* Finds valid JSON media type object from content object
*
* @param {*} contentObj - Content Object from schema
* @returns {*} - valid JSON media type if exists
*/
getJsonContentType: function (contentObj) {
let jsonContentType = _.find(_.keys(contentObj), (contentType) => {
let mediaType = this.parseMediaType(contentType);
return mediaType.type === 'application' && (
mediaType.subtype === 'json' || _.endsWith(mediaType.subtype, '+json')
);
});
return jsonContentType;
},
/**
*
* @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 schmeResolution caches
* @param {string} jsonSchemaDialect The schema dialect defined in the OAS object
* @param {Function} callback - For return
* @returns {Array} array of mismatches
*/
checkValueAgainstSchema: function (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,
// This is dereferenced schema (converted to JSON schema for validation)
schema = deref.resolveRefs(openApiSchemaObj, parameterSourceOption, components,
schemaCache.schemaResolutionCache, PROCESSING_TYPE.VALIDATION, 'example', 0, {}, options.stackLimit),
compositeSchema = schema.oneOf || schema.anyOf;
if (needJsonMatching) {
try {
jsonValue = JSON.parse(value);
// If valid JSON is detected, the parsed value should be used
// to determine mismatches
valueToUse = jsonValue;
}
catch (e) {
jsonValue = '';
invalidJson = true;
}
}
// For anyOf and oneOf schemas, validate value against each schema and report result with least mismatches
if (compositeSchema) {
// get mismatches of value against each schema
async.map(compositeSchema, (elementSchema, cb) => {
setTimeout(() => {
this.checkValueAgainstSchema(property, jsonPathPrefix, txnParamName, value,
`${schemaPathPrefix}.${schema.oneOf ? 'oneOf' : 'anyOf'}[${_.findIndex(compositeSchema, elementSchema)}]`,
elementSchema, parameterSourceOption, components, options, schemaCache, jsonSchemaDialect, cb);
}, 0);
}, (err, results) => {
let sortedResults;
if (err) {
return callback(err, []);
}
// return mismatches of schema against which least validation mismatches were found
sortedResults = _.sortBy(results, (res) => { return res.length; });
return callback(null, sortedResults[0]);
});
}
// When processing a reference, schema.type could also be undefined
else if (schema && schema.type) {
if (isKnownType(schema)) {
let isCorrectType;
// Treat unresolved postman collection/environment variable as correct type
if (options.ignoreUnresolvedVariables && this.isPmVariable(valueToUse)) {
isCorrectType = true;
}
else {
isCorrectType = checkIsCorrectType(valueToUse, schema);
}
if (!isCorrectType) {
// if type didn't match, no point checking for AJV
let reason = '',
mismatchObj;
// exclude mismatch errors for nested objects in parameters (at this point simple objects and array should
// be already converted to primitive schema and only nested objects remains as type object/array)
if (_.includes(['QUERYPARAM', 'PATHVARIABLE', 'HEADER'], property) &&
(schema.type === 'object' || schema.type === 'array')) {
return callback(null, []);
}
if (property === 'RESPONSE_BODY' || property === 'BODY') {
// we don't have names for the body, but there's only one
reason = 'The ' + humanPropName;
}
else if (txnParamName) {
// for query params, req/res headers, path vars, we have a name. Praise the lord.
reason = `The ${humanPropName} "${txnParamName}"`;
}
else {
// for query params, req/res headers, path vars, we might not ALWAYS have a name.
reason = `A ${humanPropName}`;
}
reason += ` needs to be of type ${schema.type}, but we found `;
if (!options.shortValidationErrors) {
reason += `"${valueToUse}"`;
}
else if (invalidJson) {
reason += 'invalid JSON';
}
else if (Array.isArray(valueToUse)) {
reason += 'an array instead';
}
else if (typeof valueToUse === 'object') {
reason += 'an object instead';
}
else {
reason += `a ${typeof valueToUse} instead`;
}
mismatchObj = {
property,
transactionJsonPath: jsonPathPrefix,
schemaJsonPath: schemaPathPrefix,
reasonCode: 'INVALID_TYPE',
reason
};
if (options.suggestAvailableFixes) {
mismatchObj.suggestedFix = {
key: txnParamName,
actualValue: valueToUse,
suggestedValue: safeSchemaFaker(openApiSchemaObj || {}, 'example', PROCESSING_TYPE.VALIDATION,
parameterSourceOption, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache,
options.stackLimit)
};
}
return callback(null, [mismatchObj]);
}
// only do AJV if type is array or object
// simpler cases are handled by a type check
if (isCorrectType && needJsonMatching) {
let filteredValidationError = validateSchema(schema, valueToUse, options, jsonSchemaDialect);
if (!_.isEmpty(filteredValidationError)) {
let mismatchObj,
suggestedValue,
fakedValue = safeSchemaFaker(openApiSchemaObj || {}, 'example', PROCESSING_TYPE.VALIDATION,
parameterSourceOption, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache,
options.stackLimit);
// Show detailed validation mismatches for only request/response body
if (options.detailedBlobValidation && needJsonMatching) {
_.forEach(filteredValidationError, (ajvError) => {
let localSchemaPath = ajvError.schemaPath.replace(/\//g, '.').slice(2),
dataPath = formatDataPath(ajvError.instancePath || '');
// discard the leading '.' if it exists
if (dataPath[0] === '.') {
dataPath = dataPath.slice(1);
}
mismatchObj = _.assign({
property: property,
transactionJsonPath: jsonPathPrefix + formatDataPath(ajvError.instancePath),
schemaJsonPath: schemaPathPrefix + '.' + localSchemaPath
}, ajvValidationError(ajvError, { property, humanPropName }));
if (options.suggestAvailableFixes) {
mismatchObj.suggestedFix = {
key: _.split(dataPath, '.').pop(),
actualValue: this.getPathValue(valueToUse, dataPath, null),
suggestedValue: this.getSuggestedValue(fakedValue, valueToUse, ajvError)
};
}
mismatches.push(mismatchObj);
});
}
else {
mismatchObj = {
reason: `The ${humanPropName} didn\'t match the specified schema`,
reasonCode: 'INVALID_TYPE'
};
// assign proper reason codes for invalid body
if (property === 'BODY') {
mismatchObj.reasonCode = 'INVALID_BODY';
}
else if (property === 'RESPONSE_BODY') {
mismatchObj.reasonCode = 'INVALID_RESPONSE_BODY';
}
if (options.suggestAvailableFixes) {
suggestedValue = _.cloneDeep(valueToUse);
// Apply each fix individually to respect existing values in request
_.forEach(filteredValidationError, (ajvError) => {
let dataPath = formatDataPath(ajvError.instancePath || '');
// discard the leading '.' if it exists
if (dataPath[0] === '.') {
dataPath = dataPath.slice(1);
}
// for empty string _.set creates new key with empty string '', so separate handling
if (dataPath === '') {
suggestedValue = this.getSuggestedValue(fakedValue, suggestedValue, ajvError);
}
else {
_.set(suggestedValue, dataPath, this.getSuggestedValue(fakedValue, suggestedValue, ajvError));
}
});
mismatchObj.suggestedFix = {
key: property.toLowerCase(),
actualValue: valueToUse,
suggestedValue
};
}
mismatches.push(_.assign({
property: property,
transactionJsonPath: jsonPathPrefix,
schemaJsonPath: schemaPathPrefix
}, mismatchObj));
}
// only return AJV mismatches
return callback(null, mismatches);
}
// result passed. No AJV mismatch
return callback(null, []);
}
// Schema was not AJV or object
// Req/Res Body was non-object but content type is application/json
else if (needJsonMatching) {
return callback(null, [{
property,
transactionJsonPath: jsonPathPrefix,
schemaJsonPath: schemaPathPrefix,
reasonCode: 'INVALID_TYPE',
reason: `The ${humanPropName} needs to be of type object/array, but we found "${valueToUse}"`,
suggestedFix: {
key: null,
actualValue: valueToUse,
suggestedValue: {} // suggest value to be object
}
}]);
}
else {
return callback(null, []);
}
}
else {
// unknown schema.type found
// TODO: Decide how to handle. Log?
return callback(null, []);
}
}
// Schema not defined
else {
return callback(null, []);
}
// if (!schemaTypeToJsValidator[schema.type](value)) {
// callback(null, [{
// property,
// transactionJsonPath: jsonPathPrefix,
// schemaJsonPath: schemaPathPrefix,
// reasonCode: 'INVALID_TYPE',
// reason: `Value must be a token of type ${schema.type}, found ${value}`
// }]);
// }
// TODO: Further checks for object type
// else {
// callback(null, []);
// }
},
/**
*
* @param {*} determinedPathVariables the key/determined-value pairs of the path variables (from Postman)
* @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 {*} components the components + paths from the OAS spec that need to be used to resolve $refs
* @param {*} options OAS options
* @param {*} schemaCache object storing schemaFaker and schmeResolution caches
* @param {string} jsonSchemaDialect Defined schema dialect at the OAS object
* @param {*} callback Callback
* @returns {array} mismatches (in the callback)
*/
checkPathVariables: function (
determinedPathVariables,
transactionPathPrefix,
schemaPath,
components,
options,
schemaCache,
jsonSchemaDialect,
callback) {
// schema path should have all parameters needed
// components need to be stored globally
var mismatchProperty = 'PATHVARIABLE',
// all path variables defined in this path. acc. to the spec, all path params are required
schemaPathVariables,
pmPathVariables;
if (options.validationPropertiesToIgnore.includes(mismatchProperty)) {
return callback(null, []);
}
// find all schema path variables that can be present as collection path variables
pmPathVariables = this.findPathVariablesFromSchemaPath(schemaPath.schemaPathName);
schemaPathVariables = _.filter(schemaPath.parameters, (param) => {
// exclude path variables stored as collection variable from being validated further
return (param.in === 'path' && _.includes(pmPathVariables, param.name));
});
async.map(determinedPathVariables, (pathVar, cb) => {
let mismatches = [],
resolvedParamValue,
index = _.findIndex(determinedPathVariables, pathVar);
const schemaPathVar = _.find(schemaPathVariables, (param) => {
return param.name === pathVar.key;
});
if (!schemaPathVar) {
// extra pathVar present in given request.
if (options.showMissingInSchemaErrors) {
mismatches.push({
property: mismatchProperty,
// not adding the pathVar name to the jsonPath because URL is just a string
transactionJsonPath: transactionPathPrefix + `[${index}]`,
schemaJsonPath: null,
reasonCode: 'MISSING_IN_SCHEMA',
reason: `The path variable "${pathVar.key}" was not found in the schema`
});
}
return cb(null, mismatches);
}
// don't validate variable if not present in transaction and URL path vars are not allowed
if (!pathVar._varMatched && !options.allowUrlPathVarMatching) {
return cb(null, mismatches);
}
// assign parameter example(s) as schema examples
this.assignParameterExamples(schemaPathVar);
resolvedParamValue = this.deserialiseParamValue(schemaPathVar, pathVar.value, PARAMETER_SOURCE.REQUEST,
components, schemaCache);
setTimeout(() => {
if (!(schemaPathVar && schemaPathVar.schema)) {
// no errors to show if there's no schema present in the spec
return cb(null, []);
}
this.checkValueAgainstSchema(mismatchProperty,
transactionPathPrefix + `[${index}].value`,
pathVar.key,
resolvedParamValue,
schemaPathVar.pathPrefix + '[?(@.name==\'' + schemaPathVar.name + '\')]',
schemaPathVar.schema,
PARAMETER_SOURCE.REQUEST,
components, options, schemaCache, jsonSchemaDialect, cb);
}, 0);
}, (err, res) => {
let mismatches = [],
mismatchObj;
if (err) {
return callback(err);
}
// go through required schemaPathVariables, and params that aren't found in the given transaction are errors
_.each(schemaPathVariables, (pathVar) => {
if (!_.find(determinedPathVariables, (param) => {
// only consider variable matching if url path variables is not allowed
return param.key === pathVar.name && (options.allowUrlPathVarMatching || param._varMatched);
})) {
// assign parameter example(s) as schema examples;
this.assignParameterExamples(pathVar);
mismatchObj = {
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix,
schemaJsonPath: pathVar.pathPrefix,
reasonCode: 'MISSING_IN_REQUEST',
reason: `The required path variable "${pathVar.name}" was not found in the transaction`
};
if (options.suggestAvailableFixes) {
mismatchObj.suggestedFix = {
key: pathVar.name,
actualValue: null,
suggestedValue: {
key: pathVar.name,
value: safeSchemaFaker(pathVar.schema || {}, 'example', PROCESSING_TYPE.VALIDATION,
PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache,
options.stackLimit),
description: this.getParameterDescription(pathVar)
}
};
}
mismatches.push(mismatchObj);
}
});
// res is an array of mismatches (also an array) from all checkValueAgainstSchema calls
return callback(null, _.concat(_.flatten(res), mismatches));
});
},
/**
*
* @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)
*/
checkMetadata(transaction, transactionPathPrefix, schemaPath, pathRoute, options, callback) {
let expectedReqName,
expectedReqDesc,
reqNameMismatch,
actualReqName = _.get(transaction, 'name'),
trimmedReqName,
actualReqDesc,
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 = this.fixPathVariablesInUrl(pathRoute.slice(pathRoute.indexOf('/')));
// convert all /{{one}}/{{two}} to /:one/:two
// Doesn't touch /{{file}}.{{format}}
reqUrl = this.sanitizeUrlPathParams(reqUrl, []).url;
// description can be one of following two
actualReqDesc = _.isObject(_.get(transaction, 'request.description')) ?
_.get(transaction, 'request.description.content') : _.get(transaction, 'request.description');
expectedReqDesc = schemaPath.description;
switch (options.requestNameSource) {
case 'fallback' : {
// operationId is usually camelcase or snake case
expectedReqName = schemaPath.summary || utils.insertSpacesInName(schemaPath.operationId) || 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;
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);
}
/**
* Collection stores empty description as null, while OpenAPI spec can have empty string as description.
* Hence We need to treat null and empty string as match. So check first if both schema and collection description
* are not empty. _.isEmpty() returns true for null/undefined/''(empty string)
* i.e. collection desc = null and schema desc = '', for this case no mismatch will occurr
*/
if ((!_.isEmpty(actualReqDesc) || !_.isEmpty(expectedReqDesc)) && (actualReqDesc !== expectedReqDesc)) {
mismatchObj = {
property: 'REQUEST_DESCRIPTION',
transactionJsonPath: transactionPathPrefix + '.request.description',
schemaJsonPath: null,
reasonCode: 'INVALID_VALUE',
reason: 'The request description didn\'t match with specified schema'
};
options.suggestAvailableFixes && (mismatchObj.suggestedFix = {
key: 'description',
actualValue: actualReqDesc || null,
suggestedValue: expectedReqDesc
});
mismatches.push(mismatchObj);
}
return callback(null, mismatches);
},
checkQueryParams(requestUrl, transactionPathPrefix, schemaPath, components, options,
schemaCache, jsonSchemaDialect, callback) {
let parsedUrl = require('url').parse(requestUrl),
schemaParams = _.filter(schemaPath.parameters, (param) => { return param.in === 'query'; }),
requestQueryArray = [],
requestQueryParams = [],
resolvedSchemaParams = [],
mismatchProperty = 'QUERYPARAM',
securityParams,
urlMalformedError;
if (options.validationPropertiesToIgnore.includes(mismatchProperty)) {
return callback(null, []);
}
if (!parsedUrl.query) {
// null query params should be treated as lack of any params
parsedUrl.query = '';
}
requestQueryArray = parsedUrl.query.split('&');
_.each(requestQueryArray, (rqp) => {
let parts = rqp.split('='),
qKey, qVal;
try {
qKey = decodeURIComponent(parts[0]);
qVal = decodeURIComponent(parts.slice(1).join('='));
}
catch (err) {
return (urlMalformedError = err);
}
if (qKey.length > 0) {
requestQueryParams.push({
key: qKey,
value: qVal
});
}
});
if (urlMalformedError) {
return callback(urlMalformedError);
}
// filter out query params added by security schemes
securityParams = _.map(this.getSecurityParams(_.get(components, 'components'), 'query'), 'name');
requestQueryParams = _.filter(requestQueryParams, (pQuery) => {
return !_.includes(securityParams, pQuery.key);
});
// resolve schema params
// below will make sure for exploded params actual schema of property present in collection is present
_.forEach(schemaParams, (param) => {
let pathPrefix = param.pathPrefix,
paramSchema = deref.resolveRefs(_.cloneDeep(param.schema), PARAMETER_SOURCE.REQUEST, components, schemaCache),
{ style, explode } = this.getParamSerialisationInfo(param, PARAMETER_SOURCE.REQUEST, components, schemaCache),
isPropSeparable = _.includes(['form', 'deepObject'], style);
if (isPropSeparable && paramSchema.type === 'array' && explode) {
/**
* avoid validation of complex array type param as OAS doesn't define serialisation
* of Array with deepObject style
*/
if (!_.includes(['array', 'object'], _.get(paramSchema, 'items.type'))) {
// add schema of corresponding items instead array
resolvedSchemaParams.push(_.assign({}, param, {
schema: _.get(paramSchema, 'items'),
isResolvedParam: true
}));
}
}
else if (isPropSeparable && paramSchema.type === 'object' && explode) {
// resolve all child params of parent param with deepObject style
if (style === 'deepObject') {
resolvedSchemaParams = _.concat(resolvedSchemaParams, this.extractChildParamSchema(paramSchema,
param.name, { required: _.get(param, 'required'), pathPrefix, description: _.get(param, 'description') }));
}
else {
// add schema of all properties instead entire object
_.forEach(_.get(paramSchema, 'properties', {}), (propSchema, propName) => {
resolvedSchemaParams.push({
name: propName,
schema: propSchema,
required: _.get(param, 'required') || false,
description: _.get(param, 'description'),
isResolvedParam: true,
pathPrefix
});
});
}
}
else {
resolvedSchemaParams.push(param);
}
});
return async.map(requestQueryParams, (pQuery, cb) => {
let mismatches = [],
index = _.findIndex(requestQueryParams, pQuery),
resolvedParamValue = pQuery.value;
const schemaParam = _.find(resolvedSchemaParams, (param) => { return param.name === pQuery.key; });
if (!schemaParam) {
// skip validation of complex array params
if (this.isParamComplexArray(pQuery.key)) {
return cb(null, mismatches);
}
if (options.showMissingInSchemaErrors) {
mismatches.push({
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix + `[${index}]`,
schemaJsonPath: null,
reasonCode: 'MISSING_IN_SCHEMA',
reason: `The query parameter ${pQuery.key} was not found in the schema`
});
}
return cb(null, mismatches);
}
// assign parameter example(s) as schema examples;
this.assignParameterExamples(schemaParam);
if (!schemaParam.isResolvedParam) {
resolvedParamValue = this.deserialiseParamValue(schemaParam, pQuery.value, PARAMETER_SOURCE.REQUEST,
components, schemaCache);
}
// query found in spec. check query's schema
setTimeout(() => {
if (!schemaParam.schema) {
// no errors to show if there's no schema present in the spec
return cb(null, []);
}
this.checkValueAgainstSchema(mismatchProperty,
transactionPathPrefix + `[${index}].value`,
pQuery.key,
resolvedParamValue,
schemaParam.pathPrefix + '[?(@.name==\'' + schemaParam.name + '\')]',
schemaParam.schema,
PARAMETER_SOURCE.REQUEST,
components, options, schemaCache, jsonSchemaDialect, cb
);
}, 0);
}, (err, res) => {
let mismatches = [],
mismatchObj;
_.each(_.filter(resolvedSchemaParams, (q) => { return q.required; }), (qp) => {
if (!_.find(requestQueryParams, (param) => { return param.key === qp.name; })) {
// assign parameter example(s) as schema examples;
this.assignParameterExamples(qp);
mismatchObj = {
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix,
schemaJsonPath: qp.pathPrefix + '[?(@.name==\'' + qp.name + '\')]',
reasonCode: 'MISSING_IN_REQUEST',
reason: `The required query parameter "${qp.name}" was not found in the transaction`
};
if (options.suggestAvailableFixes) {
mismatchObj.suggestedFix = {
key: qp.name,
actualValue: null,
suggestedValue: {
key: qp.name,
value: safeSchemaFaker(qp.schema || {}, 'example', PROCESSING_TYPE.VALIDATION,
PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache,
options.stackLimit),
description: this.getParameterDescription(qp)
}
};
}
mismatches.push(mismatchObj);
}
});
return callback(null, _.concat(_.flatten(res), mismatches));
});
},
/**
* 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
*/
checkContentTypeHeader: function (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 = this.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 = this.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 = this.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;
},
checkRequestHeaders: function (headers, transactionPathPrefix, schemaPathPrefix, schemaPath,
components, options, schemaCache, jsonSchemaDialect, callback) {
let schemaHeaders = _.filter(schemaPath.parameters, (param) => { return param.in === 'header'; }),
// key name of headers which are added by security schemes
securityHeaders = _.map(this.getSecurityParams(_.get(components, 'components'), 'header'), 'name'),
// filter out headers for following cases
reqHeaders = _.filter(headers, (header) => {
// 1. which need explicit handling according to schema (other than parameters object)
// 2. which are added by security schemes
return !_.includes(IMPLICIT_HEADERS, _.toLower(_.get(header, 'key'))) &&
!_.includes(securityHeaders, header.key);
}),
mismatchProperty = 'HEADER';
if (options.validationPropertiesToIgnore.includes(mismatchProperty)) {
return callback(null, []);
}
// 1. for each header, find relevant schemaPath property
return async.map(reqHeaders, (pHeader, cb) => {
let mismatches = [],
resolvedParamValue,
index = _.findIndex(headers, pHeader); // find actual index from collection request headers
const schemaHeader = _.find(schemaHeaders, (header) => { return header.name === pHeader.key; });
if (!schemaHeader) {
// no schema header found
if (options.showMissingInSchemaErrors) {
mismatches.push({
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix + `[${index}]`,
schemaJsonPath: null,
reasonCode: 'MISSING_IN_SCHEMA',
reason: `The header ${pHeader.key} was not found in the schema`
});
}
return cb(null, mismatches);
}
// assign parameter example(s) as schema examples;
this.assignParameterExamples(schemaHeader);
resolvedParamValue = this.deserialiseParamValue(schemaHeader, pHeader.value, PARAMETER_SOURCE.REQUEST,
components, schemaCache);
// header found in spec. check header's schema
setTimeout(() => {
if (!schemaHeader.schema) {
// no errors to show if there's no schema present in the spec
return cb(null, []);
}
this.checkValueAgainstSchema(mismatchProperty,
transactionPathPrefix + `[${index}].value`,
pHeader.key,
resolvedParamValue,
schemaHeader.pathPrefix + '[?(@.name==\'' + schemaHeader.name + '\')]',
schemaHeader.schema,
PARAMETER_SOURCE.REQUEST,
components, options, schemaCache, jsonSchemaDialect, cb
);
}, 0);
}, (err, res) => {
let mismatches = [],
mismatchObj,
reqBody = _.get(schemaPath, 'requestBody'),
contentHeaderMismatches = [];
// resolve $ref in request body if present
if (reqBody) {
if (_.has(reqBody, '$ref')) {
reqBody = this.getRefObject(reqBody.$ref, components, options);
}
contentHeaderMismatches = this.checkContentTypeHeader(headers, transactionPathPrefix,
schemaPathPrefix + '.requestBody.content', _.get(reqBody, 'content'),
mismatchProperty, options);
}
_.each(_.filter(schemaHeaders, (h) => {
// exclude non-required and implicit header from further validation
return h.required && !_.includes(IMPLICIT_HEADERS, _.toLower(h.name));
}), (header) => {
if (!_.find(reqHeaders, (param) => { return param.key === header.name; })) {
// assign parameter example(s) as schema examples;
this.assignParameterExamples(header);
mismatchObj = {
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix,
schemaJsonPath: header.pathPrefix + '[?(@.name==\'' + header.name + '\')]',
reasonCode: 'MISSING_IN_REQUEST',
reason: `The required header "${header.name}" was not found in the transaction`
};
if (options.suggestAvailableFixes) {
mismatchObj.suggestedFix = {
key: header.name,
actualValue: null,
suggestedValue: {
key: header.name,
value: safeSchemaFaker(header.schema || {}, 'example', PROCESSING_TYPE.VALIDATION,
PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache,
options.stackLimit),
description: this.getParameterDescription(header)
}
};
}
mismatches.push(mismatchObj);
}
});
return callback(null, _.concat(contentHeaderMismatches, _.flatten(res), mismatches));
});
},
checkResponseHeaders: function (schemaResponse, headers, transactionPathPrefix, schemaPathPrefix,
components, options, schemaCache, jsonSchemaDialect, callback) {
// 0. Need to find relevant response from schemaPath.responses
let schemaHeaders,
// filter out headers which need explicit handling according to schema (other than parameters object)
resHeaders = _.filter(headers, (header) => {
return !_.includes(IMPLICIT_HEADERS, _.toLower(_.get(header, 'key')));
}),
mismatchProperty = 'RESPONSE_HEADER';
if (options.validationPropertiesToIgnore.includes(mismatchProperty)) {
return callback(null, []);
}
if (!schemaResponse) {
// no default response found, we can't call it a mismatch
return callback(null, []);
}
schemaHeaders = schemaResponse.headers;
return async.map(resHeaders, (pHeader, cb) => {
let mismatches = [],
index = _.findIndex(headers, pHeader); // find actual index from collection response headers
const schemaHeader = _.get(schemaHeaders, pHeader.key);
if (!schemaHeader) {
// no schema header found
if (options.showMissingInSchemaErrors) {
mismatches.push({
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix + `[${index}]`,
schemaJsonPath: schemaPathPrefix + '.headers',
reasonCode: 'MISSING_IN_SCHEMA',
reason: `The header ${pHeader.key} was not found in the schema`
});
}
return cb(null, mismatches);
}
// assign parameter example(s) as schema examples;
this.assignParameterExamples(schemaHeader);
// header found in spec. check header's schema
setTimeout(() => {
if (!schemaHeader.schema) {
// no errors to show if there's no schema present in the spec
return cb(null, []);
}
return this.checkValueAgainstSchema(mismatchProperty,
transactionPathPrefix + `[${index}].value`,
pHeader.key,
pHeader.value,
schemaPathPrefix + '.headers[' + pHeader.key + ']',
schemaHeader.schema,
PARAMETER_SOURCE.RESPONSE,
components, options, schemaCache, jsonSchemaDialect, cb
);
}, 0);
}, (err, res) => {
let mismatches = [],
mismatchObj,
contentHeaderMismatches = this.checkContentTypeHeader(headers, transactionPathPrefix,
schemaPathPrefix + '.content', _.get(schemaResponse, 'content'), mismatchProperty, options);
_.each(_.filter(schemaHeaders, (h, hName) => {
// exclude empty headers fron validation
if (_.isEmpty(h)) {
return false;
}
h.name = hName;
// exclude non-required and implicit header from further validation
return h.required && !_.includes(IMPLICIT_HEADERS, _.toLower(hName));
}), (header) => {
if (!_.find(resHeaders, (param) => { return param.key === header.name; })) {
// assign parameter example(s) as schema examples;
this.assignParameterExamples(header);
mismatchObj = {
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix,
schemaJsonPath: schemaPathPrefix + '.headers[\'' + header.name + '\']',
reasonCode: 'MISSING_IN_REQUEST',
reason: `The required response header "${header.name}" was not found in the transaction`
};
if (options.suggestAvailableFixes) {
mismatchObj.suggestedFix = {
key: header.name,
actualValue: null,
suggestedValue: {
key: header.name,
value: safeSchemaFaker(header.schema || {}, 'example', PROCESSING_TYPE.VALIDATION,
PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache,
options.stackLimit),
description: this.getParameterDescription(header)
}
};
}
mismatches.push(mismatchObj);
}
});
callback(null, _.concat(contentHeaderMismatches, _.flatten(res), mismatches));
});
},
// Only application/json and application/x-www-form-urlencoded is validated for now
checkRequestBody: function (requestBody, transactionPathPrefix, schemaPathPrefix, schemaPath,
components, options, schemaCache, jsonSchemaDialect, callback) {
// check for body modes
let jsonSchemaBody,
jsonContentType,
mismatchProperty = 'BODY';
if (options.validationPropertiesToIgnore.includes(mismatchProperty)) {
return callback(null, []);
}
// resolve $ref in requestBody object if present
if (!_.isEmpty(_.get(schemaPath, 'requestBody.$ref'))) {
schemaPath.requestBody = this.getRefObject(schemaPath.requestBody.$ref, components, options);
}
// get valid json content type
jsonContentType = this.getJsonContentType(_.get(schemaPath, 'requestBody.content', {}));
jsonSchemaBody = _.get(schemaPath, ['requestBody', 'content', jsonContentType, 'schema']);
if (requestBody && requestBody.mode === 'raw' && jsonSchemaBody) {
setTimeout(() => {
return this.checkValueAgainstSchema(mismatchProperty,
transactionPathPrefix,
null, // no param name for the request body
requestBody.raw,
schemaPathPrefix + '.requestBody.content[' + jsonContentType + '].schema',
jsonSchemaBody,
PARAMETER_SOURCE.REQUEST,
components,
_.extend({}, options, { shortValidationErrors: true }),
schemaCache,
jsonSchemaDialect,
callback
);
}, 0);
}
else if (requestBody && requestBody.mode === 'urlencoded') {
let urlencodedBodySchema = _.get(schemaPath, ['requestBody', 'content', URLENCODED, 'schema']),
resolvedSchemaParams = [],
pathPrefix = `${schemaPathPrefix}.requestBody.content[${URLENCODED}].schema`;
urlencodedBodySchema = deref.resolveRefs(urlencodedBodySchema, PARAMETER_SOURCE.REQUEST, components,
schemaCache.schemaResolutionCache, PROCESSING_TYPE.VALIDATION, 'example', 0, {}, options.stackLimit);
// resolve each property as separate param similar to query parmas
_.forEach(_.get(urlencodedBodySchema, 'properties'), (propSchema, propName) => {
let resolvedProp = {
name: propName,
schema: propSchema,
in: 'query', // serialization follows same behaviour as query params
description: _.get(propSchema, 'description') || ''
},
encodingValue = _.get(schemaPath, ['requestBody', 'content', URLENCODED, 'encoding', propName]),
pSerialisationInfo,
isPropSeparable;
if (_.isObject(encodingValue)) {
_.has(encodingValue, 'style') && (resolvedProp.style = encodingValue.style);
_.has(encodingValue, 'explode') && (resolvedProp.explode = encodingValue.explode);
}
if (_.includes(_.get(urlencodedBodySchema, 'required'), propName)) {
resolvedProp.required = true;
}
pSerialisationInfo = this.getParamSerialisationInfo(resolvedProp, PARAMETER_SOURCE.REQUEST,
components, schemaCache);
isPropSeparable = _.includes(['form', 'deepObject'], pSerialisationInfo.style);
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 (!_.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) {
// resolve all child params of parent param with deepObject style
if (pSerialisationInfo.style === 'deepObject') {
resolvedSchemaParams = _.concat(resolvedSchemaParams, this.extractChildParamSchema(propSchema,
propName, { required: resolvedProp.required || false, description: resolvedProp.description }));
}
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 || false,
description: resolvedProp.description
});
});
}
}
else {
resolvedSchemaParams.push(resolvedProp);
}
});
return async.map(requestBody.urlencoded, (uParam, cb) => {
let mismatches = [],
index = _.findIndex(requestBody.urlencoded, uParam),
resolvedParamValue = uParam.value;
const schemaParam = _.find(resolvedSchemaParams, (param) => { return param.name === uParam.key; });
if (!schemaParam) {
// skip validation of complex array params
if (this.isParamComplexArray(uParam.key)) {
return cb(null, mismatches);
}
if (options.showMissingInSchemaErrors) {
mismatches.push({
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix + `.urlencoded[${index}]`,
schemaJsonPath: null,
reasonCode: 'MISSING_IN_SCHEMA',
reason: `The Url Encoded body param "${uParam.key}" was not found in the schema`
});
}
return cb(null, mismatches);
}
if (!schemaParam.isResolvedParam) {
resolvedParamValue = this.deserialiseParamValue(schemaParam, uParam.value, PARAMETER_SOURCE.REQUEST,
components, schemaCache);
}
// store value of transaction to use in mismatch object
schemaParam.actualValue = uParam.value;
// param found in spec. check param's schema
setTimeout(() => {
if (!schemaParam.schema) {
// no errors to show if there's no schema present in the spec
return cb(null, []);
}
this.checkValueAgainstSchema(mismatchProperty,
transactionPathPrefix + `.urlencoded[${index}].value`,
uParam.key,
resolvedParamValue,
pathPrefix + '.properties[' + schemaParam.name + ']',
schemaParam.schema,
PARAMETER_SOURCE.REQUEST,
components, options, schemaCache, jsonSchemaDialect, cb
);
}, 0);
}, (err, res) => {
let mismatches = [],
mismatchObj,
// fetches property name from schem path
getPropNameFromSchemPath = (schemaPath) => {
let regex = /\.properties\[(.+)\]/gm;
return _.last(regex.exec(schemaPath));
};
// update actual value and suggested value from JSON to serialized strings
_.forEach(_.flatten(res), (mismatchObj) => {
if (!_.isEmpty(mismatchObj)) {
let propertyName = getPropNameFromSchemPath(mismatchObj.schemaJsonPath),
schemaParam = _.find(resolvedSchemaParams, (param) => { return param.name === propertyName; }),
serializedParamValue;
if (schemaParam) {
// serialize param value (to be used in suggested value)
serializedParamValue = _.get(this.convertParamsWithStyle(schemaParam, _.get(mismatchObj,
'suggestedFix.suggestedValue'), PARAMETER_SOURCE.REQUEST, components, schemaCache, options),
'[0].value');
_.set(mismatchObj, 'suggestedFix.actualValue', schemaParam.actualValue);
_.set(mismatchObj, 'suggestedFix.suggestedValue', serializedParamValue);
}
}
});
_.each(resolvedSchemaParams, (uParam) => {
// report mismatches only for required properties
if (!_.find(requestBody.urlencoded, (param) => { return param.key === uParam.name; }) && uParam.required) {
mismatchObj = {
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix + '.urlencoded',
schemaJsonPath: pathPrefix + '.properties[' + uParam.name + ']',
reasonCode: 'MISSING_IN_REQUEST',
reason: `The Url Encoded body param "${uParam.name}" was not found in the transaction`
};
if (options.suggestAvailableFixes) {
mismatchObj.suggestedFix = {
key: uParam.name,
actualValue: null,
suggestedValue: {
key: uParam.name,
value: safeSchemaFaker(uParam.schema || {}, 'example', PROCESSING_TYPE.VALIDATION,
PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache,
options.stackLimit),
description: this.getParameterDescription(uParam)
}
};
}
mismatches.push(mismatchObj);
}
});
return callback(null, _.concat(_.flatten(res), mismatches));
});
}
else {
return callback(null, []);
}
},
checkResponseBody: function (schemaResponse, body, transactionPathPrefix, schemaPathPrefix,
components, options, schemaCache, jsonSchemaDialect, callback) {
let schemaContent,
jsonContentType,
mismatchProperty = 'RESPONSE_BODY';
if (options.validationPropertiesToIgnore.includes(mismatchProperty)) {
return callback(null, []);
}
// get valid json content type
jsonContentType = this.getJsonContentType(_.get(schemaResponse, 'content', {}));
schemaContent = _.get(schemaResponse, ['content', jsonContentType, 'schema']);
if (!schemaContent) {
// no specific or default response with application/json
// return callback(null, [{
// property: mismatchProperty,
// transactionJsonPath: transactionPathPrefix,
// schemaJsonPath: null,
// reasonCode: 'BODY_SCHEMA_NOT_FOUND',
// reason: 'No JSON schema found for this response'
// }]);
// cannot show mismatches if the schema didn't have any application/JSON response
return callback(null, []);
}
setTimeout(() => {
return this.checkValueAgainstSchema(mismatchProperty,
transactionPathPrefix,
null, // no param name for the response body
body,
schemaPathPrefix + '.content[' + jsonContentType + '].schema',
schemaContent,
PARAMETER_SOURCE.RESPONSE,
components,
_.extend({}, options, { shortValidationErrors: true }),
schemaCache,
jsonSchemaDialect,
callback
);
}, 0);
},
checkResponses: function (responses, transactionPathPrefix, schemaPathPrefix, schemaPath,
components, options, schemaCache, jsonSchemaDialect, cb) {
// responses is an array of repsonses recd. for one Postman request
// we've already determined the schemaPath against which all responses need to be validated
// loop through all responses
// for each response, find the appropriate response from schemaPath, and then validate response body and headers
async.map(responses, (response, responseCallback) => {
let thisResponseCode = response.code,
thisSchemaResponse = _.get(schemaPath, ['responses', thisResponseCode]),
responsePathPrefix = thisResponseCode;
// find this code from the schemaPath
if (!thisSchemaResponse) {
// could not find an appropriate response for this code. check default?
thisSchemaResponse = _.get(schemaPath, ['responses', 'default']);
responsePathPrefix = 'default';
}
// resolve $ref in response object if present
if (!_.isEmpty(_.get(thisSchemaResponse, '$ref'))) {
thisSchemaResponse = this.getRefObject(thisSchemaResponse.$ref, components, options);
}
// resolve $ref in all header objects if present
_.forEach(_.get(thisSchemaResponse, 'headers'), (header) => {
if (_.has(header, '$ref')) {
_.assign(header, this.getRefObject(header.$ref, components, options));
_.unset(header, '$ref');
}
});
if (!thisSchemaResponse) {
// still didn't find a response
responseCallback(null);
}
else {
// check headers and body
async.parallel({
headers: (cb) => {
this.checkResponseHeaders(thisSchemaResponse, response.header,
transactionPathPrefix + '[' + response.id + '].header',
schemaPathPrefix + '.responses.' + responsePathPrefix,
components, options, schemaCache, jsonSchemaDialect, cb);
},
body: (cb) => {
// assume it's JSON at this point
this.checkResponseBody(thisSchemaResponse, response.body,
transactionPathPrefix + '[' + response.id + '].body',
schemaPathPrefix + '.responses.' + responsePathPrefix,
components, options, schemaCache, jsonSchemaDialect, cb);
}
}, (err, result) => {
return responseCallback(null, {
id: response.id,
matched: (result.body.length === 0 && result.headers.length === 0),
mismatches: result.body.concat(result.headers)
});
});
}
}, (err, result) => {
var retVal = _.keyBy(_.reject(result, (ai) => { return !ai; }), 'id');
return cb(null, retVal);
});
},
/**
* Takes in the postman path and the schema path
* takes from the path the number of segments present in the schema path
* and returns the last segments from the path to match in an string format
*
* @param {string} pathToMatch - parsed path (exclude host and params) from the Postman request
* @param {string} schemaPath - schema path from the OAS spec (exclude servers object)
* @returns {string} only the selected segments from the pathToMatch
*/
handleExplicitServersPathToMatch: function (pathToMatch, schemaPath) {
let pathTMatchSlice,
fragment = '',
schemaPathArr = _.reject(schemaPath.split('/'), (segment) => {
return segment === '';
}),
schemaPathSegments = schemaPathArr.length,
pathToMatchArr = _.reject(pathToMatch.split('/'), (segment) => {
return segment === '';
}),
pathToMatchSegments = pathToMatchArr.length,
fragments = schemaPath.split('#');
if (fragments.length > 1) {
fragment = '#' + fragments[1];
}
if (pathToMatchSegments < schemaPathSegments) {
return pathToMatch;
}
pathTMatchSlice = pathToMatchArr.slice(pathToMatchArr.length - schemaPathSegments, pathToMatchArr.length);
return pathTMatchSlice.join('/') + fragment;
},
/**
* @param {string} postmanPath - parsed path (exclude host and params) from the Postman request
* @param {string} schemaPath - schema path from the OAS spec (exclude servers object)
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
* @returns {*} score + match + pathVars - higher score - better match. null - no match
*/
getPostmanUrlSchemaMatchScore: function (postmanPath, schemaPath, options) {
var postmanPathArr = _.reject(postmanPath.split('/'), (segment) => {
return segment === '';
}),
schemaPathArr = _.reject(schemaPath.split('/'), (segment) => {
return segment === '';
}),
matchedPathVars = null,
maxScoreFound = -Infinity,
anyMatchFound = false,
fixedMatchedSegments,
variableMatchedSegments,
postmanPathSuffixes = [];
// get array with all suffixes of postmanPath
// if postmanPath = {{url}}/a/b, the suffix array is [ [{{url}}, a, b] , [a, b] , [b]]
for (let i = postmanPathArr.length; i > 0; i--) {
// i will be 3, 2, 1
postmanPathSuffixes.push(postmanPathArr.slice(-i));
break; // we only want one item in the suffixes array for now
}
// for each suffx, calculate score against the schemaPath
// the schema<>postman score is the sum
_.each(postmanPathSuffixes, (pps) => {
let suffixMatchResult = this.getPostmanUrlSuffixSchemaScore(pps, schemaPathArr, options);
if (suffixMatchResult.match && suffixMatchResult.score > maxScoreFound) {
maxScoreFound = suffixMatchResult.score;
matchedPathVars = suffixMatchResult.pathVars;
// No. of fixed segment matches between schema and postman url path
fixedMatchedSegments = suffixMatchResult.fixedMatchedSegments;
// No. of variable segment matches between schema and postman url path
variableMatchedSegments = suffixMatchResult.variableMatchedSegments;
anyMatchFound = true;
}
});
// handle root path '/'
if (postmanPath === '/' && schemaPath === '/') {
anyMatchFound = true;
maxScoreFound = 1; // assign max possible score
matchedPathVars = []; // no path variables present
fixedMatchedSegments = 0;
variableMatchedSegments = 0;
}
if (anyMatchFound) {
return {
match: true,
score: maxScoreFound,
pathVars: matchedPathVars,
fixedMatchedSegments,
variableMatchedSegments
};
}
return {
match: false
};
},
/**
* @param {*} pmSuffix - Collection request's path suffix array
* @param {*} schemaPath - schema operation's path suffix array
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
* @returns {*} score - null of no match, int for match. higher value indicates better match
* You get points for the number of URL segments that match
* You are penalized for the number of schemaPath segments that you skipped
*/
getPostmanUrlSuffixSchemaScore: function (pmSuffix, schemaPath, options) {
let mismatchFound = false,
variables = [],
minLength = Math.min(pmSuffix.length, schemaPath.length),
sMax = schemaPath.length - 1,
pMax = pmSuffix.length - 1,
matchedSegments = 0,
// No. of fixed segment matches between schema and postman url path
fixedMatchedSegments = 0,
// No. of variable segment matches between schema and postman url path
variableMatchedSegments = 0,
// checks if schema segment provided is path variable
isSchemaSegmentPathVar = (segment) => {
return segment.startsWith('{') &&
segment.endsWith('}') &&
// check that only one path variable is present as collection path variable can contain only one var
segment.indexOf('}') === segment.lastIndexOf('}');
};
if (options.strictRequestMatching && pmSuffix.length !== schemaPath.length) {
return {
match: false,
score: null,
pathVars: []
};
}
// start from the last segment of both
// segments match if the schemaPath segment is {..} or the postmanPathStr is :<anything> or {{anything}}
// for (let i = pmSuffix.length - 1; i >= 0; i--) {
for (let i = 0; i < minLength; i++) {
let schemaFixedParts = this.getFixedPartsFromPathSegment(schemaPath[sMax - i], 'schema'),
collectionFixedParts = this.getFixedPartsFromPathSegment(pmSuffix[pMax - i], 'collection');
if (
(_.isEqual(schemaFixedParts, collectionFixedParts)) || // exact fixed parts match
(isSchemaSegmentPathVar(schemaPath[sMax - i])) || // schema segment is a pathVar
(pmSuffix[pMax - i].startsWith(':')) || // postman segment is a pathVar
(this.isPmVariable(pmSuffix[pMax - i])) // postman segment is an env/collection var
) {
// for variable match increase variable matched segments count (used for determining order for multiple matches)
if (
(isSchemaSegmentPathVar(schemaPath[sMax - i])) && // schema segment is a pathVar
((pmSuffix[pMax - i].startsWith(':')) || // postman segment is a pathVar
(this.isPmVariable(pmSuffix[pMax - i]))) // postman segment is an env/collection var
) {
variableMatchedSegments++;
}
// for exact match increase fix matched segments count (used for determining order for multiple matches)
else if (_.isEqual(schemaFixedParts, collectionFixedParts)) {
fixedMatchedSegments++;
}
// add a matched path variable only if the schema one was a pathVar and only one path variable is in segment
if (isSchemaSegmentPathVar(schemaPath[sMax - i])) {
variables.push({
key: schemaPath[sMax - i].substring(1, schemaPath[sMax - i].length - 1),
value: pmSuffix[pMax - i]
});
}
matchedSegments++;
}
else {
// there was one segment for which there was no mismatch
mismatchFound = true;
break;
}
}
if (!mismatchFound) {
return {
match: true,
// schemaPath endsWith postman path suffix
// score is length of the postman path array + schema array - length difference
// the assumption is that a longer path matching a longer path is a higher score, with
// penalty for any length difference
// schemaPath will always be > postmanPathSuffix because SchemaPath ands with pps
score: ((2 * matchedSegments) / (schemaPath.length + pmSuffix.length)),
fixedMatchedSegments,
variableMatchedSegments,
pathVars: _.reverse(variables) // keep index in order of left to right
};
}
return {
match: false,
score: null,
pathVars: []
};
},
/**
* 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
*/
getSuggestedValue: function (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 = this.getPathValue(actualValue, dataPath, {});
targetFakedValue = this.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 = this.getPathValue(fakedValue, dataPath, null);
break;
}
return suggestedValue;
},
/**
* @param {*} schema OpenAPI spec
* @param {Array} matchedEndpoints - All matched endpoints
* @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 {object} schemaCache - object storing schemaFaker and schmeResolution caches
* @returns {Array} - Array of all MISSING_ENDPOINT objects
*/
getMissingSchemaEndpoints: function (schema, matchedEndpoints, components, options, schemaCache) {
let endpoints = [],
schemaPaths = schema.paths,
rootCollectionVariables,
schemaJsonPath;
// collection variables generated for resolving for baseUrl and variables
rootCollectionVariables = this.convertToPmCollectionVariables(
schema.baseUrlVariables,
'baseUrl',
schema.baseUrl
);
_.forEach(schemaPaths, (schemaPathObj, schemaPath) => {
_.forEach(_.keys(schemaPathObj), (pathKey) => {
schemaJsonPath = `$.paths[${schemaPath}].${_.toLower(pathKey)}`;
if (METHODS.includes(pathKey) && !matchedEndpoints.includes(schemaJsonPath)) {
let mismatchObj = {
property: 'ENDPOINT',
transactionJsonPath: null,
schemaJsonPath,
reasonCode: 'MISSING_ENDPOINT',
reason: `The endpoint "${_.toUpper(pathKey)} ${schemaPath}" is missing in collection`,
endpoint: _.toUpper(pathKey) + ' ' + schemaPath
};
if (options.suggestAvailableFixes) {
let operationItem = _.get(schemaPathObj, pathKey) || {},
convertedRequest,
variables = rootCollectionVariables,
path = schemaPath,
request;
// add common parameters of path level
operationItem.parameters = this.getRequestParams(operationItem.parameters,
_.get(schemaPathObj, 'parameters'), components, options);
// discard the leading slash, if it exists
if (path[0] === '/') {
path = path.substring(1);
}
// override root level collection variables (baseUrl and vars) with path level server url and vars if exists
// storing common path/collection vars from the server object at the path item level
if (!_.isEmpty(_.get(schemaPathObj, 'servers'))) {
let pathLevelServers = schemaPathObj.servers;
// add path level server object's URL as collection variable
variables = this.convertToPmCollectionVariables(
pathLevelServers[0].variables, // these are path variables in the server block
this.fixPathVariableName(path), // the name of the variable
this.fixPathVariablesInUrl(pathLevelServers[0].url)
);
}
request = {
name: operationItem.summary || operationItem.description,
method: pathKey,
path: schemaPath[0] === '/' ? schemaPath.substring(1) : schemaPath,
properties: operationItem,
type: 'item',
servers: _.isEmpty(_.get(schemaPathObj, 'servers'))
};
// convert request to collection item and store collection variables
convertedRequest = this.convertRequestToItem(schema, request, components, options, schemaCache, variables);
mismatchObj.suggestedFix = {
key: pathKey,
actualValue: null,
// Not adding colloection variables for now
suggestedValue: {
request: convertedRequest,
variables: _.values(variables)
}
};
}
endpoints.push(mismatchObj);
}
});
});
return endpoints;
},
inputValidation,
/**
* Maps the input from detect root files to get root files
* @param {object} input - input schema
* @returns {Array} - Array of all MISSING_ENDPOINT objects
*/
mapDetectRootFilesInputToGetRootFilesInput(input) {
let adaptedData = input.data.map((file) => {
return { fileName: file.fileName };
});
return { data: adaptedData };
},
/**
* Maps the input from detect root files to get root files
* @param {object} input - input schema
* @returns {Array} - Array of all MISSING_ENDPOINT objects
*/
mapDetectRootFilesInputToFolderInput(input) {
let adaptedData = input.data.map((file) => {
if (file.content) {
return { fileName: file.path, content: file.content };
}
else {
return { fileName: file.path };
}
});
input.data = adaptedData;
return input;
},
/**
* Maps the output from get root files to detect root files
* @param {object} output - output schema
* @param {string} version - specified version of the process
* @returns {object} - Detect root files result object
*/
mapGetRootFilesOutputToDetectRootFilesOutput(output, version) {
if (!version) {
version = '3.0';
}
let adaptedData = output.map((file) => {
return { path: file };
});
return {
result: true,
output: {
type: 'rootFiles',
specification: {
type: 'OpenAPI',
version: version
},
data: adaptedData
}
};
},
/**
*
* @description Takes in a the root files obtains the related files and
* generates the result object
* @param {object} parsedRootFiles - found parsed root files
* @param {array} inputData - file data information [{path, content}]
* @param {Array} origin - process origin (BROWSER or node)
*
* @returns {object} process result { rootFile, relatedFiles, missingRelatedFiles }
*/
getRelatedFilesData(parsedRootFiles, inputData, origin) {
const data = parsedRootFiles.map((root) => {
let relatedData = getRelatedFiles(root, inputData, origin),
result = {
rootFile: { path: root.fileName },
relatedFiles: relatedData.relatedFiles,
missingRelatedFiles: relatedData.missingRelatedFiles
};
return result;
});
return data;
},
/**
* Maps the output for each bundled root file
* @param {object} format - defined output format from options
* @param {string} parsedRootFiles - The parsed root files
* @param {string} version - specified version of the process
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
* @returns {object} - { rootFile: { path }, bundledContent }
*/
mapBundleOutput(format, parsedRootFiles, version, options = {}) {
return (contentAndComponents) => {
let bundledFile = contentAndComponents.fileContent,
bundleOutput;
if (isSwagger(version)) {
Object.entries(contentAndComponents.components).forEach(([key, value]) => {
bundledFile[key] = value;
});
}
else if (!_.isEmpty(contentAndComponents.components)) {
bundledFile.components = contentAndComponents.components;
}
if (!format) {
let rootFormat = parsedRootFiles.find((inputRoot) => {
return inputRoot.fileName === contentAndComponents.fileName;
}).parsed.inputFormat;
if (rootFormat.toLowerCase() === parse.YAML_FORMAT) {
bundledFile = parse.toYAML(bundledFile);
}
else if (rootFormat.toLowerCase() === parse.JSON_FORMAT) {
bundledFile = parse.toJSON(bundledFile, null);
}
}
else if (format.toLowerCase() === parse.YAML_FORMAT) {
bundledFile = parse.toYAML(bundledFile);
}
else if (format.toLowerCase() === parse.JSON_FORMAT) {
bundledFile = parse.toJSON(bundledFile, null);
}
bundleOutput = {
rootFile: { path: contentAndComponents.fileName },
bundledContent: bundledFile
};
if (options.includeReferenceMap) {
bundleOutput.referenceMap = contentAndComponents.referenceMap;
}
return bundleOutput;
};
},
/*
* @description Takes in parsed root files and bundle it
* @param {object} parsedRootFiles - found parsed root files
* @param {array} inputData - file data information [{path, content}]
* @param {Array} origin - process origin (BROWSER or node)
* @param {string} format - output format could be either YAML or JSON
* @param {string} version - specification version specified in the input
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
*
* @returns {object} process result { rootFile, bundledContent }
*/
getBundledFileData(parsedRootFiles, inputData, origin, format, version, options = {}) {
const data = parsedRootFiles.map((root) => {
let bundleData = getBundleContentAndComponents(root, inputData, origin, version);
return bundleData;
});
let bundleData = data.map(this.mapBundleOutput(format, parsedRootFiles, version, options));
return bundleData;
},
/**
*
* @description Takes in root files, input data and origin process every root
* to find related files
* @param {object} rootFiles - rootFile:{path:string}
* @param {array} inputData - [{path:string}]}
* @param {string} origin - process origin
* @param {string} version - process specification version
* @param {string} format - the format required by the user
* @param {boolean} toBundle - if it will be used in bundle
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
*
* @returns {object} root files information and data input
*/
mapProcessRelatedFiles(rootFiles, inputData, origin, version, format, toBundle = false, options = {}) {
let bundleFormat = format,
parsedRootFiles = rootFiles.map((rootFile) => {
let parsedContent = parseFileOrThrow(rootFile.content);
return { fileName: rootFile.fileName, content: rootFile.content, parsed: parsedContent };
}).filter((rootWithParsedContent) => {
let fileVersion = isSwagger(version) ?
rootWithParsedContent.parsed.oasObject.swagger :
rootWithParsedContent.parsed.oasObject.openapi;
return compareVersion(version, fileVersion);
}),
data = toBundle ?
this.getBundledFileData(parsedRootFiles, inputData, origin, bundleFormat, version, options) :
this.getRelatedFilesData(parsedRootFiles, inputData, origin);
return data;
},
/**
*
* @description Takes in a folder and identifies the related files from the
* root file perspective (using $ref property)
* @param {string} inputRelatedFiles - {rootFile:{path:string}, data: [{path:string}]}
* @param {boolean} toBundle - if true it will return the bundle data
* @param {object} options - a standard list of options that's globally passed around. Check options.js for more.
*
* @returns {object} root files information and data input
*/
processRelatedFiles(inputRelatedFiles, toBundle = false, options = {}) {
let version = inputRelatedFiles.specificationVersion ? inputRelatedFiles.specificationVersion : '3.0',
res = {
result: true,
output: {
type: toBundle ? 'bundledContent' : 'relatedFiles',
specification: {
type: 'OpenAPI',
version: version
},
data: [
]
}
};
if (inputRelatedFiles.rootFiles && inputRelatedFiles.rootFiles.length > 0) {
try {
res.output.data = this.mapProcessRelatedFiles(inputRelatedFiles.rootFiles, inputRelatedFiles.data,
inputRelatedFiles.origin, version, inputRelatedFiles.bundleFormat, toBundle, options);
if (res.output.data === undefined || res.output.data.result === false ||
res.output.data.length === 0) {
res.result = false;
}
}
catch (error) {
if (error instanceof ParseError) {
throw (error);
}
let newError = new Error('There was an error during the process');
newError.stack = error.stack;
throw (newError);
}
return res;
}
else {
throw new Error('Input should have at least one root file');
}
},
/**
*
* @description Validates the input for multi file APIs
* @param {string} processInput - Process input data
*
* @returns {undefined} - nothing
*/
validateInputMultiFileAPI(processInput) {
if (_.isEmpty(processInput)) {
throw new Error('Input object must have "type" and "data" information');
}
if (!processInput.type) {
throw new Error('"Type" parameter should be provided');
}
if (processInput.type !== MULTI_FILE_API_TYPE_ALLOWED_VALUE) {
throw new Error('"Type" parameter value allowed is ' + MULTI_FILE_API_TYPE_ALLOWED_VALUE);
}
if (!processInput.data || processInput.data.length === 0) {
throw new Error('"Data" parameter should be provided');
}
if (processInput.data[0].path === '') {
throw new Error('"Path" of the data element should be provided');
}
if (processInput.specificationVersion && !validateSupportedVersion(processInput.specificationVersion)) {
throw new Error(`The provided version "${processInput.specificationVersion}" is not valid`);
}
},
MULTI_FILE_API_TYPE_ALLOWED_VALUE
};