diff --git a/lib/bundle.js b/lib/bundle.js index b02dcdf..07d4ade 100644 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -47,6 +47,16 @@ function parseFileOrThrow(fileContent) { 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 * @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} 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 {Array} allData - array of { path, content} objects * @returns {object} - The references in current node and the new content from the node */ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, version, rootMainKeys, - commonPathFromData) { + commonPathFromData, allData) { let referencesInNode = [], nodeReferenceDirectory = {}, mainKeys = {}; + traverseUtility(currentNode).forEach(function (property) { if (property) { let hasReferenceTypeKey; @@ -308,11 +320,21 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, ve return item !== undefined; }), this.key]; let newValue, - [, local] = tempRef.split(localPointer); + [, local] = tempRef.split(localPointer), + nodeFromData, + refHasContent = false, + parseResult; 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 }); nodeReferenceDirectory[tempRef] = { @@ -322,7 +344,8 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, ve reference: referenceInDocument, traceToParent, parentNodeKey: parentFilename, - mainKeyInTrace: nodeTrace[nodeTrace.length - 1] + mainKeyInTrace: nodeTrace[nodeTrace.length - 1], + refHasContent }; mainKeys[componentKey] = tempRef; @@ -350,13 +373,18 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, ve function getNodeContentAndReferences (currentNode, allData, specRoot, version, rootMainKeys, commonPathFromData) { let graphAdj = [], missingNodes = [], - nodeContent; + nodeContent, + parseResult; if (currentNode.parsed) { nodeContent = currentNode.parsed.oasObject; } 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( @@ -366,7 +394,8 @@ function getNodeContentAndReferences (currentNode, allData, specRoot, version, r currentNode.fileName, version, rootMainKeys, - commonPathFromData + commonPathFromData, + allData ); referencesInNode.forEach((reference) => { @@ -404,13 +433,15 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver return value.keyInComponents.length !== 0; }); notInLine.forEach(([key, value]) => { - let [, partial] = key.split('#'); - setValueInComponents( - value.keyInComponents, - components, - getContentFromTrace(documentContext.nodeContents[key], partial), - version - ); + let [, partial] = key.split(localPointer); + if (documentContext.globalReferences[key].refHasContent) { + setValueInComponents( + value.keyInComponents, + components, + getContentFromTrace(documentContext.nodeContents[key], partial), + version + ); + } }); [rootContent, components].forEach((contentData) => { traverseUtility(contentData).forEach(function (property) { @@ -430,12 +461,12 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver return missingNode.path === nodeRef; }); if (isMissingNode) { - refData.nodeContent = { - [tempRef]: 'This related node was not found in provided data' - }; - refData.inline = true; + refData.nodeContent = refData.node; refData.local = false; } + else if (!refData) { + return; + } else { refData.nodeContent = documentContext.nodeContents[nodeRef]; refData.inline = refData.keyInComponents.length === 0; @@ -454,12 +485,14 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver } this.update(refData.node); if (!refData.inline) { - setValueInComponents( - refData.keyInComponents, - components, - refData.nodeContent, - version - ); + if (documentContext.globalReferences[tempRef].refHasContent) { + setValueInComponents( + refData.keyInComponents, + components, + refData.nodeContent, + version + ); + } } } } diff --git a/lib/parse.js b/lib/parse.js index 80ff638..5923d34 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -96,9 +96,11 @@ module.exports = { * @param {Object} options computed process options * @param {Object} files Files map * @param {string} specificationVersion the string of the desired version + * @param {boolean} allowReadingFS wheter to allow reading content from file system * @return {String} rootFile */ - getRootFiles: function (input, inputValidation, options, files = {}, specificationVersion) { + getRootFiles: function (input, inputValidation, options, files = {}, specificationVersion, + allowReadingFS = true) { let rootFilesArray = [], filesPathArray = input.data, origin = input.origin || ''; @@ -114,7 +116,7 @@ module.exports = { if (!_.isEmpty(files)) { file = files[path.resolve(filePath.fileName)]; } - else { + else if (allowReadingFS) { file = fs.readFileSync(filePath.fileName, 'utf8'); } diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index c3dc25e..86c47c6 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -4851,7 +4851,9 @@ module.exports = { let bundledFile = contentAndComponents.fileContent, bundleOutput; - bundledFile.components = contentAndComponents.components; + if (!_.isEmpty(contentAndComponents.components)) { + bundledFile.components = contentAndComponents.components; + } if (!format) { let rootFormat = parsedRootFiles.find((inputRoot) => { return inputRoot.fileName === contentAndComponents.fileName; diff --git a/lib/schemapack.js b/lib/schemapack.js index 073a890..901a654 100644 --- a/lib/schemapack.js +++ b/lib/schemapack.js @@ -644,15 +644,13 @@ class SchemaPack { path = pathBrowserify; OasResolverOptions.browser = true; } - if ('content' in input.data[0]) { - input.data.forEach((file) => { - files[path.resolve(file.fileName)] = file.content ? file.content : ''; - }); - } + input.data.forEach((file) => { + files[path.resolve(file.fileName)] = file.content ? file.content : ''; + }); adaptedInput = schemaUtils.mapDetectRootFilesInputToGetRootFilesInput(input); adaptedInput.origin = input.origin; rootFiles = parse.getRootFiles(adaptedInput, concreteUtils.inputValidation, this.computedOptions, files, - input.specificationVersion); + input.specificationVersion, false); res = schemaUtils.mapGetRootFilesOutputToDetectRootFilesOutput(rootFiles, input.specificationVersion); return res; } diff --git a/test/data/toBundleExamples/local_references/expectedNodeNotProvided.json b/test/data/toBundleExamples/local_references/expectedNodeNotProvided.json index 9a8dd9c..5f1c8d1 100644 --- a/test/data/toBundleExamples/local_references/expectedNodeNotProvided.json +++ b/test/data/toBundleExamples/local_references/expectedNodeNotProvided.json @@ -60,7 +60,7 @@ }, "components": { "responses": { - "/responses.yaml": "This related node was not found in provided data" + "$ref": "./responses.yaml" }, "schemas": { "Monster": { diff --git a/test/unit/bundle.test.js b/test/unit/bundle.test.js index 1400651..9341219 100644 --- a/test/unit/bundle.test.js +++ b/test/unit/bundle.test.js @@ -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); }); - it('Should return a "/missing/node/path": NotProvided' + - ' in the place of a not providen node - local_references', async function () { + it('Should return the not handled reference ($ref: ./responses.yaml) ' + + 'in the place of a not provided node - local_references', async function () { let contentRootFile = fs.readFileSync(localRefFolder + '/root.yaml', 'utf8'), schemasIndex = fs.readFileSync(localRefFolder + '/schemas/index.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 () { let contentRootFile = fs.readFileSync(schemaCollision + '/root.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(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', '3.0', {}, - '' + '', + [] ); expect(result.nodeReferenceDirectory).to.be.an('object'); expect(Object.keys(result.nodeReferenceDirectory).length).to.equal(1); diff --git a/test/unit/detectRoot.test.js b/test/unit/detectRoot.test.js index 34b07f9..d7cc681 100644 --- a/test/unit/detectRoot.test.js +++ b/test/unit/detectRoot.test.js @@ -246,7 +246,7 @@ describe('detectRoot method', function() { 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 = { type: 'multiFile', specificationVersion: '3.1.0', @@ -262,7 +262,7 @@ describe('detectRoot method', function() { const res = await Converter.detectRootFiles(input); expect(res).to.not.be.empty; 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); + + }); });