Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for handling Remote URL refs in the bundle method #747

Merged
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
77578d2
Added support for resolving remote URL refs without support for deep …
prashantRaghu Jul 11, 2023
2359345
Added missing ref resolver arg
prashantRaghu Jul 12, 2023
72bc9bc
Fixed resolution of refs from refs
prashantRaghu Jul 17, 2023
41059e0
Fixed linting issues
prashantRaghu Aug 7, 2023
c9cbf14
Fixed failing tests and added new ones for remote ref resolution
prashantRaghu Aug 8, 2023
8b6eb7b
Fixed a check
prashantRaghu Aug 8, 2023
8171baa
Add test for yaml support
prashantRaghu Aug 8, 2023
eb1f18c
Fixed linting issues
prashantRaghu Aug 8, 2023
10741f2
Fix path value to a fixture
prashantRaghu Aug 8, 2023
b8c928b
Update the URL thats passed to remoteRefResolver
prashantRaghu Aug 8, 2023
82ecc91
Removed an unused method
prashantRaghu Aug 8, 2023
ac6f740
Add JSDocs
prashantRaghu Aug 8, 2023
7cb2ba4
Bump ajv to 8.11.0
prashantRaghu Aug 16, 2023
a8da616
Merge branch 'develop' of github.com:postmanlabs/openapi-to-postman i…
prashantRaghu Aug 16, 2023
f8cb8aa
Update package-lock
prashantRaghu Aug 16, 2023
142defa
Added handling if remoteRefResolver threw err
prashantRaghu Aug 17, 2023
88e461e
Revert a refactor that introduces more code paths than required
prashantRaghu Aug 17, 2023
da3d42c
Refactor code to keep existing code paths as is
prashantRaghu Aug 17, 2023
b0ffbb9
Added test scenarios for yaml output
prashantRaghu Aug 17, 2023
37003c1
Added handling for circular refs
prashantRaghu Aug 17, 2023
67b9fe3
Added changelog
prashantRaghu Aug 18, 2023
8b045f8
Update changelog
prashantRaghu Aug 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 208 additions & 21 deletions lib/bundle.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
const _ = require('lodash'),
{
isExtRef,
isExtURLRef,
stringIsAValidUrl,
isExtRemoteRef,
getKeyInComponents,
getJsonPointerRelationToRoot,
removeLocalReferenceFromPath,
localPointer,
httpSeparator,
jsonPointerLevelSeparator,
isLocalRef,
jsonPointerDecodeAndReplace,
Expand Down Expand Up @@ -83,15 +87,20 @@
* @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);
});
const isReferenceRemoteURL = stringIsAValidUrl(referencePath),
partialComponents = referencePath.split(localPointer),
isPartial = partialComponents.length > 1;
if (isPartial && !isReferenceRemoteURL) {
referencePath = partialComponents.slice(0, partialComponents.length - 1).join('#');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment in the code on why this is needed with an example if possible? I'm not getting the need of joining it with # compared to the previous logic, as we're changing this code that's applicable for non-remote refs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VShingala had done this refactor to test out some deep ref scenarios, but this check is just a more generic check in case we encounter reference paths that contain multiple hash fragments, but turns out we don't need this for any scenario so reverting this back to the original check

}

let node = allData.find((node) => {
if (isReferenceRemoteURL) {
return _.startsWith(node.path, referencePath);
}

return comparePaths(node.fileName, referencePath);
});
return node;
}

