Skip to content

Commit

Permalink
fix(reference): add support for external cycles detection in OpenAPI …
Browse files Browse the repository at this point in the history
…3.1.0 dereference strategy (#3873)

Refs #3863
  • Loading branch information
char0n committed Feb 28, 2024
1 parent 910a974 commit 9cd15ff
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 5 deletions.
Expand Up @@ -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);
Expand Down
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
@@ -0,0 +1,8 @@
{
"type": "object",
"properties": {
"parent": {
"$ref": "#"
}
}
}
@@ -0,0 +1,10 @@
{
"openapi": "3.1.0",
"components": {
"schemas": {
"externalSchema": {
"$ref": "./ex.json"
}
}
}
}
Expand Up @@ -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');

Expand Down

0 comments on commit 9cd15ff

Please sign in to comment.