From 5a2141d230a6cda5b1cfddb0092799fdcd234219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Wed, 28 Feb 2024 14:25:00 +0100 Subject: [PATCH] fix(reference): add support for external cycles detection in OpenAPI 2.0 dereference strategy (#3871) Refs #3863 --- .../strategies/openapi-2/visitor.ts | 37 +++++++++++++++++-- .../fixtures/external-cycle/ex.json | 13 +++++++ .../fixtures/external-cycle/root.json | 8 ++++ .../openapi-2/json-reference-object/index.ts | 18 +++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/fixtures/external-cycle/ex.json create mode 100644 packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/fixtures/external-cycle/root.json diff --git a/packages/apidom-reference/src/dereference/strategies/openapi-2/visitor.ts b/packages/apidom-reference/src/dereference/strategies/openapi-2/visitor.ts index 4980fff5a..aeb59b076 100644 --- a/packages/apidom-reference/src/dereference/strategies/openapi-2/visitor.ts +++ b/packages/apidom-reference/src/dereference/strategies/openapi-2/visitor.ts @@ -57,13 +57,22 @@ const OpenApi2DereferenceVisitor = stampit({ reference: null, options: null, ancestors: null, + refractCache: null, }, - init({ indirections = [], reference, namespace, options, ancestors = new AncestorLineage() }) { + init({ + indirections = [], + reference, + namespace, + options, + ancestors = new AncestorLineage(), + refractCache = new Map(), + }) { this.indirections = indirections; this.namespace = namespace; this.reference = reference; this.options = options; this.ancestors = new AncestorLineage(...ancestors); + this.refractCache = refractCache; }, methods: { toBaseURI(uri: string): string { @@ -149,15 +158,20 @@ const OpenApi2DereferenceVisitor = stampit({ // applying semantics to a fragment if (isPrimitiveElement(referencedElement)) { const referencedElementType = toValue(referencingElement.meta.get('referenced-element')); + const cacheKey = `${referencedElementType}-${toValue(identityManager.identify(referencedElement))}`; - if (isReferenceLikeElement(referencedElement)) { + if (this.refractCache.has(cacheKey)) { + referencedElement = this.refractCache.get(cacheKey); + } else if (isReferenceLikeElement(referencedElement)) { // handling indirect references referencedElement = ReferenceElement.refract(referencedElement); referencedElement.setMetaProperty('referenced-element', referencedElementType); + this.refractCache.set(cacheKey, referencedElement); } else { // handling direct references const ElementClass = this.namespace.getElementClass(referencedElementType); referencedElement = ElementClass.refract(referencedElement); + this.refractCache.set(cacheKey, referencedElement); } } @@ -183,6 +197,7 @@ const OpenApi2DereferenceVisitor = stampit({ indirections: [...this.indirections], options: this.options, ancestors: ancestorsLineage, + refractCache: this.refractCache, }); referencedElement = await visitAsync(referencedElement, visitor, { keyMap, @@ -272,7 +287,14 @@ const OpenApi2DereferenceVisitor = stampit({ // applying semantics to a referenced element if (isPrimitiveElement(referencedElement)) { - referencedElement = PathItemElement.refract(referencedElement); + const cacheKey = `pathItem-${toValue(identityManager.identify(referencedElement))}`; + + if (this.refractCache.has(cacheKey)) { + referencedElement = this.refractCache.get(cacheKey); + } else { + referencedElement = PathItemElement.refract(referencedElement); + this.refractCache.set(cacheKey, referencedElement); + } } // detect direct or indirect reference @@ -297,6 +319,7 @@ const OpenApi2DereferenceVisitor = stampit({ indirections: [...this.indirections], options: this.options, ancestors: ancestorsLineage, + refractCache: this.refractCache, }); referencedElement = await visitAsync(referencedElement, visitor, { keyMap, @@ -394,15 +417,20 @@ const OpenApi2DereferenceVisitor = stampit({ // applying semantics to a fragment if (isPrimitiveElement(referencedElement)) { const referencedElementType = toValue(referencingElement.meta.get('referenced-element')); + const cacheKey = `pathItem-${toValue(identityManager.identify(referencedElement))}`; - if (isJSONReferenceLikeElement(referencedElement)) { + if (this.refractCache.has(cacheKey)) { + referencedElement = this.refractCache.get(cacheKey); + } else if (isJSONReferenceLikeElement(referencedElement)) { // handling indirect references referencedElement = ReferenceElement.refract(referencedElement); referencedElement.setMetaProperty('referenced-element', referencedElementType); + this.refractCache.set(cacheKey, referencedElement); } else { // handling direct references const ElementClass = this.namespace.getElementClass(referencedElementType); referencedElement = ElementClass.refract(referencedElement); + this.refractCache.set(cacheKey, referencedElement); } } @@ -428,6 +456,7 @@ const OpenApi2DereferenceVisitor = stampit({ indirections: [...this.indirections], options: this.options, ancestors: ancestorsLineage, + refractCache: this.refractCache, }); referencedElement = await visitAsync(referencedElement, visitor, { keyMap, diff --git a/packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/fixtures/external-cycle/ex.json b/packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/fixtures/external-cycle/ex.json new file mode 100644 index 000000000..b1bbcad76 --- /dev/null +++ b/packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/fixtures/external-cycle/ex.json @@ -0,0 +1,13 @@ +{ + "definitions": { + "externalSchema": { + "type": "object", + "description": "external schema", + "properties": { + "parent": { + "$ref": "#/definitions/externalSchema" + } + } + } + } +} diff --git a/packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/fixtures/external-cycle/root.json b/packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/fixtures/external-cycle/root.json new file mode 100644 index 000000000..e768b7b1b --- /dev/null +++ b/packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/fixtures/external-cycle/root.json @@ -0,0 +1,8 @@ +{ + "swagger": "2.0", + "definitions": { + "schema": { + "$ref": "./ex.json#/definitions/externalSchema" + } + } +} diff --git a/packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/index.ts b/packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/index.ts index a00f74e1a..97fa267ef 100644 --- a/packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/index.ts +++ b/packages/apidom-reference/test/dereference/strategies/openapi-2/json-reference-object/index.ts @@ -84,6 +84,24 @@ describe('dereference', function () { }); }); + context('given JSONReference Objects pointing to external cycles', function () { + const fixturePath = path.join(rootFixturePath, 'external-cycle'); + + specify('should dereference', async function () { + const rootFilePath = path.join(fixturePath, 'root.json'); + const dereferenced = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const parent = evaluate('/0/definitions/schema/properties', dereferenced); + const cyclicParent = evaluate( + '/0/definitions/schema/properties/parent/properties', + dereferenced, + ); + + assert.strictEqual(parent, cyclicParent); + }); + }); + context('given JSONReference Objects pointing to external indirections', function () { const fixturePath = path.join(rootFixturePath, 'external-indirections');