diff --git a/lib/bundle.js b/lib/bundle.js index 3f98348..a4210b1 100644 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -247,7 +247,14 @@ function getTraceFromParentKeyInComponents(nodeContext, tempRef, mainKeys, versi [key, ...parentKeys], nodeTrace = getRootFileTrace(nodeParentsKey), componentKey = createComponentMainKey(tempRef, mainKeys), - keyTraceInComponents = getKeyInComponents(nodeTrace, componentKey, version, commonPathFromData); + parentNodeKey = nodeContext.parent.key, + keyTraceInComponents = getKeyInComponents( + nodeTrace, + componentKey, + version, + commonPathFromData, + parentNodeKey + ); return keyTraceInComponents; } @@ -518,10 +525,14 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver * Generates the components object wrapper * @param {object} parsedOasObject The parsed root * @param {string} version - The current version + * @param {object} nodesContent - The nodes content * @returns {object} The components object wrapper */ -function generateComponentsWrapper(parsedOasObject, version) { - let components = {}; +function generateComponentsWrapper(parsedOasObject, version, nodesContent = {}) { + let components = _.isNil(parsedOasObject.components) ? + {} : + parsedOasObject.components, + componentsAreReferenced = components.$ref !== undefined && !_.isEmpty(nodesContent); if (isSwagger(version)) { getBundleRulesDataByVersion(version).COMPONENTS_KEYS.forEach((property) => { @@ -531,7 +542,10 @@ function generateComponentsWrapper(parsedOasObject, version) { }); } else if (parsedOasObject.hasOwnProperty('components')) { - components = parsedOasObject.components; + if (componentsAreReferenced) { + components = _.merge(parsedOasObject.components, nodesContent[components.$ref]); + delete components.$ref; + } } return components; @@ -604,7 +618,11 @@ module.exports = { rootContextData = algorithm.traverseAndBundle(specRoot, (currentNode) => { return getNodeContentAndReferences(currentNode, allData, specRoot, version, initialMainKeys, commonPathFromData); }); - components = generateComponentsWrapper(specRoot.parsed.oasObject, version); + components = generateComponentsWrapper( + specRoot.parsed.oasObject, + version, + rootContextData.nodeContents + ); generateComponentsObject( rootContextData, rootContextData.nodeContents[specRoot.fileName], diff --git a/lib/jsonPointer.js b/lib/jsonPointer.js index 1939883..ea27064 100644 --- a/lib/jsonPointer.js +++ b/lib/jsonPointer.js @@ -52,9 +52,10 @@ function generateObjectName(filePathName, hash = '') { * @param {string} mainKey - The generated mainKey for the components * @param {string} version - The current spec version * @param {string} commonPathFromData - The common path in the file's paths +* @param {string} parentNodeKey - The key from the parent element of the trace * @returns {Array} - the calculated keys in an array representing each nesting property name */ -function getKeyInComponents(traceFromParent, mainKey, version, commonPathFromData) { +function getKeyInComponents(traceFromParent, mainKey, version, commonPathFromData, parentNodeKey = undefined) { const { CONTAINERS, DEFINITIONS, @@ -70,9 +71,11 @@ function getKeyInComponents(traceFromParent, mainKey, version, commonPathFromDat ].reverse(), traceToKey = [], matchFound = false, - isRootAndReusableItemsContainer = ROOT_CONTAINERS_KEYS.includes(traceFromParent[0]); + hasNotParent = parentNodeKey === undefined, + isRootAndReusableItemsContainer = ROOT_CONTAINERS_KEYS.includes(traceFromParent[0]), + isAComponentKeyReferenced = COMPONENTS_KEYS.includes(traceFromParent[0]) && hasNotParent; - if (isRootAndReusableItemsContainer) { + if (isRootAndReusableItemsContainer || isAComponentKeyReferenced) { return []; } diff --git a/test/data/toBundleExamples/referenced_components/components.yaml b/test/data/toBundleExamples/referenced_components/components.yaml new file mode 100644 index 0000000..c87ca43 --- /dev/null +++ b/test/data/toBundleExamples/referenced_components/components.yaml @@ -0,0 +1,4 @@ +responses: + $ref: './responses.yaml' +schemas: + $ref: './schemas.yaml' \ No newline at end of file diff --git a/test/data/toBundleExamples/referenced_components/expected.json b/test/data/toBundleExamples/referenced_components/expected.json new file mode 100644 index 0000000..3e96ee9 --- /dev/null +++ b/test/data/toBundleExamples/referenced_components/expected.json @@ -0,0 +1,82 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Sample API", + "description": "Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.", + "version": "0.1.9" + }, + "paths": { + "/hello": { + "get": { + "description": "Returns all pets alesuada ac...", + "operationId": "findPets", + "responses": { + "$ref": "#/components/responses/responseA" + } + } + } + }, + "components": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "address": { + "type": "string" + } + } + } + } + } + }, + "responseA": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/_schemaA.yaml" + } + } + } + }, + "responseB": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "fromB": { + "type": "string" + }, + "addressFromB": { + "type": "string" + } + } + } + } + } + } + }, + "schemas": { + "_schemaA.yaml": { + "type": "object", + "properties": { + "fromA": { + "type": "string" + }, + "addressFromA": { + "type": "string" + } + } + }, + "schemaB": { + "type": "integer" + } + } + } +} \ No newline at end of file diff --git a/test/data/toBundleExamples/referenced_components/responses.yaml b/test/data/toBundleExamples/referenced_components/responses.yaml new file mode 100644 index 0000000..4f9f8a0 --- /dev/null +++ b/test/data/toBundleExamples/referenced_components/responses.yaml @@ -0,0 +1,25 @@ +"200": + content: + application/json: + schema: + type: object + properties: + name: + type: string + address: + type: string +"responseA": + content: + application/json: + schema: + $ref: './schemaA.yaml' +"responseB": + content: + application/json: + schema: + type: object + properties: + fromB: + type: string + addressFromB: + type: string \ No newline at end of file diff --git a/test/data/toBundleExamples/referenced_components/root.yaml b/test/data/toBundleExamples/referenced_components/root.yaml new file mode 100644 index 0000000..cbb7d61 --- /dev/null +++ b/test/data/toBundleExamples/referenced_components/root.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.0 +info: + title: Sample API + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. + version: 0.1.9 +paths: + /hello: + get: + description: Returns all pets alesuada ac... + operationId: findPets + responses: + $ref: '#/components/responses/responseA' + +components: + $ref: './components.yaml' \ No newline at end of file diff --git a/test/data/toBundleExamples/referenced_components/schemaA.yaml b/test/data/toBundleExamples/referenced_components/schemaA.yaml new file mode 100644 index 0000000..74c10ec --- /dev/null +++ b/test/data/toBundleExamples/referenced_components/schemaA.yaml @@ -0,0 +1,6 @@ +type: object +properties: + fromA: + type: string + addressFromA: + type: string \ No newline at end of file diff --git a/test/data/toBundleExamples/referenced_components/schemas.yaml b/test/data/toBundleExamples/referenced_components/schemas.yaml new file mode 100644 index 0000000..ea6be40 --- /dev/null +++ b/test/data/toBundleExamples/referenced_components/schemas.yaml @@ -0,0 +1,2 @@ +schemaB: + type: integer \ No newline at end of file diff --git a/test/data/toBundleExamples/swagger20/referenced_root_components/definitions.yaml b/test/data/toBundleExamples/swagger20/referenced_root_components/definitions.yaml new file mode 100644 index 0000000..7ec0b9c --- /dev/null +++ b/test/data/toBundleExamples/swagger20/referenced_root_components/definitions.yaml @@ -0,0 +1,22 @@ +Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string +Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string \ No newline at end of file diff --git a/test/data/toBundleExamples/swagger20/referenced_root_components/expected.json b/test/data/toBundleExamples/swagger20/referenced_root_components/expected.json new file mode 100644 index 0000000..654b54d --- /dev/null +++ b/test/data/toBundleExamples/swagger20/referenced_root_components/expected.json @@ -0,0 +1,141 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Swagger API Team", + "email": "apiteam@swagger.io", + "url": "http://swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "paths": { + "/pets": { + "get": { + "description": "Returns all pets alesuada ac...", + "operationId": "findPets", + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "definitions": { + "Pet": { + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Error": { + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "type": "oauth2", + "authorizationUrl": "http://swagger.io/api/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + }, + "tags": [ + { + "name": "Authorization", + "x-bx-tag": "authorization", + "x-bx-priority": true + }, + { + "name": "Bx Sign", + "x-bx-tag": "sign_requests" + } + ], + "responses": { + "200": { + "description": "A simple string response", + "schema": { + "type": "string" + } + }, + "400": { + "description": "A simple string response from 400 code", + "schema": { + "type": "string" + } + } + } +} \ No newline at end of file diff --git a/test/data/toBundleExamples/swagger20/referenced_root_components/info.yaml b/test/data/toBundleExamples/swagger20/referenced_root_components/info.yaml new file mode 100644 index 0000000..2ddd567 --- /dev/null +++ b/test/data/toBundleExamples/swagger20/referenced_root_components/info.yaml @@ -0,0 +1,11 @@ +version: 1.0.0 +title: Swagger Petstore +description: A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification +termsOfService: http://swagger.io/terms/ +contact: + name: Swagger API Team + email: apiteam@swagger.io + url: http://swagger.io +license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html \ No newline at end of file diff --git a/test/data/toBundleExamples/swagger20/referenced_root_components/paths/path.yaml b/test/data/toBundleExamples/swagger20/referenced_root_components/paths/path.yaml new file mode 100644 index 0000000..dab6ca5 --- /dev/null +++ b/test/data/toBundleExamples/swagger20/referenced_root_components/paths/path.yaml @@ -0,0 +1,31 @@ +description: Returns all pets alesuada ac... +operationId: findPets +responses: + "200": + description: pet response + content: + application/json: + schema: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + default: + description: unexpected error + content: + application/json: + schema: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/test/data/toBundleExamples/swagger20/referenced_root_components/paths/paths.yaml b/test/data/toBundleExamples/swagger20/referenced_root_components/paths/paths.yaml new file mode 100644 index 0000000..e826f5c --- /dev/null +++ b/test/data/toBundleExamples/swagger20/referenced_root_components/paths/paths.yaml @@ -0,0 +1,3 @@ +/pets: + get: + "$ref": "./path.yaml" diff --git a/test/data/toBundleExamples/swagger20/referenced_root_components/responses.yaml b/test/data/toBundleExamples/swagger20/referenced_root_components/responses.yaml new file mode 100644 index 0000000..0fa534b --- /dev/null +++ b/test/data/toBundleExamples/swagger20/referenced_root_components/responses.yaml @@ -0,0 +1,8 @@ +'200': + description: A simple string response + schema: + type: string +'400': + description: A simple string response from 400 code + schema: + type: string diff --git a/test/data/toBundleExamples/swagger20/referenced_root_components/root.yaml b/test/data/toBundleExamples/swagger20/referenced_root_components/root.yaml new file mode 100644 index 0000000..8cd48a6 --- /dev/null +++ b/test/data/toBundleExamples/swagger20/referenced_root_components/root.yaml @@ -0,0 +1,13 @@ +swagger: '2.0' +info: + $ref: './info.yaml' +paths: + "$ref": "./paths/paths.yaml" +definitions: + $ref: './definitions.yaml' +securityDefinitions: + $ref: './securitySchemes.yaml' +tags: + $ref: './tags.yaml' +responses: + $ref: './responses.yaml' \ No newline at end of file diff --git a/test/data/toBundleExamples/swagger20/referenced_root_components/securitySchemes.yaml b/test/data/toBundleExamples/swagger20/referenced_root_components/securitySchemes.yaml new file mode 100644 index 0000000..68a4da3 --- /dev/null +++ b/test/data/toBundleExamples/swagger20/referenced_root_components/securitySchemes.yaml @@ -0,0 +1,6 @@ +type: oauth2 +authorizationUrl: http://swagger.io/api/oauth/dialog +flow: implicit +scopes: + write:pets: modify pets in your account + read:pets: read your pets diff --git a/test/data/toBundleExamples/swagger20/referenced_root_components/tags.yaml b/test/data/toBundleExamples/swagger20/referenced_root_components/tags.yaml new file mode 100644 index 0000000..a123fd4 --- /dev/null +++ b/test/data/toBundleExamples/swagger20/referenced_root_components/tags.yaml @@ -0,0 +1,6 @@ +- name: Authorization + x-bx-tag: authorization + x-bx-priority: true + +- name: Bx Sign + x-bx-tag: sign_requests diff --git a/test/unit/bundle.test.js b/test/unit/bundle.test.js index 1f4f994..733b01c 100644 --- a/test/unit/bundle.test.js +++ b/test/unit/bundle.test.js @@ -43,6 +43,7 @@ let expect = require('chai').expect, schemaCollisionWRootComponent = path.join(__dirname, BUNDLES_FOLDER + '/schema_collision_w_root_components'), nestedExamplesAsValue = path.join(__dirname, BUNDLES_FOLDER + '/nested_examples_as_value'), referencedProperties = path.join(__dirname, BUNDLES_FOLDER + '/referenced_properties'), + referencedComponents = path.join(__dirname, BUNDLES_FOLDER + '/referenced_components'), referencedPath = path.join(__dirname, BUNDLES_FOLDER + '/referenced_path'); describe('bundle files method - 3.0', function () { @@ -2402,6 +2403,54 @@ describe('bundle files method - 3.0', function () { expect(res.result).to.be.true; expect(res.output.data[0].referenceMap).to.deep.equal(expected); }); + + it('Should return bundled file - referenced-components', async function () { + let contentRootFile = fs.readFileSync(referencedComponents + '/root.yaml', 'utf8'), + components = fs.readFileSync(referencedComponents + '/components.yaml', 'utf8'), + responses = fs.readFileSync(referencedComponents + '/responses.yaml', 'utf8'), + schemas = fs.readFileSync(referencedComponents + '/schemas.yaml', 'utf8'), + schemaA = fs.readFileSync(referencedComponents + '/schemaA.yaml', 'utf8'), + expected = fs.readFileSync(referencedComponents + '/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/components.yaml', + content: components + }, + { + path: '/responses.yaml', + content: responses + }, + { + path: '/schemas.yaml', + content: schemas + }, + { + path: '/schemaA.yaml', + content: schemaA + } + ], + options: {}, + bundleFormat: 'JSON' + }; + 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(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); + }); }); describe('getReferences method when node does not have any reference', function() { diff --git a/test/unit/bundle20.test.js b/test/unit/bundle20.test.js index 99034dd..39eec5e 100644 --- a/test/unit/bundle20.test.js +++ b/test/unit/bundle20.test.js @@ -27,7 +27,9 @@ let expect = require('chai').expect, schemaCollision = path.join(__dirname, SWAGGER_MULTIFILE_FOLDER + '/schema_collision_from_responses'), schemaCollisionWRootComponent = path.join(__dirname, SWAGGER_MULTIFILE_FOLDER + - '/schema_collision_w_root_components'); + '/schema_collision_w_root_components'), + referencedRootComponents = path.join(__dirname, SWAGGER_MULTIFILE_FOLDER + + '/referenced_root_components'); describe('bundle files method - 2.0', function() { it('Should return bundled result from - nestedProperties20', async function() { @@ -906,4 +908,67 @@ describe('bundle files method - 2.0', function() { expect(res.output.specification.version).to.equal('2.0'); expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); }); + + it('Should return bundled file as json - referenced_root_components', async function () { + let contentRootFile = fs.readFileSync(referencedRootComponents + '/root.yaml', 'utf8'), + definitions = fs.readFileSync(referencedRootComponents + '/definitions.yaml', 'utf8'), + info = fs.readFileSync(referencedRootComponents + '/info.yaml', 'utf8'), + responses = fs.readFileSync(referencedRootComponents + '/responses.yaml', 'utf8'), + securitySchemes = fs.readFileSync(referencedRootComponents + '/securitySchemes.yaml', 'utf8'), + tags = fs.readFileSync(referencedRootComponents + '/tags.yaml', 'utf8'), + paths = fs.readFileSync(referencedRootComponents + '/paths/paths.yaml', 'utf8'), + singlePath = fs.readFileSync(referencedRootComponents + '/paths/path.yaml', 'utf8'), + expected = fs.readFileSync(referencedRootComponents + '/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '2.0', + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/definitions.yaml', + content: definitions + }, + { + path: '/info.yaml', + content: info + }, + { + path: '/responses.yaml', + content: responses + }, + { + path: '/securitySchemes.yaml', + content: securitySchemes + }, + { + path: '/tags.yaml', + content: tags + }, + { + path: '/paths/paths.yaml', + content: paths + }, + { + path: '/paths/path.yaml', + content: singlePath + } + ], + options: {}, + bundleFormat: 'JSON' + }; + 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('2.0'); + expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); + }); });