From 70ea0fe3c146d0fd91bf9b2d5aaedb878570ee07 Mon Sep 17 00:00:00 2001 From: Luis Tejeda <46000487+LuisTejedaS@users.noreply.github.com> Date: Wed, 13 Jul 2022 15:50:39 -0500 Subject: [PATCH] Support circularRefs inline Support circularRefs inline --- lib/bundle.js | 72 +++++++++++----- .../circular_reference/expected.json | 6 +- .../circular_reference/root.yaml | 2 +- .../circular_reference_inline/expected.json | 82 +++++++++++++++++++ .../circular_reference_inline/root.yaml | 27 ++++++ .../schemas/schemas.yaml | 32 ++++++++ test/unit/bundle.test.js | 35 +++++++- 7 files changed, 229 insertions(+), 27 deletions(-) create mode 100644 test/data/toBundleExamples/circular_reference_inline/expected.json create mode 100644 test/data/toBundleExamples/circular_reference_inline/root.yaml create mode 100644 test/data/toBundleExamples/circular_reference_inline/schemas/schemas.yaml diff --git a/lib/bundle.js b/lib/bundle.js index f7108fc..2abe7a4 100644 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -22,7 +22,8 @@ let path = require('path'), { DFS } = require('./dfs'), deref = require('./deref.js'), { isSwagger, getBundleRulesDataByVersion } = require('./common/versionUtils'), - CIRCULAR_REF_EXT_PROP = 'x-circularRef'; + CIRCULAR_REF_EXT_PROP = 'x-circularRef', + CIRCULAR_Or_REF_EXT_PROP = 'x-orRef'; /** @@ -481,6 +482,33 @@ function findReferenceByMainKeyInTraceFromContext(documentContext, mainKeyInTrac return relatedRef; } +/** + * Verifies if a node has same content than one of the parents so it is a circular ref + * @param {function} traverseContext - The context of the traverse function + * @param {object} contentFromTrace - The resolved content of the node to deref + * @returns {boolean} whether is circular reference or not. + */ +function isCircularReference(traverseContext, contentFromTrace) { + return traverseContext.parents.find((parent) => { return parent.node === contentFromTrace; }) !== undefined; +} + +/** + * Modifies content of a node if it is circular reference. + * @param {function} traverseContext - The context of the traverse function + * @param {object} documentContext The document context from root + * @returns {undefined} nothing + */ +function handleCircularReference(traverseContext, documentContext) { + let relatedRef = ''; + if (traverseContext.circular) { + relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, traverseContext.circular.key); + traverseContext.update({ $ref: relatedRef, [CIRCULAR_REF_EXT_PROP]: true }); + } + if (traverseContext.keys && traverseContext.keys.includes(CIRCULAR_Or_REF_EXT_PROP)) { + traverseContext.update({ $ref: traverseContext.node[CIRCULAR_Or_REF_EXT_PROP], [CIRCULAR_REF_EXT_PROP]: true }); + } +} + /** * Generates the components object from the documentContext data * @param {object} documentContext The document context from root @@ -541,6 +569,16 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver if (!contentFromTrace) { refData.nodeContent = { $ref: `${localPointer + local}` }; } + else if (isCircularReference(this, contentFromTrace)) { + if (refData.inline) { + refData.nodeContent = { [CIRCULAR_Or_REF_EXT_PROP]: tempRef, [CIRCULAR_REF_EXT_PROP]: true }; + } + else { + refData.node = { [CIRCULAR_Or_REF_EXT_PROP]: refData.reference, + [CIRCULAR_REF_EXT_PROP]: true }; + refData.nodeContent = contentFromTrace; + } + } else { refData.nodeContent = contentFromTrace; } @@ -551,38 +589,28 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver refData.node = hasSiblings ? _.merge(referenceSiblings, refData.nodeContent) : refData.nodeContent; - documentContext.globalReferences[property.$ref].reference = + documentContext.globalReferences[tempRef].reference = resolveJsonPointerInlineNodes(this.parents, this.key); } - this.update(refData.node); - if (!refData.inline) { - if (documentContext.globalReferences[tempRef].refHasContent) { - setValueInComponents( - refData.keyInComponents, - components, - refData.nodeContent, - COMPONENTS_KEYS - ); - } + else if (refData.refHasContent) { + setValueInComponents( + refData.keyInComponents, + components, + refData.nodeContent, + COMPONENTS_KEYS + ); } + this.update(refData.node); } } }); }); return { resRoot: traverseUtility(rootContent).map(function () { - let relatedRef = ''; - if (this.circular) { - relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, this.circular.key); - this.update({ $ref: relatedRef, [CIRCULAR_REF_EXT_PROP]: true }); - } + handleCircularReference(this, documentContext); }), newComponents: traverseUtility(components).map(function () { - let relatedRef = ''; - if (this.circular) { - relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, this.circular.key); - this.update({ $ref: relatedRef, [CIRCULAR_REF_EXT_PROP]: true }); - } + handleCircularReference(this, documentContext); }) }; } diff --git a/test/data/toBundleExamples/circular_reference/expected.json b/test/data/toBundleExamples/circular_reference/expected.json index 6cc400f..a33a9f9 100644 --- a/test/data/toBundleExamples/circular_reference/expected.json +++ b/test/data/toBundleExamples/circular_reference/expected.json @@ -28,7 +28,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/_schemas_schemas.yaml-components_schemas_ErrorDetail" + "$ref": "#/components/schemas/_schemas_schemas.yaml-_components_schemas_ErrorDetail" } } } @@ -40,7 +40,7 @@ }, "components": { "schemas": { - "_schemas_schemas.yaml-components_schemas_ErrorDetail": { + "_schemas_schemas.yaml-_components_schemas_ErrorDetail": { "type": "object", "description": "The error detail.", "properties": { @@ -63,7 +63,7 @@ "readOnly": true, "type": "array", "items": { - "$ref": "#/components/schemas/_schemas_schemas.yaml-components_schemas_ErrorDetail", + "$ref": "#/components/schemas/_schemas_schemas.yaml-_components_schemas_ErrorDetail", "x-circularRef": true }, "description": "The error details." diff --git a/test/data/toBundleExamples/circular_reference/root.yaml b/test/data/toBundleExamples/circular_reference/root.yaml index 79fe8e2..11a53ab 100644 --- a/test/data/toBundleExamples/circular_reference/root.yaml +++ b/test/data/toBundleExamples/circular_reference/root.yaml @@ -25,4 +25,4 @@ paths: schema: type: array items: - $ref: "./schemas/schemas.yaml#components/schemas/ErrorDetail" + $ref: "./schemas/schemas.yaml#/components/schemas/ErrorDetail" diff --git a/test/data/toBundleExamples/circular_reference_inline/expected.json b/test/data/toBundleExamples/circular_reference_inline/expected.json new file mode 100644 index 0000000..098bbd0 --- /dev/null +++ b/test/data/toBundleExamples/circular_reference_inline/expected.json @@ -0,0 +1,82 @@ +{ + "openapi": "3.0.2", + "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": "An paged array of pets", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/_schemas_schemas.yaml-_components_schemas_ErrorResponse" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "_schemas_schemas.yaml-_components_schemas_ErrorResponse": { + "title": "Error response", + "description": "Common error response for all Azure Resource Manager APIs to return error details for failed operations. (This also follows the OData error response format.).", + "type": "object", + "properties": { + "error": { + "type": "object", + "description": "The error detail.", + "properties": { + "code": { + "readOnly": true, + "type": "string", + "description": "The error code." + }, + "message": { + "readOnly": true, + "type": "string", + "description": "The error message." + }, + "target": { + "readOnly": true, + "type": "string", + "description": "The error target." + }, + "details": { + "readOnly": true, + "type": "array", + "items": { + "$ref": "/schemas/schemas.yaml#components/schemas/ErrorDetail", + "x-circularRef": true + }, + "description": "The error details." + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/test/data/toBundleExamples/circular_reference_inline/root.yaml b/test/data/toBundleExamples/circular_reference_inline/root.yaml new file mode 100644 index 0000000..f8c84d6 --- /dev/null +++ b/test/data/toBundleExamples/circular_reference_inline/root.yaml @@ -0,0 +1,27 @@ +openapi: "3.0.2" +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: An paged array of pets + content: + application/json: + schema: + type: array + items: + $ref: "./schemas/schemas.yaml#/components/schemas/ErrorResponse" diff --git a/test/data/toBundleExamples/circular_reference_inline/schemas/schemas.yaml b/test/data/toBundleExamples/circular_reference_inline/schemas/schemas.yaml new file mode 100644 index 0000000..69a10f8 --- /dev/null +++ b/test/data/toBundleExamples/circular_reference_inline/schemas/schemas.yaml @@ -0,0 +1,32 @@ +components: + schemas: + ErrorDetail: + type: object + description: The error detail. + properties: + code: + readOnly: true + type: string + description: The error code. + message: + readOnly: true + type: string + description: The error message. + target: + readOnly: true + type: string + description: The error target. + details: + readOnly: true + type: array + items: + $ref: "#components/schemas/ErrorDetail" + description: The error details. + ErrorResponse: + title: "Error response" + description: "Common error response for all Azure Resource Manager APIs to return error details for failed operations. (This also follows the OData error response format.)." + type: "object" + properties: + error: + description: "The error object." + $ref: "#components/schemas/ErrorDetail" diff --git a/test/unit/bundle.test.js b/test/unit/bundle.test.js index f4702c0..c4a0442 100644 --- a/test/unit/bundle.test.js +++ b/test/unit/bundle.test.js @@ -48,7 +48,8 @@ let expect = require('chai').expect, referencedPathSchema = path.join(__dirname, BUNDLES_FOLDER + '/paths_schema'), exampleValue = path.join(__dirname, BUNDLES_FOLDER + '/example_value'), example2 = path.join(__dirname, BUNDLES_FOLDER + '/example2'), - schemaCircularRef = path.join(__dirname, BUNDLES_FOLDER + '/circular_reference'); + schemaCircularRef = path.join(__dirname, BUNDLES_FOLDER + '/circular_reference'), + schemaCircularRefInline = path.join(__dirname, BUNDLES_FOLDER + '/circular_reference_inline'); describe('bundle files method - 3.0', function () { it('Should return bundled file as json - schema_from_response', async function () { @@ -2646,6 +2647,38 @@ 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 resolve circular reference in schema correctly resolved inline', async function () { + let contentRootFile = fs.readFileSync(schemaCircularRefInline + '/root.yaml', 'utf8'), + schema = fs.readFileSync(schemaCircularRefInline + '/schemas/schemas.yaml', 'utf8'), + expected = fs.readFileSync(schemaCircularRefInline + '/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/schemas/schemas.yaml', + content: schema + } + ], + 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() {