Expand Down Expand Up @@ -290,13 +299,78 @@
* @param {string} commonPathFromData - The common path in the file's paths
* @param {Array} allData - array of { path, content} objects
* @param {object} globalReferences - The accumulated global references from all nodes
* @param {function} remoteRefResolver - The function that would be called to fetch remote ref contents
* @returns {object} - The references in current node and the new content from the node
*/
function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, version, rootMainKeys,
commonPathFromData, allData, globalReferences) {
async function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, version, rootMainKeys,
commonPathFromData, allData, globalReferences, remoteRefResolver) {
let referencesInNode = [],
nodeReferenceDirectory = {},
mainKeys = {};
mainKeys = {},
remoteRefContentMap = new Map(),
remoteRefSet = new Set(),
remoteRefResolutionPromises = [];

remoteRefResolver && traverseUtility(currentNode).forEach(function (property) {
if (property) {
let hasReferenceTypeKey;

hasReferenceTypeKey = Object.keys(property)
.find(
(key) => {
const isExternal = isExtURLRef(property, key),
isReferenciable = isExternal;

return isReferenciable;
}
);

if (hasReferenceTypeKey) {
const tempRef = calculatePath(parentFilename, property.$ref),
isRefEncountered = remoteRefSet.has(tempRef);

if (isRefEncountered) {
return;
}

remoteRefResolutionPromises.push(
new Promise(async (resolveInner) => {

/**

Check warning on line 339 in lib/bundle.js

View workflow job for this annotation

GitHub Actions / Unit-Tests (14.x)

Missing JSDoc return type

Check warning on line 339 in lib/bundle.js

View workflow job for this annotation

GitHub Actions / Unit-Tests (16.x)

Missing JSDoc return type

Check warning on line 339 in lib/bundle.js

View workflow job for this annotation

GitHub Actions / Unit-Tests (18.x)

Missing JSDoc return type
* Converts contents received from remoteRefResolver into stringified JSON
VShingala marked this conversation as resolved.
Show resolved Hide resolved
* @param {string | object} content - contents from remoteRefResolver
* @returns Stringified JSON contents
*/
function convertToJSONString (content) {
if (typeof content === 'object') {
return JSON.stringify(content);
}

const parsedFile = parseFile(content);

return JSON.stringify(parsedFile.oasObject);
}

let contentFromRemote = await remoteRefResolver(property.$ref),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have some handling in cases where this may fail and throw error.

we can either ignore and keep the field as is or mention it missing in bundled content somehow. But not handling it at all can cause issues.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled err scenario

nodeTemp = {
fileName: tempRef,
path: tempRef,
content: convertToJSONString(contentFromRemote)
};

remoteRefContentMap.set(tempRef, contentFromRemote);

allData.push(nodeTemp);
resolveInner();
})
);

remoteRefSet.add(tempRef);
}
}
});

await Promise.all(remoteRefResolutionPromises);

traverseUtility(currentNode).forEach(function (property) {
if (property) {
Expand Down Expand Up @@ -371,6 +445,87 @@
referencesInNode.push({ path: pathSolver(property), keyInComponents: nodeTrace, newValue: this.node });
}
}

const hasRemoteReferenceTypeKey = Object.keys(property)
.find(
(key) => {
const isExternal = isExtURLRef(property, key),

// Only process URL refs if remoteRefResolver is provided and a valid function
isReferenciable = isExternal && _.isFunction(remoteRefResolver);

return isReferenciable;
}
);

if (hasRemoteReferenceTypeKey) {
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 = Object.assign({}, this.node),
[, local] = tempRef.split(localPointer),
nodeFromData,
refHasContent = false,
parseResult,
newRefInDoc,
inline,
contentFromRemote = remoteRefContentMap.get(tempRef),
nodeTemp = {
fileName: tempRef,
path: tempRef,
content: contentFromRemote
};

nodeFromData = nodeTemp;

if (nodeFromData && nodeFromData.content) {
parseResult = parseFile(JSON.stringify(nodeFromData.content));
if (parseResult.result) {
newValue.$ref = referenceInDocument;
refHasContent = true;
nodeFromData.parsed = parseResult;
}
}
this.update({ $ref: tempRef });

if (nodeTrace.length === 0) {
inline = true;
}

if (_.isNil(globalReferences[tempRef])) {
nodeReferenceDirectory[tempRef] = {
local,
keyInComponents: nodeTrace,
node: newValue,
reference: inline ? newRefInDoc : referenceInDocument,
traceToParent,
parentNodeKey: parentFilename,
mainKeyInTrace: nodeTrace[nodeTrace.length - 1],
refHasContent,
inline
};
}

mainKeys[componentKey] = tempRef;

if (!added(property.$ref, referencesInNode)) {
referencesInNode.push({ path: pathSolver(property), keyInComponents: nodeTrace, newValue: this.node });
}
}
}
});

