Files
fastapi-openapi-to-postman/lib/bundle.js

555 lines
19 KiB
JavaScript

const {
isExtRef,
getKeyInComponents,
getJsonPointerRelationToRoot,
removeLocalReferenceFromPath,
localPointer,
jsonPointerLevelSeparator,
isLocalRef,
jsonPointerDecodeAndReplace,
generateObjectName
} = require('./jsonPointer'),
traverseUtility = require('traverse'),
parse = require('./parse.js'),
{ ParseError } = require('./common/ParseError'),
Utils = require('./utils'),
crypto = require('crypto');
let path = require('path'),
pathBrowserify = require('path-browserify'),
BROWSER = 'browser',
{ DFS } = require('./dfs'),
deref = require('./deref.js'),
{ isSwagger, getBundleRulesDataByVersion } = require('./common/versionUtils');
/**
* Locates a referenced node from the data input by path
* @param {string} path1 - path1 to compare
* @param {string} path2 - path2 to compare
* @returns {boolean} - wheter is the same path
*/
function comparePaths(path1, path2) {
return path1 === path2;
}
/**
* Parses a node content or throw ParseError if there's any error
* @param {string} fileContent The content from the current node
* @returns {object} The parsed content
*/
function parseFileOrThrow(fileContent) {
const result = parse.getOasObject(fileContent);
if (result.result === false) {
throw new ParseError(result.reason);
}
return result;
}
/**
* Calculates the path relative to parent
* @param {string} parentFileName - parent file name of the current node
* @param {string} referencePath - value of the $ref property
* @returns {object} - Detect root files result object
*/
function calculatePath(parentFileName, referencePath) {
if (path.isAbsolute(referencePath)) {
return referencePath;
}
if (referencePath[0] === localPointer) {
return `${parentFileName}${referencePath}`;
}
let currentDirName = path.dirname(parentFileName),
refDirName = path.join(currentDirName, referencePath);
return refDirName;
}
/**
* Locates a referenced node from the data input by path
* @param {string} referencePath - value from the $ref property
* @param {Array} allData - array of { path, content} objects
* @returns {object} - Detect root files result object
*/
function findNodeFromPath(referencePath, allData) {
const partialComponents = referencePath.split(localPointer);
let isPartial = partialComponents.length > 1,
node = allData.find((node) => {
if (isPartial) {
referencePath = partialComponents[0];
}
return comparePaths(node.fileName, referencePath);
});
return node;
}
/**
* Calculates the path relative to parent
* @param {string} parentFileName - parent file name of the current node
* @param {string} referencePath - value of the $ref property
* @returns {object} - Detect root files result object
*/
function calculatePathMissing(parentFileName, referencePath) {
let currentDirName = path.dirname(parentFileName),
refDirName = path.join(currentDirName, referencePath);
if (refDirName.startsWith('..' + path.sep)) {
return { path: undefined, $ref: referencePath };
}
else if (path.isAbsolute(parentFileName) && !path.isAbsolute(referencePath)) {
let relativeToRoot = path.join(currentDirName.replace(path.sep, ''), referencePath);
if (relativeToRoot.startsWith('..' + path.sep)) {
return { path: undefined, $ref: referencePath };
}
}
return { path: refDirName, $ref: undefined };
}
/**
* verifies if the path has been added to the result
* @param {string} path - path to find
* @param {Array} referencesInNode - Array with the already added paths
* @returns {boolean} - wheter a node with the same path has been added
*/
function added(path, referencesInNode) {
return referencesInNode.find((reference) => { return reference.path === path; }) !== undefined;
}
/**
* Return a trace from the first parent node name attachable in components object
* @param {array} nodeParents - The parent node's name from the current node
* @returns {array} A trace from the first node name attachable in components object
*/
function getRootFileTrace(nodeParents) {
let trace = [];
for (let parentKey of nodeParents) {
if ([undefined, 'oasObject'].includes(parentKey)) {
break;
}
trace.push(parentKey);
}
return trace.reverse();
}
/**
* Get partial content from file content
* @param {object} content - The content in related node
* @param {string} partial - The partial part from reference
* @returns {object} The related content to the trace
*/
function getContentFromTrace(content, partial) {
if (!partial) {
return content;
}
partial = partial[0] === jsonPointerLevelSeparator ? partial.substring(1) : partial;
const trace = partial.split(jsonPointerLevelSeparator).map((item) => {
return jsonPointerDecodeAndReplace(item);
});
let currentValue = content;
currentValue = deref._getEscaped(content, trace, undefined);
return currentValue;
}
/**
* Set a value in the global components object following the provided trace
* @param {array} keyInComponents - The trace to the key in components
* @param {object} components - A global components object
* @param {object} value - The value from node matched with data
* @param {array} componentsKeys - The keys of the reusable component
* @returns {null} It modifies components global context
*/
function setValueInComponents(keyInComponents, components, value, componentsKeys) {
let currentPlace = components,
target = keyInComponents[keyInComponents.length - 2],
key = keyInComponents.length === 2 && componentsKeys.includes(keyInComponents[0]) ?
keyInComponents[1] :
null;
if (componentsKeys.includes(keyInComponents[0])) {
if (keyInComponents[0] === 'schema') {
keyInComponents[0] = 'schemas';
}
target = key;
}
for (let place of keyInComponents) {
if (place === target) {
currentPlace[place] = value;
break;
}
else if (currentPlace[place]) {
currentPlace = currentPlace[place];
}
else {
currentPlace[place] = {};
currentPlace = currentPlace[place];
}
}
}
/**
* Calculates the has of a provided key and join its first 4 numbers at the end of the current key by a _
* @param {string} currentKey The key we will hash
* @returns {string} A key with a hashed part joined by _
*/
function createComponentHashedKey(currentKey) {
let hashPart = crypto.createHash('sha1')
.update(currentKey)
.digest('base64')
.substring(0, 4),
newKey = generateObjectName(currentKey, hashPart);
return newKey;
}
/**
* Generates a not repeated mainKey for the component
* @param {string} tempRef - The reference to the node we are processing
* @param {array} mainKeys - A list of previous generated mainKeys
* @returns {string} A generated mainKey from the provided tempRef
*/
function createComponentMainKey(tempRef, mainKeys) {
let newKey = generateObjectName(tempRef),
mainKey = mainKeys[newKey];
if (mainKey && mainKey !== tempRef) {
newKey = createComponentHashedKey(tempRef);
}
return newKey;
}
/**
* Return a trace from the current node's root to the place where we find a $ref
* @param {object} nodeContext - The current node we are processing
* @param {object} tempRef - The tempRef from the $ref
* @param {object} mainKeys - The dictionary of the previous keys generated
* @param {string} version - The current version of the spec
* @param {string} commonPathFromData - The common path in the file's paths
* @returns {array} The trace to the place where the $ref appears
*/
function getTraceFromParentKeyInComponents(nodeContext, tempRef, mainKeys, version, commonPathFromData) {
const parents = [...nodeContext.parents].reverse(),
isArrayKeyRegexp = new RegExp('^\\d$', 'g'),
key = nodeContext.key,
keyIsAnArrayItem = key.match(isArrayKeyRegexp),
parentKeys = [...parents.map((parent) => {
return parent.key;
})],
nodeParentsKey = keyIsAnArrayItem ?
parentKeys :
[key, ...parentKeys],
nodeTrace = getRootFileTrace(nodeParentsKey),
componentKey = createComponentMainKey(tempRef, mainKeys),
keyTraceInComponents = getKeyInComponents(nodeTrace, componentKey, version, commonPathFromData);
return keyTraceInComponents;
}
/**
* Modifies the generated trace if there is a collision with a key that exists in root components object
* @param {array} trace The generated trace
* @param {object} initialMainKeys the main keys in local components object if it exists
* @returns {array} A modified trace if there is any collision with local reusable objects
*/
function handleLocalCollisions(trace, initialMainKeys) {
if (trace.length === 0) {
return trace;
}
const componentType = trace[trace.length - 2],
initialKeysOfType = initialMainKeys[componentType],
generatedKeyIndex = trace.length - 1;
if (initialKeysOfType && initialKeysOfType.includes(trace[generatedKeyIndex])) {
trace[generatedKeyIndex] = createComponentHashedKey(trace[generatedKeyIndex]);
}
return trace;
}
/**
* Gets all the $refs from an object
* @param {object} currentNode - current node in process
* @param {Function} isOutOfRoot - A filter to know if the current components was called from root or not
* @param {Function} pathSolver - function to resolve the Path
* @param {string} parentFilename - The parent's filename
* @param {object} version - The version of the spec we are bundling
* @param {object} rootMainKeys - A dictionary with the component keys in local components object and its mainKeys
* @param {string} commonPathFromData - The common path in the file's paths
* @returns {object} - The references in current node and the new content from the node
*/
function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, version, rootMainKeys,
commonPathFromData) {
let referencesInNode = [],
nodeReferenceDirectory = {},
mainKeys = {};
traverseUtility(currentNode).forEach(function (property) {
if (property) {
let hasReferenceTypeKey;
hasReferenceTypeKey = Object.keys(property)
.find(
(key) => {
const isLocalOutOfRoot = isOutOfRoot && isLocalRef(property, key),
isExternal = isExtRef(property, key),
isReferenciable = isLocalOutOfRoot || isExternal;
return isReferenciable;
}
);
if (hasReferenceTypeKey) {
const tempRef = calculatePath(parentFilename, property.$ref),
nodeTrace = handleLocalCollisions(
getTraceFromParentKeyInComponents(this, tempRef, mainKeys, version, commonPathFromData),
rootMainKeys
),
componentKey = nodeTrace[nodeTrace.length - 1],
referenceInDocument = getJsonPointerRelationToRoot(
tempRef,
nodeTrace,
version
),
traceToParent = [...this.parents.map((item) => {
return item.key;
}).filter((item) => {
return item !== undefined;
}), this.key];
let newValue,
[, local] = tempRef.split(localPointer);
newValue = Object.assign({}, this.node);
newValue.$ref = referenceInDocument;
this.update({ $ref: tempRef });
nodeReferenceDirectory[tempRef] = {
local,
keyInComponents: nodeTrace,
node: newValue,
reference: referenceInDocument,
traceToParent,
parentNodeKey: parentFilename,
mainKeyInTrace: nodeTrace[nodeTrace.length - 1]
};
mainKeys[componentKey] = tempRef;
if (!added(property.$ref, referencesInNode)) {
referencesInNode.push({ path: pathSolver(property), keyInComponents: nodeTrace, newValue: this.node });
}
}
}
});
return { referencesInNode, nodeReferenceDirectory };
}
/**
* Maps the output from get root files to detect root files
* @param {object} currentNode - current { path, content} object
* @param {Array} allData - array of { path, content} objects
* @param {object} specRoot - root file information
* @param {string} version - The current version
* @param {object} rootMainKeys - A dictionary with the local reusable components keys and its mainKeys
* @param {string} commonPathFromData - The common path in the file's paths
* @returns {object} - Detect root files result object
*/
function getNodeContentAndReferences (currentNode, allData, specRoot, version, rootMainKeys, commonPathFromData) {
let graphAdj = [],
missingNodes = [],
nodeContent;
if (currentNode.parsed) {
nodeContent = currentNode.parsed.oasObject;
}
else {
nodeContent = parseFileOrThrow(currentNode.content).oasObject;
}
const { referencesInNode, nodeReferenceDirectory } = getReferences(
nodeContent,
currentNode.fileName !== specRoot.fileName,
removeLocalReferenceFromPath,
currentNode.fileName,
version,
rootMainKeys,
commonPathFromData
);
referencesInNode.forEach((reference) => {
let referencePath = reference.path,
adjacentNode = findNodeFromPath(calculatePath(currentNode.fileName, referencePath), allData);
if (adjacentNode) {
graphAdj.push(adjacentNode);
}
else if (!comparePaths(referencePath, specRoot.fileName)) {
let calculatedPathForMissing = calculatePathMissing(currentNode.fileName, referencePath);
if (!calculatedPathForMissing.$ref) {
missingNodes.push({ path: calculatedPathForMissing.path });
}
else {
missingNodes.push({ $ref: calculatedPathForMissing.$ref, path: null });
}
}
});
return { graphAdj, missingNodes, nodeContent, nodeReferenceDirectory, nodeName: currentNode.fileName };
}
/**
* Generates the components object from the documentContext data
* @param {object} documentContext The document context from root
* @param {object} rootContent - The root's parsed content
* @param {function} refTypeResolver - The resolver function to test if node has a reference
* @param {object} components - The global components object
* @param {string} version - The current version
* @returns {object} The components object related to the file
*/
function generateComponentsObject (documentContext, rootContent, refTypeResolver, components, version) {
let notInLine = Object.entries(documentContext.globalReferences).filter(([, value]) => {
return value.keyInComponents.length !== 0;
});
const { COMPONENTS_KEYS } = getBundleRulesDataByVersion(version);
notInLine.forEach(([key, value]) => {
let [, partial] = key.split(localPointer);
setValueInComponents(
value.keyInComponents,
components,
getContentFromTrace(documentContext.nodeContents[key], partial),
COMPONENTS_KEYS
);
});
[rootContent, components].forEach((contentData) => {
traverseUtility(contentData).forEach(function (property) {
if (property) {
let hasReferenceTypeKey;
hasReferenceTypeKey = Object.keys(property)
.find(
(key) => {
return refTypeResolver(property, key);
}
);
if (hasReferenceTypeKey) {
let tempRef = property.$ref,
[nodeRef, local] = tempRef.split(localPointer),
refData = documentContext.globalReferences[tempRef],
isMissingNode = documentContext.missing.find((missingNode) => {
return missingNode.path === nodeRef;
});
if (isMissingNode) {
refData.nodeContent = {
[tempRef]: 'This related node was not found in provided data'
};
refData.inline = true;
refData.local = false;
}
else {
refData.nodeContent = documentContext.nodeContents[nodeRef];
refData.inline = refData.keyInComponents.length === 0;
}
if (local) {
let contentFromTrace = getContentFromTrace(refData.nodeContent, local);
if (!contentFromTrace) {
refData.nodeContent = { $ref: `${localPointer + local}` };
}
else {
refData.nodeContent = contentFromTrace;
}
}
if (refData.inline) {
refData.node = refData.nodeContent;
}
this.update(refData.node);
if (!refData.inline) {
setValueInComponents(
refData.keyInComponents,
components,
refData.nodeContent,
COMPONENTS_KEYS
);
}
}
}
});
});
}
/**
* Generates the components object wrapper
* @param {object} parsedOasObject The parsed root
* @param {string} version - The current version
* @returns {object} The components object wrapper
*/
function generateComponentsWrapper(parsedOasObject, version) {
let components = {};
if (isSwagger(version)) {
getBundleRulesDataByVersion(version).COMPONENTS_KEYS.forEach((property) => {
if (parsedOasObject.hasOwnProperty(property)) {
components[property] = parsedOasObject[property];
}
});
}
else if (parsedOasObject.hasOwnProperty('components')) {
components = parsedOasObject.components;
}
return components;
}
/**
* Returns a dictionary with the resusable component keys and their mainKeys in document before the bundle
* @param {object} components - The wrapper with the root reusable components before the bundle
* @param {string} version - The spec version
* @returns {object} - A directory with the local components keys related with their mainKeys
*/
function getMainKeysFromComponents(components, version) {
const {
COMPONENTS_KEYS
} = getBundleRulesDataByVersion(version);
let componentsDictionary = {};
COMPONENTS_KEYS.forEach((key) => {
if (components[key]) {
componentsDictionary[key] = Object.keys(components[key]);
}
});
return componentsDictionary;
}
module.exports = {
/**
* Takes in an spec root file and an array of data files
* Bundles the content of the files into one single file according to the
* json pointers ($ref)
* @param {object} specRoot - root file information
* @param {Array} allData - array of { path, content} objects
* @param {Array} origin - process origin (BROWSER or node)
* @param {string} version - The version we are using
* @returns {object} - Detect root files result object
*/
getBundleContentAndComponents: function (specRoot, allData, origin, version) {
if (origin === BROWSER) {
path = pathBrowserify;
}
const initialComponents = generateComponentsWrapper(specRoot.parsed.oasObject, version),
initialMainKeys = getMainKeysFromComponents(initialComponents, version);
let algorithm = new DFS(),
components = {},
commonPathFromData = '',
rootContextData;
commonPathFromData = Utils.findCommonSubpath(allData.map((fileData) => {
return fileData.fileName;
}));
rootContextData = algorithm.traverseAndBundle(specRoot, (currentNode) => {
return getNodeContentAndReferences(currentNode, allData, specRoot, version, initialMainKeys, commonPathFromData);
});
components = generateComponentsWrapper(specRoot.parsed.oasObject, version);
generateComponentsObject(
rootContextData,
rootContextData.nodeContents[specRoot.fileName],
isExtRef,
components,
version
);
return {
fileContent: rootContextData.nodeContents[specRoot.fileName],
components,
fileName: specRoot.fileName
};
},
getReferences,
parseFileOrThrow
};