Merge branch 'split/develop/multiFileSupport' of github.com:postmanlabs/openapi-to-postman into feature/add-reference-map-bundle

This commit is contained in:
Vishal Shingala
2022-06-24 11:36:45 +05:30
7 changed files with 249 additions and 40 deletions

View File

@@ -47,6 +47,16 @@ function parseFileOrThrow(fileContent) {
return result; return result;
} }
/**
* 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 parseFile(fileContent) {
const result = parse.getOasObject(fileContent);
return result;
}
/** /**
* Calculates the path relative to parent * Calculates the path relative to parent
* @param {string} parentFileName - parent file name of the current node * @param {string} parentFileName - parent file name of the current node
@@ -271,13 +281,15 @@ function handleLocalCollisions(trace, initialMainKeys) {
* @param {object} version - The version of the spec we are bundling * @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 {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 {string} commonPathFromData - The common path in the file's paths
* @param {Array} allData - array of { path, content} objects
* @returns {object} - The references in current node and the new content from the node * @returns {object} - The references in current node and the new content from the node
*/ */
function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, version, rootMainKeys, function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, version, rootMainKeys,
commonPathFromData) { commonPathFromData, allData) {
let referencesInNode = [], let referencesInNode = [],
nodeReferenceDirectory = {}, nodeReferenceDirectory = {},
mainKeys = {}; mainKeys = {};
traverseUtility(currentNode).forEach(function (property) { traverseUtility(currentNode).forEach(function (property) {
if (property) { if (property) {
let hasReferenceTypeKey; let hasReferenceTypeKey;
@@ -308,11 +320,21 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, ve
return item !== undefined; return item !== undefined;
}), this.key]; }), this.key];
let newValue, let newValue,
[, local] = tempRef.split(localPointer); [, local] = tempRef.split(localPointer),
nodeFromData,
refHasContent = false,
parseResult;
newValue = Object.assign({}, this.node); newValue = Object.assign({}, this.node);
newValue.$ref = referenceInDocument; nodeFromData = findNodeFromPath(tempRef, allData);
if (nodeFromData && nodeFromData.content) {
parseResult = parseFile(nodeFromData.content);
if (parseResult.result) {
newValue.$ref = referenceInDocument;
refHasContent = true;
nodeFromData.parsed = parseResult;
}
}
this.update({ $ref: tempRef }); this.update({ $ref: tempRef });
nodeReferenceDirectory[tempRef] = { nodeReferenceDirectory[tempRef] = {
@@ -322,7 +344,8 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, ve
reference: referenceInDocument, reference: referenceInDocument,
traceToParent, traceToParent,
parentNodeKey: parentFilename, parentNodeKey: parentFilename,
mainKeyInTrace: nodeTrace[nodeTrace.length - 1] mainKeyInTrace: nodeTrace[nodeTrace.length - 1],
refHasContent
}; };
mainKeys[componentKey] = tempRef; mainKeys[componentKey] = tempRef;
@@ -350,13 +373,18 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, ve
function getNodeContentAndReferences (currentNode, allData, specRoot, version, rootMainKeys, commonPathFromData) { function getNodeContentAndReferences (currentNode, allData, specRoot, version, rootMainKeys, commonPathFromData) {
let graphAdj = [], let graphAdj = [],
missingNodes = [], missingNodes = [],
nodeContent; nodeContent,
parseResult;
if (currentNode.parsed) { if (currentNode.parsed) {
nodeContent = currentNode.parsed.oasObject; nodeContent = currentNode.parsed.oasObject;
} }
else { else {
nodeContent = parseFileOrThrow(currentNode.content).oasObject; parseResult = parseFile(currentNode.content);
if (parseResult.result === false) {
return { graphAdj, missingNodes, undefined, nodeReferenceDirectory: {}, nodeName: currentNode.fileName };
}
nodeContent = parseResult.oasObject;
} }
const { referencesInNode, nodeReferenceDirectory } = getReferences( const { referencesInNode, nodeReferenceDirectory } = getReferences(
@@ -366,7 +394,8 @@ function getNodeContentAndReferences (currentNode, allData, specRoot, version, r
currentNode.fileName, currentNode.fileName,
version, version,
rootMainKeys, rootMainKeys,
commonPathFromData commonPathFromData,
allData
); );
referencesInNode.forEach((reference) => { referencesInNode.forEach((reference) => {
@@ -404,13 +433,15 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
return value.keyInComponents.length !== 0; return value.keyInComponents.length !== 0;
}); });
notInLine.forEach(([key, value]) => { notInLine.forEach(([key, value]) => {
let [, partial] = key.split('#'); let [, partial] = key.split(localPointer);
setValueInComponents( if (documentContext.globalReferences[key].refHasContent) {
value.keyInComponents, setValueInComponents(
components, value.keyInComponents,
getContentFromTrace(documentContext.nodeContents[key], partial), components,
version getContentFromTrace(documentContext.nodeContents[key], partial),
); version
);
}
}); });
[rootContent, components].forEach((contentData) => { [rootContent, components].forEach((contentData) => {
traverseUtility(contentData).forEach(function (property) { traverseUtility(contentData).forEach(function (property) {
@@ -430,12 +461,12 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
return missingNode.path === nodeRef; return missingNode.path === nodeRef;
}); });
if (isMissingNode) { if (isMissingNode) {
refData.nodeContent = { refData.nodeContent = refData.node;
[tempRef]: 'This related node was not found in provided data'
};
refData.inline = true;
refData.local = false; refData.local = false;
} }
else if (!refData) {
return;
}
else { else {
refData.nodeContent = documentContext.nodeContents[nodeRef]; refData.nodeContent = documentContext.nodeContents[nodeRef];
refData.inline = refData.keyInComponents.length === 0; refData.inline = refData.keyInComponents.length === 0;
@@ -454,12 +485,14 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
} }
this.update(refData.node); this.update(refData.node);
if (!refData.inline) { if (!refData.inline) {
setValueInComponents( if (documentContext.globalReferences[tempRef].refHasContent) {
refData.keyInComponents, setValueInComponents(
components, refData.keyInComponents,
refData.nodeContent, components,
version refData.nodeContent,
); version
);
}
} }
} }
} }

View File

@@ -96,9 +96,11 @@ module.exports = {
* @param {Object} options computed process options * @param {Object} options computed process options
* @param {Object} files Files map * @param {Object} files Files map
* @param {string} specificationVersion the string of the desired version * @param {string} specificationVersion the string of the desired version
* @param {boolean} allowReadingFS wheter to allow reading content from file system
* @return {String} rootFile * @return {String} rootFile
*/ */
getRootFiles: function (input, inputValidation, options, files = {}, specificationVersion) { getRootFiles: function (input, inputValidation, options, files = {}, specificationVersion,
allowReadingFS = true) {
let rootFilesArray = [], let rootFilesArray = [],
filesPathArray = input.data, filesPathArray = input.data,
origin = input.origin || ''; origin = input.origin || '';
@@ -114,7 +116,7 @@ module.exports = {
if (!_.isEmpty(files)) { if (!_.isEmpty(files)) {
file = files[path.resolve(filePath.fileName)]; file = files[path.resolve(filePath.fileName)];
} }
else { else if (allowReadingFS) {
file = fs.readFileSync(filePath.fileName, 'utf8'); file = fs.readFileSync(filePath.fileName, 'utf8');
} }

View File

@@ -4851,7 +4851,9 @@ module.exports = {
let bundledFile = contentAndComponents.fileContent, let bundledFile = contentAndComponents.fileContent,
bundleOutput; bundleOutput;
bundledFile.components = contentAndComponents.components; if (!_.isEmpty(contentAndComponents.components)) {
bundledFile.components = contentAndComponents.components;
}
if (!format) { if (!format) {
let rootFormat = parsedRootFiles.find((inputRoot) => { let rootFormat = parsedRootFiles.find((inputRoot) => {
return inputRoot.fileName === contentAndComponents.fileName; return inputRoot.fileName === contentAndComponents.fileName;

View File

@@ -644,15 +644,13 @@ class SchemaPack {
path = pathBrowserify; path = pathBrowserify;
OasResolverOptions.browser = true; OasResolverOptions.browser = true;
} }
if ('content' in input.data[0]) { input.data.forEach((file) => {
input.data.forEach((file) => { files[path.resolve(file.fileName)] = file.content ? file.content : '';
files[path.resolve(file.fileName)] = file.content ? file.content : ''; });
});
}
adaptedInput = schemaUtils.mapDetectRootFilesInputToGetRootFilesInput(input); adaptedInput = schemaUtils.mapDetectRootFilesInputToGetRootFilesInput(input);
adaptedInput.origin = input.origin; adaptedInput.origin = input.origin;
rootFiles = parse.getRootFiles(adaptedInput, concreteUtils.inputValidation, this.computedOptions, files, rootFiles = parse.getRootFiles(adaptedInput, concreteUtils.inputValidation, this.computedOptions, files,
input.specificationVersion); input.specificationVersion, false);
res = schemaUtils.mapGetRootFilesOutputToDetectRootFilesOutput(rootFiles, input.specificationVersion); res = schemaUtils.mapGetRootFilesOutputToDetectRootFilesOutput(rootFiles, input.specificationVersion);
return res; return res;
} }

View File

@@ -60,7 +60,7 @@
}, },
"components": { "components": {
"responses": { "responses": {
"/responses.yaml": "This related node was not found in provided data" "$ref": "./responses.yaml"
}, },
"schemas": { "schemas": {
"Monster": { "Monster": {

View File

@@ -663,8 +663,8 @@ describe('bundle files method - 3.0', function () {
expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected);
}); });
it('Should return a "/missing/node/path": NotProvided' + it('Should return the not handled reference ($ref: ./responses.yaml) ' +
' in the place of a not providen node - local_references', async function () { 'in the place of a not provided node - local_references', async function () {
let contentRootFile = fs.readFileSync(localRefFolder + '/root.yaml', 'utf8'), let contentRootFile = fs.readFileSync(localRefFolder + '/root.yaml', 'utf8'),
schemasIndex = fs.readFileSync(localRefFolder + '/schemas/index.yaml', 'utf8'), schemasIndex = fs.readFileSync(localRefFolder + '/schemas/index.yaml', 'utf8'),
schemasClient = fs.readFileSync(localRefFolder + '/schemas/client.yaml', 'utf8'), schemasClient = fs.readFileSync(localRefFolder + '/schemas/client.yaml', 'utf8'),
@@ -1937,6 +1937,7 @@ describe('bundle files method - 3.0', function () {
} }
}); });
it('Should return bundled file as json - schema_collision_from_responses', async function () { it('Should return bundled file as json - schema_collision_from_responses', async function () {
let contentRootFile = fs.readFileSync(schemaCollision + '/root.yaml', 'utf8'), let contentRootFile = fs.readFileSync(schemaCollision + '/root.yaml', 'utf8'),
user = fs.readFileSync(schemaCollision + '/schemas_/_user.yaml', 'utf8'), user = fs.readFileSync(schemaCollision + '/schemas_/_user.yaml', 'utf8'),
@@ -2069,6 +2070,157 @@ describe('bundle files method - 3.0', function () {
expect(res.output.specification.version).to.equal('3.0'); expect(res.output.specification.version).to.equal('3.0');
expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected);
}); });
it('should ignore reference when is empty content and no root is sent', async function () {
let input =
{
type: 'multiFile',
specificationVersion: '3.0',
bundleFormat: 'YAML',
data: [
{
path: 'hello.yaml',
content: ''
},
{
path: 'openapi.yaml',
content: 'openapi: 3.0.0\n' +
'info:\n' +
' title: hello world\n' +
' version: 0.1.1\n' +
'paths:\n' +
' /hello:\n' +
' get:\n' +
' summary: get the hello\n' +
' responses:\n' +
' \'200\':\n' +
' description: sample des\n' +
' content:\n' +
' application/json:\n' +
' schema:\n' +
' $ref: ./hello.yaml\n'
}
]
};
const res = await Converter.bundle(input);
expect(res).to.not.be.empty;
expect(res.result).to.be.true;
expect(res.output.specification.version).to.equal('3.0');
expect(res.output.data[0].bundledContent).to.be.equal(input.data[1].content);
});
it('should ignore reference when is empty content', async function () {
let input =
{
type: 'multiFile',
specificationVersion: '3.0',
bundleFormat: 'YAML',
rootFiles: [{ path: 'openapi.yaml' }],
data: [
{
path: 'hello.yaml',
content: ''
},
{
path: 'openapi.yaml',
content: 'openapi: 3.0.0\n' +
'info:\n' +
' title: hello world\n' +
' version: 0.1.1\n' +
'paths:\n' +
' /hello:\n' +
' get:\n' +
' summary: get the hello\n' +
' responses:\n' +
' \'200\':\n' +
' description: sample des\n' +
' content:\n' +
' application/json:\n' +
' schema:\n' +
' $ref: ./hello.yaml\n'
}
]
};
const res = await Converter.bundle(input);
expect(res).to.not.be.empty;
expect(res.result).to.be.true;
expect(res.output.specification.version).to.equal('3.0');
expect(res.output.data[0].bundledContent).to.be.equal(input.data[1].content);
});
it('should ignore reference when is invalid content', async function () {
let input =
{
type: 'multiFile',
specificationVersion: '3.0',
bundleFormat: 'YAML',
rootFiles: [{ path: 'openapi.yaml' }],
data: [
{
path: 'hello.yaml',
content: 'asd'
},
{
path: 'openapi.yaml',
content: 'openapi: 3.0.0\n' +
'info:\n' +
' title: hello world\n' +
' version: 0.1.1\n' +
'paths:\n' +
' /hello:\n' +
' get:\n' +
' summary: get the hello\n' +
' responses:\n' +
' \'200\':\n' +
' description: sample des\n' +
' content:\n' +
' application/json:\n' +
' schema:\n' +
' $ref: ./hello.yaml\n'
}
]
};
const res = await Converter.bundle(input);
expect(res).to.not.be.empty;
expect(res.result).to.be.true;
expect(res.output.specification.version).to.equal('3.0');
expect(res.output.data[0].bundledContent).to.be.equal(input.data[1].content);
});
it('should ignore reference when is invalid', async function () {
let input =
{
type: 'multiFile',
specificationVersion: '3.0',
bundleFormat: 'YAML',
rootFiles: [{ path: 'openapi.yaml' }],
data: [
{
path: 'openapi.yaml',
content: 'openapi: 3.0.0\n' +
'info:\n' +
' title: hello world\n' +
' version: 0.1.1\n' +
'paths:\n' +
' /hello:\n' +
' get:\n' +
' summary: get the hello\n' +
' responses:\n' +
' \'200\':\n' +
' description: sample des\n' +
' content:\n' +
' application/json:\n' +
' schema:\n' +
' $ref: ./hello.yaml\n'
}
]
};
const res = await Converter.bundle(input);
expect(res).to.not.be.empty;
expect(res.result).to.be.true;
expect(res.output.specification.version).to.equal('3.0');
expect(res.output.data[0].bundledContent).to.be.equal(input.data[0].content);
});
}); });
@@ -2125,7 +2277,8 @@ describe('getReferences method when node does not have any reference', function(
'the/parent/filename', 'the/parent/filename',
'3.0', '3.0',
{}, {},
'' '',
[]
); );
expect(result.nodeReferenceDirectory).to.be.an('object'); expect(result.nodeReferenceDirectory).to.be.an('object');
expect(Object.keys(result.nodeReferenceDirectory).length).to.equal(1); expect(Object.keys(result.nodeReferenceDirectory).length).to.equal(1);

View File

@@ -246,7 +246,7 @@ describe('detectRoot method', function() {
expect(res.output.data[0].path).to.equal('/swagger.json'); expect(res.output.data[0].path).to.equal('/swagger.json');
}); });
it('should read content when is not present 3.0 and no specific version', async function () { it('should not read content from FS when is not present', async function () {
let input = { let input = {
type: 'multiFile', type: 'multiFile',
specificationVersion: '3.1.0', specificationVersion: '3.1.0',
@@ -262,7 +262,7 @@ describe('detectRoot method', function() {
const res = await Converter.detectRootFiles(input); const res = await Converter.detectRootFiles(input);
expect(res).to.not.be.empty; expect(res).to.not.be.empty;
expect(res.result).to.be.true; expect(res.result).to.be.true;
expect(res.output.data[0].path).to.equal(validHopService31x); expect(res.output.data.length).to.equal(0);
}); });
@@ -307,4 +307,25 @@ describe('detectRoot method', function() {
} }
}); });
it('should not read content from FS when is not present ', async function () {
let petSchema = fs.readFileSync(petstoreSeparatedPet, 'utf8'),
input = {
type: 'multiFile',
specificationVersion: '3.0',
data: [
{
path: validPetstore
},
{
path: '/Pet.yaml',
content: petSchema
}
]
};
const res = await Converter.detectRootFiles(input);
expect(res).to.not.be.empty;
expect(res.result).to.be.true;
expect(res.output.data.length).to.equal(0);
});
}); });