Expand All @@ -386,10 +541,11 @@
* @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
* @param {object} globalReferences - The accumulated global refernces from all nodes
* @param {function} remoteRefResolver - The function that would be called to fetch remote ref contents
* @returns {object} - Detect root files result object
*/
function getNodeContentAndReferences (currentNode, allData, specRoot, version, rootMainKeys,
commonPathFromData, globalReferences) {
async function getNodeContentAndReferences (currentNode, allData, specRoot, version, rootMainKeys,
commonPathFromData, globalReferences, remoteRefResolver) {
let graphAdj = [],
missingNodes = [],
nodeContent,
Expand All @@ -406,7 +562,7 @@
nodeContent = parseResult.oasObject;
}

const { referencesInNode, nodeReferenceDirectory } = getReferences(
const { referencesInNode, nodeReferenceDirectory } = await getReferences(
nodeContent,
currentNode.fileName !== specRoot.fileName,
removeLocalReferenceFromPath,
Expand All @@ -415,7 +571,8 @@
rootMainKeys,
commonPathFromData,
allData,
globalReferences
globalReferences,
remoteRefResolver
);

referencesInNode.forEach((reference) => {
Expand Down Expand Up @@ -516,9 +673,11 @@
* @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
* @param {function} remoteRefResolver - The function that would be called to fetch remote ref contents
* @returns {object} The components object related to the file
*/
function generateComponentsObject (documentContext, rootContent, refTypeResolver, components, version) {
function generateComponentsObject(documentContext, rootContent,
refTypeResolver, components, version, remoteRefResolver) {
let notInLine = Object.entries(documentContext.globalReferences).filter(([, value]) => {
return value.keyInComponents.length !== 0;
}),
Expand Down Expand Up @@ -555,15 +714,39 @@
isMissingNode = documentContext.missing.find((missingNode) => {
return missingNode.path === nodeRef;
});

if (isMissingNode) {
refData.nodeContent = refData.node;
refData.local = false;
}
else if (!refData) {
return;
}
else if (isExtRef(property, '$ref') && !isExtURLRef(property, '$ref')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the existing condition logic for else statement as is and use (!isExtRef(property, '$ref') && isExtURLRef(property, '$ref') as condition and add new logic here.

Reason behind it is as logic for HTTP is very specific and to handle future scenarios better as existing logic is more generic.

refData.nodeContent = documentContext.nodeContents[nodeRef];
}
else {
let splitPathByHttp = property.$ref.split(httpSeparator),
prefix = splitPathByHttp
.slice(0, splitPathByHttp.length - 1).join(httpSeparator) +
httpSeparator + splitPathByHttp[splitPathByHttp.length - 1]
.split(localPointer)[0],
separatedPaths = [prefix, splitPathByHttp[splitPathByHttp.length - 1].split(localPointer)[1]];

nodeRef = separatedPaths[0];
local = separatedPaths[1];

refData.nodeContent = documentContext.nodeContents[nodeRef];

const isReferenceRemoteURL = stringIsAValidUrl(nodeRef);

if (isReferenceRemoteURL && _.isFunction(remoteRefResolver)) {
Object.keys(documentContext.nodeContents).forEach((key) => {
if (_.startsWith(key, nodeRef) && !key.split(nodeRef)[1].includes(httpSeparator)) {
refData.nodeContent = documentContext.nodeContents[key];
}
});
}
}
if (local) {
let contentFromTrace = getContentFromTrace(refData.nodeContent, local);
Expand Down Expand Up @@ -697,9 +880,10 @@
* @param {Array} allData - array of { path, content} objects
* @param {Array} origin - process origin (BROWSER or node)
* @param {string} version - The version we are using
* @param {function} remoteRefResolver - The function that would be called to fetch remote ref contents
* @returns {object} - Detect root files result object
*/
getBundleContentAndComponents: function (specRoot, allData, origin, version) {
getBundleContentAndComponents: async function (specRoot, allData, origin, version, remoteRefResolver) {
if (origin === BROWSER) {
path = pathBrowserify;
}
Expand All @@ -716,15 +900,16 @@
commonPathFromData = Utils.findCommonSubpath(allData.map((fileData) => {
return fileData.fileName;
}));
rootContextData = algorithm.traverseAndBundle(specRoot, (currentNode, globalReferences) => {
rootContextData = await algorithm.traverseAndBundle(specRoot, (currentNode, globalReferences) => {
return getNodeContentAndReferences(
currentNode,
allData,
specRoot,
version,
initialMainKeys,
commonPathFromData,
globalReferences
globalReferences,
remoteRefResolver
);
});
components = generateComponentsWrapper(
Expand All @@ -735,10 +920,12 @@
finalElements = generateComponentsObject(
rootContextData,
rootContextData.nodeContents[specRoot.fileName],
isExtRef,
isExtRemoteRef,
components,
version
version,
remoteRefResolver
);

return {
fileContent: finalElements.resRoot,
components: finalElements.newComponents,
Expand Down
4 changes: 2 additions & 2 deletions lib/dfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class DFS {
return { traverseOrder, missing };
}

traverseAndBundle(node, getAdjacentAndBundle) {
async traverseAndBundle(node, getAdjacentAndBundle) {
let traverseOrder = [],
stack = [],
missing = [],
Expand All @@ -48,7 +48,7 @@ class DFS {
nodeContent,
nodeReferenceDirectory,
nodeName
} = getAdjacentAndBundle(node, globalReferences);
} = await getAdjacentAndBundle(node, globalReferences);
nodeContents[nodeName] = nodeContent;
Object.entries(nodeReferenceDirectory).forEach(([key, data]) => {
globalReferences[key] = data;
Expand Down
Loading
Loading