From 7c482b97b8d70d76b46de2297a2ec70599694d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Tue, 7 Nov 2023 13:54:01 +0100 Subject: [PATCH] fix(resolve): fix critical bug in OpenAPI 3.1.0 cycle detection (#3226) Refs https://github.com/swagger-api/swagger-ui/issues/9337 --- package-lock.json | 121 ++++++++--------- package.json | 9 +- .../visitors/dereference.js | 117 ++++++++++++---- .../strategies/openapi-3-1-apidom/resolve.js | 2 +- .../dereferenced.json | 8 +- .../dereferenced.json | 7 +- .../dereferenced.json | 7 +- .../dereferenced.json | 8 +- .../cycle-internal-advanced-2/root.json | 55 ++++++++ .../dereferenced.json | 78 +++++++++++ .../root.json | 55 ++++++++ .../dereferenced.json | 78 +++++++++++ .../root.json | 55 ++++++++ .../schema-object/index.js | 125 ++++++++++++++++-- 14 files changed, 623 insertions(+), 102 deletions(-) create mode 100644 test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-2/root.json create mode 100644 test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-circular-structures-2/dereferenced.json create mode 100644 test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-circular-structures-2/root.json create mode 100644 test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-http-circular-structures-2/dereferenced.json create mode 100644 test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-http-circular-structures-2/root.json diff --git a/package-lock.json b/package-lock.json index 0969f9557..d4cc4c4b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,11 @@ "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.22.15", - "@swagger-api/apidom-core": ">=0.82.2 <1.0.0", - "@swagger-api/apidom-json-pointer": ">=0.82.2 <1.0.0", - "@swagger-api/apidom-ns-openapi-3-1": ">=0.82.2 <1.0.0", - "@swagger-api/apidom-reference": ">=0.82.2 <1.0.0", + "@swagger-api/apidom-core": ">=0.83.0 <1.0.0", + "@swagger-api/apidom-error": ">=0.83.0 <1.0.0", + "@swagger-api/apidom-json-pointer": ">=0.83.0 <1.0.0", + "@swagger-api/apidom-ns-openapi-3-1": ">=0.83.0 <1.0.0", + "@swagger-api/apidom-reference": ">=0.83.0 <1.0.0", "cookie": "~0.5.0", "deepmerge": "~4.3.0", "fast-json-patch": "^3.0.0-1", @@ -3662,12 +3663,12 @@ } }, "node_modules/@swagger-api/apidom-ast": { - "version": "0.82.2", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-0.82.2.tgz", - "integrity": "sha512-k41OHMe5FftHFJhj5LH+Y44BA4/ddoVH4vUv36tW+fU3qkC350VmkdMVglD0BhwZA9S8OpCSz4xmRfbyOGHirw==", + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-0.83.0.tgz", + "integrity": "sha512-zAn9kHFi2JmEldYxzw6x7rbKxL4NVWvOeCWQL0AlwcWHPRhW+16/1VeHNhoWeiWm6QMERNT8z0o5frg+2czb6g==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-error": "^0.82.1", + "@swagger-api/apidom-error": "^0.83.0", "@types/ramda": "~0.29.6", "ramda": "~0.29.0", "ramda-adjunct": "^4.1.1", @@ -3676,13 +3677,13 @@ } }, "node_modules/@swagger-api/apidom-core": { - "version": "0.82.2", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-0.82.2.tgz", - "integrity": "sha512-RVPpIA+qti1t116K3dhieofGvembdP3j7THs8+d0j3AMvz2/DK6+2uwLb2EptOAOAqWgIf/fycgwGBoo8/PyuQ==", + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-0.83.0.tgz", + "integrity": "sha512-4pWzSbxfYrS5rH7tl4WLO5nyR7pF+aAIymwsyV2Xrec44p6d4UZaJEn1iI3r9PBBdlmOHPKgr3QiOxn71Q3XUA==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^0.82.2", - "@swagger-api/apidom-error": "^0.82.1", + "@swagger-api/apidom-ast": "^0.83.0", + "@swagger-api/apidom-error": "^0.83.0", "@types/ramda": "~0.29.6", "minim": "~0.23.8", "ramda": "~0.29.0", @@ -3692,21 +3693,21 @@ } }, "node_modules/@swagger-api/apidom-error": { - "version": "0.82.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-error/-/apidom-error-0.82.1.tgz", - "integrity": "sha512-nL/7kDBtwf7JQqSWet1Bl0fMaCjxvyC5sKyNRGO1KzkB2XJp2DPOXsoXgPjnCvAG5ksgIa0LNyxUr+6hKbB19g==", + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-error/-/apidom-error-0.83.0.tgz", + "integrity": "sha512-0T3B+5Q2cApW0EkcMAqpgvsj+ab46HPvkVsYClA9/L0suRvyPiI5XDkHsw26qPGsmuB5nCH4hveZHlbWwRINMg==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7" } }, "node_modules/@swagger-api/apidom-json-pointer": { - "version": "0.82.2", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-0.82.2.tgz", - "integrity": "sha512-AQ9etS31kNDOVwpy7K9n9dvBYFmnbV7f/9zwrU/WElYdJzWVORxvCfTb7QjVjgQrZg+X387aHaI1LHqs1DE2Kg==", + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-0.83.0.tgz", + "integrity": "sha512-mT60Dfqfym9LisGcFEUV/ZwCWrcd/sI24ACAUr7D/gCMX2GuJHC7qrRwWVjGDaaDMVhDM5eCi6GKPjQhs0Ckmw==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.82.2", - "@swagger-api/apidom-error": "^0.82.1", + "@swagger-api/apidom-core": "^0.83.0", + "@swagger-api/apidom-error": "^0.83.0", "@types/ramda": "~0.29.6", "ramda": "~0.29.0", "ramda-adjunct": "^4.0.0" @@ -3720,13 +3721,13 @@ "optional": true }, "node_modules/@swagger-api/apidom-ns-json-schema-draft-4": { - "version": "0.82.2", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-0.82.2.tgz", - "integrity": "sha512-cQxENlN8ZGCSHVgoVgZZ2kxPyUxae8tKG6b11Etx7XnTuGVwC5etD3kz2tQYSp8ovR7vVq0f5Fqz4T0enPRXnw==", + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-0.83.0.tgz", + "integrity": "sha512-boknhIfrXF1k9IxLV0CkO1EoeXed4mzDNbFNKTkIv7UAdFwAa7NiQLVlEehNY3Ufm3/PjVMzYVQ80tUbyQE2Sw==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^0.82.2", - "@swagger-api/apidom-core": "^0.82.2", + "@swagger-api/apidom-ast": "^0.83.0", + "@swagger-api/apidom-core": "^0.83.0", "@types/ramda": "~0.29.6", "ramda": "~0.29.0", "ramda-adjunct": "^4.1.1", @@ -3741,14 +3742,14 @@ "optional": true }, "node_modules/@swagger-api/apidom-ns-openapi-3-0": { - "version": "0.82.2", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-0.82.2.tgz", - "integrity": "sha512-u5MhdP1F+l8HpBhpBHMCGsBNtFGrgi6/ImDqXtjjzTx1syWce2GHjPnQMHup+HelK/IvXbTkSS5oQx+hbKJEvA==", + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-0.83.0.tgz", + "integrity": "sha512-OAN6buySWrWSvnctKVSxkG5HyUOVc8F87zHy8mxcKn91AaHPC6h8LBxIXcmXFDfZNvORZYTi7GFw3W+mnIMTwg==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.82.2", - "@swagger-api/apidom-error": "^0.82.1", - "@swagger-api/apidom-ns-json-schema-draft-4": "^0.82.2", + "@swagger-api/apidom-core": "^0.83.0", + "@swagger-api/apidom-error": "^0.83.0", + "@swagger-api/apidom-ns-json-schema-draft-4": "^0.83.0", "@types/ramda": "~0.29.6", "ramda": "~0.29.0", "ramda-adjunct": "^4.1.1", @@ -3756,14 +3757,14 @@ } }, "node_modules/@swagger-api/apidom-ns-openapi-3-1": { - "version": "0.82.2", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-0.82.2.tgz", - "integrity": "sha512-7f+mVam2zrdpXWSaWeaHkg+9vle2Pk3WuCLzT1SujbqdahN6znGi1jr6ScrO9SyaJOBPCRLr/mRMY+BBgyCW7g==", + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-0.83.0.tgz", + "integrity": "sha512-xD/T5f9Phqk4/FN5iaH8OM+5AbUqXQV92zdN5twrLCgCCA3l/1PMA7g9qEBTCG3f6UmyJ/6TTFOJyz7utye7Hg==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^0.82.2", - "@swagger-api/apidom-core": "^0.82.2", - "@swagger-api/apidom-ns-openapi-3-0": "^0.82.2", + "@swagger-api/apidom-ast": "^0.83.0", + "@swagger-api/apidom-core": "^0.83.0", + "@swagger-api/apidom-ns-openapi-3-0": "^0.83.0", "@types/ramda": "~0.29.6", "ramda": "~0.29.0", "ramda-adjunct": "^4.1.1", @@ -3855,12 +3856,12 @@ "optional": true }, "node_modules/@swagger-api/apidom-reference": { - "version": "0.82.2", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-0.82.2.tgz", - "integrity": "sha512-QWD3WuSwcPwhPvMz+c9JdEpUbV5sTw8PyVvRGkgH8vr+fWbSBnY0pOUg1ST4qdQKSZnhwVaKB8a1zQTsFtRYBw==", + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-0.83.0.tgz", + "integrity": "sha512-f7Pm3fQwjf1pqniV+9abkC+oYUAbL/31GCg58r8ou4Cx+5hGTpUg81caMjdeg5Y4+Txj2ZUaAaUYyigEV25i4w==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.82.2", + "@swagger-api/apidom-core": "^0.83.0", "@types/ramda": "~0.29.6", "axios": "^1.4.0", "minimatch": "^7.4.3", @@ -3870,24 +3871,24 @@ "stampit": "^4.3.2" }, "optionalDependencies": { - "@swagger-api/apidom-error": "^0.82.1", - "@swagger-api/apidom-json-pointer": "^0.82.2", - "@swagger-api/apidom-ns-asyncapi-2": "^0.82.2", - "@swagger-api/apidom-ns-openapi-2": "^0.82.2", - "@swagger-api/apidom-ns-openapi-3-0": "^0.82.2", - "@swagger-api/apidom-ns-openapi-3-1": "^0.82.2", - "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^0.82.2", - "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^0.82.2", - "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^0.82.2", - "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^0.82.2", - "@swagger-api/apidom-parser-adapter-json": "^0.82.2", - "@swagger-api/apidom-parser-adapter-openapi-json-2": "^0.82.2", - "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^0.82.2", - "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^0.82.2", - "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^0.82.2", - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.82.2", - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.82.2", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.82.2" + "@swagger-api/apidom-error": "^0.83.0", + "@swagger-api/apidom-json-pointer": "^0.83.0", + "@swagger-api/apidom-ns-asyncapi-2": "^0.83.0", + "@swagger-api/apidom-ns-openapi-2": "^0.83.0", + "@swagger-api/apidom-ns-openapi-3-0": "^0.83.0", + "@swagger-api/apidom-ns-openapi-3-1": "^0.83.0", + "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^0.83.0", + "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^0.83.0", + "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^0.83.0", + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^0.83.0", + "@swagger-api/apidom-parser-adapter-json": "^0.83.0", + "@swagger-api/apidom-parser-adapter-openapi-json-2": "^0.83.0", + "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^0.83.0", + "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^0.83.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^0.83.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.83.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.83.0", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.83.0" } }, "node_modules/@tootallnate/once": { diff --git a/package.json b/package.json index 597aa237c..5f85a2359 100644 --- a/package.json +++ b/package.json @@ -109,10 +109,11 @@ }, "dependencies": { "@babel/runtime-corejs3": "^7.22.15", - "@swagger-api/apidom-core": ">=0.82.2 <1.0.0", - "@swagger-api/apidom-json-pointer": ">=0.82.2 <1.0.0", - "@swagger-api/apidom-ns-openapi-3-1": ">=0.82.2 <1.0.0", - "@swagger-api/apidom-reference": ">=0.82.2 <1.0.0", + "@swagger-api/apidom-core": ">=0.83.0 <1.0.0", + "@swagger-api/apidom-error": ">=0.83.0 <1.0.0", + "@swagger-api/apidom-json-pointer": ">=0.83.0 <1.0.0", + "@swagger-api/apidom-ns-openapi-3-1": ">=0.83.0 <1.0.0", + "@swagger-api/apidom-reference": ">=0.83.0 <1.0.0", "cookie": "~0.5.0", "deepmerge": "~4.3.0", "fast-json-patch": "^3.0.0-1", diff --git a/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js b/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js index b63e3ce7e..66819c2de 100644 --- a/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js +++ b/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js @@ -4,11 +4,14 @@ import { isPrimitiveElement, isStringElement, isMemberElement, + IdentityManager, visit, + includesClasses, toValue, cloneShallow, cloneDeep, } from '@swagger-api/apidom-core'; +import { ApiDOMError } from '@swagger-api/apidom-error'; import { isReferenceElementExternal, isReferenceLikeElement, @@ -47,8 +50,22 @@ import specMapMod from '../../../../../../../specmap/lib/refs.js'; import { SchemaRefError } from '../errors/index.js'; const { wrapError } = specMapMod; + const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; +// initialize element identity manager +const identityManager = IdentityManager(); + +/** + * Predicate for detecting if element was created by merging referencing + * element with particular element identity with a referenced element. + */ +const wasReferencedBy = (referencingElement) => (element) => + element.meta.hasKey('ref-referencing-element-id') && + element.meta + .get('ref-referencing-element-id') + .equals(toValue(identityManager.identify(referencingElement))); + const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.compose({ props: { useCircularStructures: true, @@ -69,6 +86,11 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c try { const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]); + // skip already identified cycled Path Item Objects + if (includesClasses(['cycle'], referencingElement.$ref)) { + return false; + } + // detect possible cycle in traversal and avoid it if (ancestorsLineage.includesCycle(referencingElement)) { return false; @@ -107,7 +129,7 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c // detect direct or indirect reference if (this.indirections.includes(referencedElement)) { - throw new Error('Recursive JSON Pointer detected'); + throw new ApiDOMError('Recursive JSON Pointer detected'); } // detect maximum depth of dereferencing @@ -122,11 +144,13 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c if (hasCycles) { if (url.isHttpUrl(retrievalURI) || url.isFileSystemPath(retrievalURI)) { // make the referencing URL or file system path absolute - return new ReferenceElement( + const cycledReferenceElement = new ReferenceElement( { $ref: $refBaseURI }, cloneDeep(referencingElement.meta), cloneDeep(referencingElement.attributes) ); + cycledReferenceElement.get('$ref').classes.push('cycle'); + return cycledReferenceElement; } // skip processing this reference return false; @@ -166,26 +190,25 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c // annotate fragment with info about original Reference element copy.setMetaProperty('ref-fields', { $ref: toValue(referencingElement.$ref), - // @ts-ignore description: toValue(referencingElement.description), - // @ts-ignore summary: toValue(referencingElement.summary), }); // annotate fragment with info about origin copy.setMetaProperty('ref-origin', reference.uri); + // annotate fragment with info about referencing element + copy.setMetaProperty( + 'ref-referencing-element-id', + cloneDeep(identityManager.identify(referencingElement)) + ); // override description and summary (outer has higher priority then inner) if (isObjectElement(refedElement)) { if (referencingElement.hasKey('description') && 'description' in refedElement) { - // @ts-ignore copy.remove('description'); - // @ts-ignore copy.set('description', referencingElement.get('description')); } if (referencingElement.hasKey('summary') && 'summary' in refedElement) { - // @ts-ignore copy.remove('summary'); - // @ts-ignore copy.set('summary', referencingElement.get('summary')); } } @@ -203,13 +226,18 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c }; // attempting to create cycle - if (ancestorsLineage.includes(referencedElement)) { + if ( + ancestorsLineage.includes(referencingElement) || + ancestorsLineage.includes(referencedElement) + ) { + const replaceWith = + ancestorsLineage.findItem(wasReferencedBy(referencingElement)) ?? + mergeAndAnnotateReferencedElement(referencedElement); if (isMemberElement(parent)) { - parent.value = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign + parent.value = replaceWith; // eslint-disable-line no-param-reassign } else if (Array.isArray(parent)) { - parent[key] = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign + parent[key] = replaceWith; // eslint-disable-line no-param-reassign } - return false; } @@ -241,6 +269,11 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c return undefined; } + // skip already identified cycled Path Item Objects + if (includesClasses(['cycle'], pathItemElement.$ref)) { + return false; + } + // detect possible cycle in traversal and avoid it if (ancestorsLineage.includesCycle(pathItemElement)) { return false; @@ -269,7 +302,7 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c // detect direct or indirect reference if (this.indirections.includes(referencedElement)) { - throw new Error('Recursive JSON Pointer detected'); + throw new ApiDOMError('Recursive JSON Pointer detected'); } // detect maximum depth of dereferencing @@ -284,11 +317,13 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c if (hasCycles) { if (url.isHttpUrl(retrievalURI) || url.isFileSystemPath(retrievalURI)) { // make the referencing URL or file system path absolute - return new PathItemElement( + const cycledPathItemElement = new PathItemElement( { $ref: $refBaseURI }, cloneDeep(pathItemElement.meta), cloneDeep(pathItemElement.attributes) ); + cycledPathItemElement.get('$ref').classes.push('cycle'); + return cycledPathItemElement; } // skip processing this path item and all it's child elements return false; @@ -339,6 +374,11 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c }); // annotate referenced element with info about origin mergedElement.setMetaProperty('ref-origin', reference.uri); + // annotate fragment with info about referencing element + mergedElement.setMetaProperty( + 'ref-referencing-element-id', + cloneDeep(identityManager.identify(pathItemElement)) + ); // apply meta patches if (this.allowMetaPatches) { @@ -353,13 +393,18 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c }; // attempting to create cycle - if (ancestorsLineage.includes(referencedElement)) { + if ( + ancestorsLineage.includes(pathItemElement) || + ancestorsLineage.includes(referencedElement) + ) { + const replaceWith = + ancestorsLineage.findItem(wasReferencedBy(pathItemElement)) ?? + mergeAndAnnotateReferencedElement(referencedElement); if (isMemberElement(parent)) { - parent.value = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign + parent.value = replaceWith; // eslint-disable-line no-param-reassign } else if (Array.isArray(parent)) { - parent[key] = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign + parent[key] = replaceWith; // eslint-disable-line no-param-reassign } - return false; } @@ -389,6 +434,11 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c return undefined; } + // skip already identified cycled Path Item Objects + if (includesClasses(['cycle'], referencingElement.$ref)) { + return false; + } + // detect possible cycle in traversal and avoid it if (ancestorsLineage.includesCycle(referencingElement)) { return false; @@ -463,7 +513,7 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c // detect direct or indirect reference if (this.indirections.includes(referencedElement)) { - throw new Error('Recursive Schema Object reference detected'); + throw new ApiDOMError('Recursive Schema Object reference detected'); } // detect maximum depth of dereferencing @@ -475,16 +525,18 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c // useCircularStructures option processing if (!this.useCircularStructures) { - const hasCycles = ancestorsLineage.some((ancs) => ancs.has(referencedElement)); + const hasCycles = ancestorsLineage.includes(referencedElement); if (hasCycles) { if (url.isHttpUrl(retrievalURI) || url.isFileSystemPath(retrievalURI)) { // make the referencing URL or file system path absolute const baseURI = url.resolve(retrievalURI, $refBaseURI); - return new SchemaElement( + const cycledSchemaElement = new SchemaElement( { $ref: baseURI }, cloneDeep(referencingElement.meta), cloneDeep(referencingElement.attributes) ); + cycledSchemaElement.get('$ref').classes.push('cycle'); + return cycledSchemaElement; } // skip processing this schema and all it's child schemas return false; @@ -526,6 +578,11 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c }); // annotate referenced element with info about origin booleanJsonSchemaElement.setMetaProperty('ref-origin', reference.uri); + // annotate fragment with info about referencing element + booleanJsonSchemaElement.setMetaProperty( + 'ref-referencing-element-id', + cloneDeep(identityManager.identify(referencingElement)) + ); return booleanJsonSchemaElement; } @@ -548,6 +605,11 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c }); // annotate fragment with info about origin mergedElement.setMetaProperty('ref-origin', reference.uri); + // annotate fragment with info about referencing element + mergedElement.setMetaProperty( + 'ref-referencing-element-id', + cloneDeep(identityManager.identify(referencingElement)) + ); // allowMetaPatches option processing if (this.allowMetaPatches) { @@ -562,13 +624,18 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c }; // attempting to create cycle - if (ancestorsLineage.includes(referencedElement)) { + if ( + ancestorsLineage.includes(referencingElement) || + ancestorsLineage.includes(referencedElement) + ) { + const replaceWith = + ancestorsLineage.findItem(wasReferencedBy(referencingElement)) ?? + mergeAndAnnotateReferencedElement(referencedElement); if (isMemberElement(parent)) { - parent.value = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign + parent.value = replaceWith; // eslint-disable-line no-param-reassign } else if (Array.isArray(parent)) { - parent[key] = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign + parent[key] = replaceWith; // eslint-disable-line no-param-reassign } - return false; } diff --git a/src/resolver/strategies/openapi-3-1-apidom/resolve.js b/src/resolver/strategies/openapi-3-1-apidom/resolve.js index 155b30701..b7575ac66 100644 --- a/src/resolver/strategies/openapi-3-1-apidom/resolve.js +++ b/src/resolver/strategies/openapi-3-1-apidom/resolve.js @@ -72,7 +72,7 @@ const resolveOpenAPI31Strategy = async (options) => { if (jsonPointer !== '') refSet.rootRef = null; // reset root reference as we want fragment to become the root reference // prepare ancestors; needed for cases where fragment is not OpenAPI element - const ancestors = [new WeakSet([fragmentElement])]; + const ancestors = [new Set([fragmentElement])]; const errors = []; const dereferenced = await dereferenceApiDOM(fragmentElement, { diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-circular-structures/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-circular-structures/dereferenced.json index a13a1ef00..d1990bca3 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-circular-structures/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-circular-structures/dereferenced.json @@ -23,7 +23,13 @@ "type": "object", "properties": { "user": { - "$ref": "https://swagger.io/schemas/user" + "$id": "https://swagger.io/schemas/user", + "type": "object", + "properties": { + "profile": { + "$ref": "https://swagger.io/schemas/user-profile" + } + } } } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-pointer-circular-structures/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-pointer-circular-structures/dereferenced.json index d5c2360f0..413ab41d6 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-pointer-circular-structures/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-pointer-circular-structures/dereferenced.json @@ -20,7 +20,12 @@ "type": "object", "properties": { "user": { - "$ref": "/home/smartbear/root.json#/components/schemas/User" + "type": "object", + "properties": { + "profile": { + "$ref": "/home/smartbear/root.json#/components/schemas/UserProfile" + } + } } } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-relative-reference-circular-structures/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-relative-reference-circular-structures/dereferenced.json index 79daa8162..8b5c9b610 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-relative-reference-circular-structures/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-relative-reference-circular-structures/dereferenced.json @@ -23,7 +23,12 @@ "type": "object", "properties": { "user": { - "$ref": "https://swagger.io/schemas/user" + "type": "object", + "$id": "https://swagger.io/schemas/user", + "properties": { + "profile": { "$ref": "https://swagger.io/schemas/user-profile" } + } + } } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-circular-structures/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-circular-structures/dereferenced.json index 5bc23ccb6..bd480d701 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-circular-structures/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-circular-structures/dereferenced.json @@ -23,7 +23,13 @@ "type": "object", "properties": { "user": { - "$ref": "urn:uuid:ff564b8a-7a87-4125-8c96-e9f123d6766f" + "$id": "urn:uuid:ff564b8a-7a87-4125-8c96-e9f123d6766f", + "type": "object", + "properties": { + "profile": { + "$ref": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" + } + } } } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-2/root.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-2/root.json new file mode 100644 index 000000000..f5b9a645c --- /dev/null +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-2/root.json @@ -0,0 +1,55 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/hello": { + "get": { + "summary": "Hello", + "operationId": "hello_hello_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Message": { + "properties": { + "message": { + "type": "string", + "title": "Message" + }, + "details": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array" + } + ], + "title": "Details" + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "Message" + } + } + } +} diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-circular-structures-2/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-circular-structures-2/dereferenced.json new file mode 100644 index 000000000..33904f978 --- /dev/null +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-circular-structures-2/dereferenced.json @@ -0,0 +1,78 @@ +[ + { + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/hello": { + "get": { + "summary": "Hello", + "operationId": "hello_hello_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "title": "Message" + }, + "details": { + "anyOf": [ + { + "items": { + "$ref": "/home/smartbear/root.json#/components/schemas/Message" + }, + "type": "array" + } + ], + "title": "Details" + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "Message" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Message": { + "properties": { + "message": { + "type": "string", + "title": "Message" + }, + "details": { + "anyOf": [ + { + "items": { + "$ref": "/home/smartbear/root.json#/components/schemas/Message" + }, + "type": "array" + } + ], + "title": "Details" + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "Message" + } + } + } + } +] diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-circular-structures-2/root.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-circular-structures-2/root.json new file mode 100644 index 000000000..f5b9a645c --- /dev/null +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-circular-structures-2/root.json @@ -0,0 +1,55 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/hello": { + "get": { + "summary": "Hello", + "operationId": "hello_hello_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Message": { + "properties": { + "message": { + "type": "string", + "title": "Message" + }, + "details": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array" + } + ], + "title": "Details" + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "Message" + } + } + } +} diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-http-circular-structures-2/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-http-circular-structures-2/dereferenced.json new file mode 100644 index 000000000..0951f14d4 --- /dev/null +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-http-circular-structures-2/dereferenced.json @@ -0,0 +1,78 @@ +[ + { + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/hello": { + "get": { + "summary": "Hello", + "operationId": "hello_hello_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "title": "Message" + }, + "details": { + "anyOf": [ + { + "items": { + "$ref": "http://localhost:8123/root.json#/components/schemas/Message" + }, + "type": "array" + } + ], + "title": "Details" + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "Message" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Message": { + "properties": { + "message": { + "type": "string", + "title": "Message" + }, + "details": { + "anyOf": [ + { + "items": { + "$ref": "http://localhost:8123/root.json#/components/schemas/Message" + }, + "type": "array" + } + ], + "title": "Details" + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "Message" + } + } + } + } +] diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-http-circular-structures-2/root.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-http-circular-structures-2/root.json new file mode 100644 index 000000000..f5b9a645c --- /dev/null +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/cycle-internal-advanced-http-circular-structures-2/root.json @@ -0,0 +1,55 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/hello": { + "get": { + "summary": "Hello", + "operationId": "hello_hello_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Message": { + "properties": { + "message": { + "type": "string", + "title": "Message" + }, + "details": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array" + } + ], + "title": "Details" + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "Message" + } + } + } +} diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js index 7474ad2b5..4d37338d9 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js @@ -1,7 +1,7 @@ import path from 'node:path'; import { toValue, toJSON } from '@swagger-api/apidom-core'; import { isSchemaElement, mediaTypes } from '@swagger-api/apidom-ns-openapi-3-1'; -import { evaluate } from '@swagger-api/apidom-json-pointer'; +import { evaluate, escape } from '@swagger-api/apidom-json-pointer'; import { parse, dereference, @@ -278,6 +278,91 @@ describe('dereference', () => { }); }); + describe('given Schema Objects with advanced internal cycles #2', () => { + test('should dereference', async () => { + const fixturePath = path.join(rootFixturePath, 'cycle-internal-advanced-2'); + const rootFilePath = path.join(fixturePath, 'root.json'); + const dereferenced = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const parent = evaluate( + `/0/paths/${escape('/hello')}/get/responses/200/content/${escape( + 'application/json' + )}/schema/properties/details/anyOf/0/items/properties/details`, + dereferenced + ); + const cyclicParent = evaluate( + `/0/paths/${escape('/hello')}/get/responses/200/content/${escape( + 'application/json' + )}/schema/properties/details/anyOf/0/items/properties/details/anyOf/0/items/properties/details`, + dereferenced + ); + + expect(parent).toStrictEqual(cyclicParent); + }); + + describe('and useCircularStructures=false', () => { + test('should avoid cycles by skipping transclusion', async () => { + const fixturePath = path.join( + rootFixturePath, + 'cycle-internal-advanced-circular-structures-2' + ); + const rootFilePath = path.join(fixturePath, 'root.json'); + const refSet = await resolve(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + refSet.refs[0].uri = '/home/smartbear/root.json'; + const actual = await dereference(refSet.refs[0].uri, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { + refSet, + strategies: [ + OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), + ], + }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); + + expect(typeof toJSON(actual)).toBe('string'); + expect(toValue(actual)).toEqual(expected); + }); + + describe('and using HTTP protocol', () => { + test('should make JSON Pointer absolute', async () => { + const fixturePath = path.join( + rootFixturePath, + 'cycle-internal-advanced-http-circular-structures-2' + ); + const dereferenceThunk = async () => { + const httpServer = globalThis.createHTTPServer({ port: 8123, cwd: fixturePath }); + + try { + return toValue( + await dereference('http://localhost:8123/root.json', { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { + strategies: [ + OpenApi3_1SwaggerClientDereferenceStrategy({ + useCircularStructures: false, + }), + ], + }, + }) + ); + } finally { + await httpServer.terminate(); + } + }; + const expected = globalThis.loadJsonFile( + path.join(fixturePath, 'dereferenced.json') + ); + + await expect(dereferenceThunk()).resolves.toEqual(expected); + }); + }); + }); + }); + describe('given Schema Objects with external cycles', () => { test('should dereference', async () => { const fixturePath = path.join(rootFixturePath, 'cycle-external'); @@ -354,7 +439,7 @@ describe('dereference', () => { describe('given Schema Object pointing externally', () => { describe('and allowMetaPatches=true', () => { - test.only('should dereference', async () => { + test('should dereference', async () => { const fixturePath = path.join(rootFixturePath, 'meta-patches-external'); const httpServer = globalThis.createHTTPServer({ port: 8123, cwd: fixturePath }); const dereferenceThunk = async () => @@ -1683,8 +1768,8 @@ describe('dereference', () => { dereference: { dereferenceOpts: { errors } }, }); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchObject({ + expect(errors).toHaveLength(2); + expect(errors.at(0)).toMatchObject({ message: expect.stringMatching( /^Could not resolve reference: Recursive Schema Object reference detected/ ), @@ -1692,6 +1777,14 @@ describe('dereference', () => { $ref: '#/components/schemas/User', fullPath: ['components', 'schemas', 'User', '$ref'], }); + expect(errors.at(1)).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Recursive Schema Object reference detected/ + ), + baseDoc: expect.stringMatching(/infinite-recursion\/root\.json$/), + $ref: '#/components/schemas/UserProfile', + fullPath: ['components', 'schemas', 'UserProfile', '$ref'], + }); }); }); @@ -1856,8 +1949,8 @@ describe('dereference', () => { dereference: { dereferenceOpts: { errors } }, }); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchObject({ + expect(errors).toHaveLength(4); + expect(errors.at(0)).toMatchObject({ message: expect.stringMatching( /^Could not resolve reference: Recursive Schema Object reference detected/ ), @@ -1865,6 +1958,14 @@ describe('dereference', () => { $ref: '#/components/schemas/User', fullPath: ['components', 'schemas', 'User', '$ref'], }); + expect(errors.at(3)).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Recursive Schema Object reference detected/ + ), + baseDoc: expect.stringMatching(/indirect-internal-circular\/root\.json$/), + $ref: '#/components/schemas/Indirection3', + fullPath: ['components', 'schemas', 'Indirection3', '$ref'], + }); }); describe('and useCircularStructures=false', () => { @@ -1891,8 +1992,8 @@ describe('dereference', () => { }, }); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchObject({ + expect(errors).toHaveLength(4); + expect(errors.at(0)).toMatchObject({ message: expect.stringMatching( /^Could not resolve reference: Recursive Schema Object reference detected/ ), @@ -1900,6 +2001,14 @@ describe('dereference', () => { $ref: '#/components/schemas/User', fullPath: ['components', 'schemas', 'User', '$ref'], }); + expect(errors.at(3)).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Recursive Schema Object reference detected/ + ), + baseDoc: expect.stringMatching(/indirect-internal-circular\/root\.json$/), + $ref: '#/components/schemas/Indirection3', + fullPath: ['components', 'schemas', 'Indirection3', '$ref'], + }); }); }); });