text: /**
   * This file contains util functions that need OAS-awareness
   * utils.js contains other util functions
   */
  
  const async = require('async'),
    sdk = require('postman-collection'),
    schemaFaker = require('../assets/json-schema-faker.js'),
    parse = require('./parse.js'),
    deref = require('./deref.js'),
    _ = require('lodash'),
    xmlFaker = require('./xmlSchemaFaker.js'),
    openApiErr = require('./error.js'),
    ajvValidationError = require('./ajvValidationError'),
    utils = require('./utils.js'),
    defaultOptions = require('../lib/options.js').getOptions('use'),
    { Node, Trie } = require('./trie.js'),
    { validateSchema } = require('./ajvValidation'),
    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'
    ],
  
    /* eslint-disable arrow-body-style */
    schemaTypeToJsValidator = {
      'string': (d) => typeof d === 'string',
      'number': (d) => !isNaN(d),
      'integer': (d) => !isNaN(d) && Number.isInteger(Number(d)),
      'boolean': (d) => _.isBoolean(d) || d === 'true' || d === 'false',
      'array': (d) => Array.isArray(d),
      'object': (d) => typeof d === 'object' && !Array.isArray(d)
    },
    crypto = require('crypto');
    /* 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', {});
  
    resolvedSchema = deref.resolveRefs(oldSchema, parameterSourceOption, components, schemaResolutionCache,
      resolveFor, resolveTo, 0, {}, stackLimit);
    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) {
        _.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;
    },
  
    /**
     * Parses an OAS string/object as a YAML or JSON
     * @param {YAML/JSON} openApiSpec - The OAS 3.x specification specified in either YAML or JSON
     * @returns {Object} - Contains the parsed JSON-version of the OAS spec, or an error
     * @no-unit-test
     */
    parseSpec: function (openApiSpec) {
      var openApiObj = openApiSpec,
        obj,
        rootValidation;
  
      // If the open api specification is a string could be YAML or JSON
      if (typeof openApiSpec === 'string') {
        obj = parse.getOasObject(openApiSpec);
        if (obj.result) {
          openApiObj = obj.oasObject;
        }
        else {
          return obj;
        }
      }
  
      // spec is a valid JSON object at this point
  
      // Validate the root level object for semantics
      rootValidation = parse.validateSpec(openApiObj);
      if (!rootValidation.result) {
        return {
          result: false,
          reason: rootValidation.reason
        };
      }
  
      // Valid openapi root object
      return {
        result: true,
        openapi: rootValidation.openapi
      };
    },
  
    /**
     * 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 (param.hasOwnProperty('$ref')) {
          arr[index] = this.getRefObject(param.$ref, components, options);
        }
      });
  
      operationParam.forEach((param, index, arr) => {
        if (param.hasOwnProperty('$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.
     * @returns {Object} - The final object consists of the tree structure
     */
    generateTrieFromPaths: function (spec, options) {
      options = _.merge({}, defaultOptions, options);
      var paths = spec.paths, // the first level of paths
        currentPath = '',
        currentPathObject = '',
        commonParams = '',
        collectionVariables = {},
        operationItem,
        pathLevelServers = '',
        pathLength,
        currentPathRequestCount,
        currentNode,
        i,
        summary,
        path,
        componentsAndPaths = {
          components: spec.components,
          paths: spec.paths
        },
        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;
        };
  
      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);
          }
  
          // 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,
              componentsAndPaths, 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.
      };
    },
  
    /**
     * 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 = providedPathVars;
      // 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);
        });
      }
  
      return variables;
    },
  
    /**
     * 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
     * @returns {*} Postman itemGroup or request
     * @no-unit-test
     */
    convertChildToItemGroup: function (openapi, child, components, options, schemaCache, variableStore) {
      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);
        }
        /* 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)
          );
        }
  
        // 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)
            );
          }
        }
        /* 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);
      }
  
      // 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);
        }
      }
    },
  
    /**
     * 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);
  
      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;
        // return example value if present else example is returned
        if (bodyData.hasOwnProperty('value')) {
          bodyData = bodyData.value;
        }
      }
      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')) {
          bodyObj.schema = this.getRefObject(bodyObj.schema.$ref, components, options);
        }
        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 = bodyObj.schema.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 = [];
      // console.log(deepObject);
  
      _.forEach(deepObject, (value, 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 = [];
  
      // @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';
        if (contentObj[URLENCODED].hasOwnProperty('schema') && contentObj[URLENCODED].schema.hasOwnProperty('$ref')) {
          contentObj[URLENCODED].schema = this.getRefObject(contentObj[URLENCODED].schema.$ref, components, options);
        }
        bodyData = this.convertToPmBodyData(contentObj[URLENCODED], requestType, URLENCODED,
          PARAMETER_SOURCE.REQUEST, options.indentCharacter, components, options, schemaCache);
        encoding = contentObj[URLENCODED].encoding ? contentObj[URLENCODED].encoding : {};
        // 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 (
          bodyType &&
          !_.isEmpty(_.get(contentObj, [bodyType, 'schema'])) &&
          contentObj[bodyType].schema.type === 'string' &&
          contentObj[bodyType].schema.format === 'binary'
        ) {
          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, 4)
          };
        }
  
        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;
  
      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 };
    },
  
    /**
     * 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
    * @returns {Object} postman request Item
    * @no-unit-test
    */
    convertRequestToItem: function(openapi, operationItem, components, options, schemaCache, variableStore) {
      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 = '{{baseUrl}}',
        pathVarArray = [],
        authHelper,
        item,
        serverObj,
        displayUrl,
        reqUrl = '/' + operationItem.path,
        pmBody,
        authMeta,
        swagResponse,
        localServers = _.get(operationItem, 'properties.servers'),
        exampleRequestBody,
        sanitizeResult,
        globalServers = _.get(operationItem, 'servers');
  
      // 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) {
            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
          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;
        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 = {};
          }
          convertedResponse = this.convertToPmResponse(swagResponse, code, thisOriginalRequest,
            components, options, schemaCache);
          convertedResponse && item.responses.add(convertedResponse);
        });
      }
  
      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,
        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;
        }, []);
  
        // check if path and pathToMatch match (non-null)
        let 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 {Function} callback - For return
     * @returns {Array} array of mismatches
     */
    checkValueAgainstSchema: function (property, jsonPathPrefix, txnParamName, value, schemaPathPrefix, openApiSchemaObj,
      parameterSourceOption, components, options, schemaCache, 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, 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 (typeof schemaTypeToJsValidator[schema.type] === 'function') {
          let isCorrectType;
  
          // Treat unresolved postman collection/environment variable as correct type
          if (options.ignoreUnresolvedVariables && this.isPmVariable(valueToUse)) {
            isCorrectType = true;
          }
          else {
            isCorrectType = schemaTypeToJsValidator[schema.type](valueToUse);
          }
  
          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);
  
            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 = ajvError.dataPath || '';
  
                  // discard the leading '.' if it exists
                  if (dataPath[0] === '.') {
                    dataPath = dataPath.slice(1);
                  }
  
                  mismatchObj = _.assign({
                    property: property,
                    transactionJsonPath: jsonPathPrefix + ajvError.dataPath,
                    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 = ajvError.dataPath || '';
  
                    // 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 {*} callback Callback
     * @returns {array} mismatches (in the callback)
     */
    checkPathVariables: function (
      determinedPathVariables,
      transactionPathPrefix,
      schemaPath,
      components,
      options,
      schemaCache,
      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
        if (!pathVar._varMatched) {
          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, 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) => {
            return param.key === pathVar.name && 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'),
        actualReqDesc,
        mismatches = [],
        mismatchObj,
        reqUrl;
  
      if (!options.validateMetadata) {
        return callback(null, []);
      }
  
      // 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;
          reqNameMismatch = (actualReqName !== 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];
          reqNameMismatch = (actualReqName !== 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, 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, 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(contentMediaType.type + '/' + contentMediaType.subtype);
      });
  
      // prefer JSON > XML > Other media types for suggested header.
      _.forEach(mediaTypes, (mediaType) => {
        let headerFamily = this.getHeaderFamily(mediaType);
  
        if (headerFamily !== HEADER_TYPE.INVALID) {
          suggestedContentHeader = mediaType;
          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];
        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) && !_.includes(mediaTypes, contentHeaderMediaType)) {
        let 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, 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, cb
          );
        }, 0);
      }, (err, res) => {
        let mismatches = [],
          mismatchObj,
          reqBody = _.get(schemaPath, 'requestBody'),
          contentHeaderMismatches;
  
        // resolve $ref in request body if present
        if (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, 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, 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, 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,
            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, 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, 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,
          callback
        );
      }, 0);
    },
  
    checkResponses: function (responses, transactionPathPrefix, schemaPathPrefix, schemaPath,
      components, options, schemaCache, 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, 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, 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);
      });
    },
  
    /**
     * @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 = ajvValidationErrorObj.dataPath || '',
        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;
    }
  };
  