Files
fastapi-openapi-to-postman/lib/bundle.js
2022-05-24 17:11:13 -05:00

427 lines
14 KiB
JavaScript

const {
isExtRef,
getKeyInComponents,
getJsonPointerRelationToRoot,
jsonPointerEncodeAndReplace
} = require('./jsonPointer'),
traverseUtility = require('traverse'),
parse = require('./parse.js');
let path = require('path'),
pathBrowserify = require('path-browserify'),
BROWSER = 'browser',
{ DFS } = require('./dfs');
/**
* 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;
}
/**
* Removes the local pointer inside a path
* aab.yaml#component returns aab.yaml
* @param {string} refValue - value of the $ref property
* @returns {string} - the calculated path only
*/
function removeLocalReferenceFromPath(refValue) {
if (refValue.$ref.includes('#')) {
return refValue.$ref.split('#')[0];
}
return refValue.$ref;
}
/**
* 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) {
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('#');
let isPartial = partialComponents.length > 1,
node = allData.find((node) => {
if (isPartial) {
referencePath = partialComponents[0];
}
return comparePaths(node.fileName, referencePath);
});
if (node) {
node.isPartial = isPartial;
node.partialCalled = partialComponents[1];
}
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) {
partial = partial[0] === '/' ? partial.substring(1) : partial;
const trace = partial.split('/');
let currentValue = content;
for (let place of trace) {
currentValue = currentValue[place];
}
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
* @returns {null} It modifies components global context
*/
function setValueInComponents(keyInComponents, components, value) {
let currentPlace = components,
target = keyInComponents[keyInComponents.length - 2],
referencedPart = keyInComponents[keyInComponents.length - 1],
[, local] = referencedPart.split('#'),
key = keyInComponents.length === 2 && keyInComponents[0] === 'schema' ?
keyInComponents[1] :
null;
if (keyInComponents[0] === 'schema') {
keyInComponents[0] = 'schemas';
target = key;
}
for (let place of keyInComponents) {
if (place === target) {
if (local) {
value = getContentFromTrace(value, local);
}
currentPlace[place] = value;
break;
}
else if (currentPlace[place]) {
currentPlace = currentPlace[place];
}
else {
currentPlace[place] = {};
currentPlace = currentPlace[place];
}
}
}
/**
* 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} property - The current property that contains the $ref
* @param {string} parentFilename - The parent's filename
* @returns {array} The trace to the place where the $ref appears
*/
function getTraceFromParent(nodeContext, property, parentFilename) {
const parents = [...nodeContext.parents].reverse(),
key = nodeContext.key,
nodeParentsKey = [key, ...parents.map((parent) => {
return parent.key;
})],
nodeTrace = getRootFileTrace(nodeParentsKey),
cleanFileName = (filename) => {
const [file, local] = filename.split('#');
return [calculatePath(parentFilename, file), local];
},
[file, local] = cleanFileName(property.$ref),
keyInComponents = getKeyInComponents(nodeTrace, file, local);
return keyInComponents;
}
/**
* Returns the key trace if this node will be included in components else returns an empty array
* @param {array} nodeTrace - The trace from file to the $ref
* @returns {array} An arrat with the trace to the key in components
*/
function getKeyParent(nodeTrace) {
const componentsKeys = [
'schemas',
'schema',
'responses',
'parameters',
'examples',
'requestBodies',
'headers',
'securitySchemes',
'links',
'callbacks'
],
trace = [...nodeTrace].reverse();
let traceToKey = [];
for (let item of trace) {
traceToKey.push(item);
if (componentsKeys.includes(item)) {
break;
}
}
return traceToKey.length === trace.length ?
[] :
traceToKey.reverse();
}
/**
* Gets all the $refs from an object
* @param {object} currentNode - current node in process
* @param {Function} refTypeResolver - function to resolve the ref according to type (local, external, web etc)
* @param {Function} pathSolver - function to resolve the Path
* @param {string} parentFilename - The parent's filename
* @param {object} globalComponentsContext - The global context from root file
* @param {array} allData The data from files provided in the input
* @returns {object} - {path : $ref value}
*/
function getReferences (currentNode, refTypeResolver, pathSolver, parentFilename, globalComponentsContext, allData) {
let referencesInNode = [];
traverseUtility(currentNode).forEach(function (property) {
if (property) {
let hasReferenceTypeKey;
hasReferenceTypeKey = Object.keys(property)
.find(
(key) => {
return refTypeResolver(property, key);
}
);
if (hasReferenceTypeKey) {
const nodeTrace = getTraceFromParent(this, property, parentFilename),
keyParent = getKeyParent(nodeTrace),
referenceInDocument = getJsonPointerRelationToRoot(
jsonPointerEncodeAndReplace,
property.$ref,
keyParent
);
let newValue,
nodeData = findNodeFromPath(calculatePath(parentFilename, property.$ref), allData).content,
nodeContent = nodeData ?
parse.getOasObject(nodeData).oasObject :
{ $missedReference: `property ${property.$ref} was not provided in data` };
if (keyParent.length === 0) {
newValue = nodeContent;
}
else {
newValue = Object.assign({}, this.node);
newValue.$ref = referenceInDocument;
}
globalComponentsContext[property.$ref] = {
newValue: newValue,
keyInComponents: keyParent,
nodeContent
};
if (!added(property.$ref, referencesInNode)) {
referencesInNode.push({ path: pathSolver(property), keyInComponents: keyParent, newValue: this.node });
}
}
}
});
return referencesInNode;
}
/**
* 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} globalComponentsContext - the context from the global level
* @returns {object} - Detect root files result object
*/
function getAdjacentAndMissingToBundle (currentNode, allData, specRoot, globalComponentsContext) {
let currentNodeReferences,
graphAdj = [],
missingNodes = [],
bundleDataInAdjacent = [],
OASObject;
if (currentNode.parsed) {
OASObject = currentNode.parsed.oasObject;
}
else {
OASObject = parse.getOasObject(currentNode.content).oasObject;
}
currentNodeReferences = getReferences(
OASObject,
isExtRef,
removeLocalReferenceFromPath,
currentNode.fileName,
globalComponentsContext,
allData
);
currentNodeReferences.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 });
}
}
});
if (missingNodes.length > 0) {
throw new Error('Some files are missing, run detectRelatedFiles to get more detail');
}
return { graphAdj, missingNodes, bundleDataInAdjacent, currentNode };
}
/**
* 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
* @returns {object} The components object related to the file
*/
function generateComponentsObject (documentContext, rootContent, refTypeResolver, components) {
[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 refData = documentContext[property.$ref];
this.update(refData.newValue);
if (refData.keyInComponents.length > 0) {
setValueInComponents(
refData.keyInComponents,
components,
refData.nodeContent
);
}
}
}
});
});
}
module.exports = {
/**
* Maps the output from get root files to detect root files
* @param {object} specRoot - root file information
* @param {Array} allData - array of { path, content} objects
* @param {Array} origin - process origin (BROWSER or node)
* @returns {object} - Detect root files result object
*/
getRelatedFilesAndBundleData: function (specRoot, allData, origin) {
if (origin === BROWSER) {
path = pathBrowserify;
}
let algorithm = new DFS(),
globalComponentsContext = {},
components = {};
algorithm.traverseAndBundle(specRoot, (currentNode) => {
return getAdjacentAndMissingToBundle(currentNode, allData, specRoot, globalComponentsContext);
});
generateComponentsObject(globalComponentsContext, specRoot.parsed.oasObject, isExtRef, components);
return {
fileContent: specRoot.parsed.oasObject,
components
};
},
bundleFiles: function(data) {
let { bundleData, missingRelatedFiles } = data[0],
components = {},
componentsFromFile = false;
if (missingRelatedFiles.length > 0) {
throw new Error(`There are ${missingRelatedFiles.length} missing files in yopur spec`);
}
Object.keys(bundleData).forEach((key) => {
if (bundleData[key].hasOwnProperty('components')) {
if (componentsFromFile) {
throw new Error('Multiple components definition through your files');
}
components = fillExistentComponents(bundleData.key.components, components);
componentsFromFile = true;
}
else {
components[key] = bundleData[key].content;
}
});
return components;
}
};