openapi-to-postmanv2
Version:
Convert a given OpenAPI specification to Postman Collection v2.0
391 lines (351 loc) • 15 kB
JavaScript
const _ = require('lodash');
/**
* Checks if value is postman variable or not
*
* @param {*} value - Value to check for
* @returns {Boolean} postman variable or not
*/
function isPmVariable (value) {
// collection/environment variables are in format - {{var}}
return _.isString(value) && _.startsWith(value, '{{') && _.endsWith(value, '}}');
}
/**
* Takes in the postman path and the schema path
* takes from the path the number of segments present in the schema path
* and returns the last segments from the path to match in an string format
*
* @param {string} pathToMatch - parsed path (exclude host and params) from the Postman request
* @param {string} schemaPath - schema path from the OAS spec (exclude servers object)
* @returns {string} only the selected segments from the pathToMatch
*/
function handleExplicitServersPathToMatch (pathToMatch, schemaPath) {
let pathTMatchSlice,
schemaPathArr = _.reject(schemaPath.split('/'), (segment) => {
return segment === '';
}),
schemaPathSegments = schemaPathArr.length,
pathToMatchArr = _.reject(pathToMatch.split('/'), (segment) => {
return segment === '';
}),
pathToMatchSegments = pathToMatchArr.length;
if (pathToMatchSegments < schemaPathSegments) {
return pathToMatch;
}
pathTMatchSlice = pathToMatchArr.slice(pathToMatchArr.length - schemaPathSegments, pathToMatchArr.length);
return pathTMatchSlice.join('/');
}
/**
* 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
*/
function getFixedPartsFromPathSegment (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;
}
/**
* @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
*/
function getPostmanUrlSuffixSchemaScore (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 = 0; i < minLength; i++) {
let schemaFixedParts = getFixedPartsFromPathSegment(schemaPath[sMax - i], 'schema'),
collectionFixedParts = 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
(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
(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: []
};
}
/**
* @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
*/
function getPostmanUrlSchemaMatchScore (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 = 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
};
}
module.exports = {
/**
* Finds matching endpoint from definition corresponding to request/transaction.
*
* @param {*} method - Request method
* @param {*} url - Request URL
* @param {*} schema - OAS definition object
* @param {*} options - a standard list of options that's globally passed around. Check options.js for more.
* @returns {Array} - Array of matched definition endpoints
*/
findMatchingRequestFromSchema: function (method, url, schema, options) {
// first step - get array of requests from schema
let parsedUrl = require('url').parse(_.isString(url) ? url : ''),
retVal = [],
pathToMatch,
matchedPath,
matchedPathJsonPath,
schemaPathItems = schema.paths,
pathToMatchServer,
filteredPathItemsArray = [];
// Return no matches for invalid url (if unable to decode parsed url)
try {
pathToMatch = decodeURI(parsedUrl.pathname);
if (!_.isNil(parsedUrl.hash)) {
pathToMatch += parsedUrl.hash;
}
}
catch (e) {
console.warn(
'Error decoding request URI endpoint. URI: ', url,
'Error', e
);
return retVal;
}
// if pathToMatch starts with '/', we assume it's the correct path
// if not, we assume the segment till the first '/' is the host
// this is because a Postman URL like "{{url}}/a/b" will
// likely have {{url}} as the host segment
if (!pathToMatch.startsWith('/')) {
pathToMatch = pathToMatch.substring(pathToMatch.indexOf('/'));
}
// Here, only take pathItemObjects that have the right method
// of those that do, determine a score
// then just pick that key-value pair from schemaPathItems
_.forOwn(schemaPathItems, (pathItemObject, path) => {
if (!pathItemObject) {
// invalid schema. schema.paths had an invalid entry
return true;
}
if (!pathItemObject.hasOwnProperty(method.toLowerCase())) {
// the required method was not found at this path
return true;
}
// filter empty parameters
pathItemObject.parameters = _.reduce(pathItemObject.parameters, (accumulator, param) => {
if (!_.isEmpty(param)) {
accumulator.push(param);
}
return accumulator;
}, []);
let schemaMatchResult = { match: false };
// check if path and pathToMatch match (non-null)
// check in explicit (local defined) servers
if (pathItemObject[method.toLowerCase()].servers) {
pathToMatchServer = handleExplicitServersPathToMatch(pathToMatch, path);
schemaMatchResult = getPostmanUrlSchemaMatchScore(pathToMatchServer, path, options);
}
else {
schemaMatchResult = 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;
},
getPostmanUrlSuffixSchemaScore,
isPmVariable
};