From 9cd15ff864718f5687f71ef6e1f4dbf61e3c855b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Wed, 28 Feb 2024 15:32:43 +0100 Subject: [PATCH] fix(reference): add support for external cycles detection in OpenAPI 3.1.0 dereference strategy (#3873) Refs #3863 --- .../strategies/openapi-3-0/visitor.ts | 9 ++++- .../strategies/openapi-3-1/visitor.ts | 38 +++++++++++++++++-- .../fixtures/external-cycle/ex.json | 8 ++++ .../fixtures/external-cycle/root.json | 10 +++++ .../openapi-3-1/reference-object/index.ts | 21 ++++++++++ 5 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/fixtures/external-cycle/ex.json create mode 100644 packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/fixtures/external-cycle/root.json diff --git a/packages/apidom-reference/src/dereference/strategies/openapi-3-0/visitor.ts b/packages/apidom-reference/src/dereference/strategies/openapi-3-0/visitor.ts index 4869490e4..d27f22a4e 100644 --- a/packages/apidom-reference/src/dereference/strategies/openapi-3-0/visitor.ts +++ b/packages/apidom-reference/src/dereference/strategies/openapi-3-0/visitor.ts @@ -418,7 +418,14 @@ const OpenApi3_0DereferenceVisitor = stampit({ operationElement = evaluate(jsonPointer, reference.value.result); // applying semantics to a referenced element if (isPrimitiveElement(operationElement)) { - operationElement = OperationElement.refract(operationElement); + const cacheKey = `operation-${toValue(identityManager.identify(operationElement))}`; + + if (this.refractCache.has(cacheKey)) { + operationElement = this.refractCache.get(cacheKey); + } else { + operationElement = OperationElement.refract(operationElement); + this.refractCache.set(cacheKey, operationElement); + } } // create shallow clone to be able to annotate with metadata operationElement = cloneShallow(operationElement); diff --git a/packages/apidom-reference/src/dereference/strategies/openapi-3-1/visitor.ts b/packages/apidom-reference/src/dereference/strategies/openapi-3-1/visitor.ts index cc9429820..52b4837e2 100644 --- a/packages/apidom-reference/src/dereference/strategies/openapi-3-1/visitor.ts +++ b/packages/apidom-reference/src/dereference/strategies/openapi-3-1/visitor.ts @@ -73,13 +73,22 @@ const OpenApi3_1DereferenceVisitor = 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 { @@ -165,15 +174,20 @@ const OpenApi3_1DereferenceVisitor = 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); } } @@ -199,6 +213,7 @@ const OpenApi3_1DereferenceVisitor = stampit({ indirections: [...this.indirections], options: this.options, ancestors: ancestorsLineage, + refractCache: this.refractCache, }); referencedElement = await visitAsync(referencedElement, visitor, { keyMap, @@ -307,7 +322,14 @@ const OpenApi3_1DereferenceVisitor = 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 @@ -332,6 +354,7 @@ const OpenApi3_1DereferenceVisitor = stampit({ indirections: [...this.indirections], options: this.options, ancestors: ancestorsLineage, + refractCache: this.refractCache, }); referencedElement = await visitAsync(referencedElement, visitor, { keyMap, @@ -425,7 +448,14 @@ const OpenApi3_1DereferenceVisitor = stampit({ operationElement = jsonPointerEvaluate(jsonPointer, reference.value.result); // applying semantics to a referenced element if (isPrimitiveElement(operationElement)) { - operationElement = OperationElement.refract(operationElement); + const cacheKey = `operation-${toValue(identityManager.identify(operationElement))}`; + + if (this.refractCache.has(cacheKey)) { + operationElement = this.refractCache.get(cacheKey); + } else { + operationElement = OperationElement.refract(operationElement); + this.refractCache.set(cacheKey, operationElement); + } } // create shallow clone to be able to annotate with metadata operationElement = cloneShallow(operationElement); diff --git a/packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/fixtures/external-cycle/ex.json b/packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/fixtures/external-cycle/ex.json new file mode 100644 index 000000000..ead8d481b --- /dev/null +++ b/packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/fixtures/external-cycle/ex.json @@ -0,0 +1,8 @@ +{ + "type": "object", + "properties": { + "parent": { + "$ref": "#" + } + } +} diff --git a/packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/fixtures/external-cycle/root.json b/packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/fixtures/external-cycle/root.json new file mode 100644 index 000000000..2bbb546b6 --- /dev/null +++ b/packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/fixtures/external-cycle/root.json @@ -0,0 +1,10 @@ +{ + "openapi": "3.1.0", + "components": { + "schemas": { + "externalSchema": { + "$ref": "./ex.json" + } + } + } +} diff --git a/packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/index.ts b/packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/index.ts index 2a6e8058b..4975db53a 100644 --- a/packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/index.ts +++ b/packages/apidom-reference/test/dereference/strategies/openapi-3-1/reference-object/index.ts @@ -90,6 +90,27 @@ describe('dereference', function () { }); }); + context('given Reference 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/components/schemas/externalSchema/properties', + dereferenced, + ); + const cyclicParent = evaluate( + '/0/components/schemas/externalSchema/properties/parent/properties', + dereferenced, + ); + + assert.strictEqual(parent, cyclicParent); + }); + }); + context('given Reference Objects pointing to external indirections', function () { const fixturePath = path.join(rootFixturePath, 'external-indirections');