Skip to content

Commit 0735471

Browse files
authored
fix(reference): add support for external cycles detection in OpenAPI 3.0.x dereference strategy (#3870)
Refs #3863
1 parent be012d7 commit 0735471

File tree

7 files changed

+105
-3
lines changed

7 files changed

+105
-3
lines changed

packages/apidom-reference/src/dereference/strategies/openapi-3-0/visitor.ts

+26-3
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,22 @@ const OpenApi3_0DereferenceVisitor = stampit({
6262
reference: null,
6363
options: null,
6464
ancestors: null,
65+
refractCache: null,
6566
},
66-
init({ indirections = [], reference, namespace, options, ancestors = new AncestorLineage() }) {
67+
init({
68+
indirections = [],
69+
reference,
70+
namespace,
71+
options,
72+
ancestors = new AncestorLineage(),
73+
refractCache = new Map(),
74+
}) {
6775
this.indirections = indirections;
6876
this.namespace = namespace;
6977
this.reference = reference;
7078
this.options = options;
7179
this.ancestors = new AncestorLineage(...ancestors);
80+
this.refractCache = refractCache;
7281
},
7382
methods: {
7483
toBaseURI(uri: string): string {
@@ -154,15 +163,20 @@ const OpenApi3_0DereferenceVisitor = stampit({
154163
// applying semantics to a fragment
155164
if (isPrimitiveElement(referencedElement)) {
156165
const referencedElementType = toValue(referencingElement.meta.get('referenced-element'));
166+
const cacheKey = `${referencedElementType}-${toValue(identityManager.identify(referencedElement))}`;
157167

158-
if (isReferenceLikeElement(referencedElement)) {
168+
if (this.refractCache.has(cacheKey)) {
169+
referencedElement = this.refractCache.get(cacheKey);
170+
} else if (isReferenceLikeElement(referencedElement)) {
159171
// handling indirect references
160172
referencedElement = ReferenceElement.refract(referencedElement);
161173
referencedElement.setMetaProperty('referenced-element', referencedElementType);
174+
this.refractCache.set(cacheKey, referencedElement);
162175
} else {
163176
// handling direct references
164177
const ElementClass = this.namespace.getElementClass(referencedElementType);
165178
referencedElement = ElementClass.refract(referencedElement);
179+
this.refractCache.set(cacheKey, referencedElement);
166180
}
167181
}
168182

@@ -188,6 +202,7 @@ const OpenApi3_0DereferenceVisitor = stampit({
188202
indirections: [...this.indirections],
189203
options: this.options,
190204
ancestors: ancestorsLineage,
205+
refractCache: this.refractCache,
191206
});
192207
referencedElement = await visitAsync(referencedElement, visitor, {
193208
keyMap,
@@ -277,7 +292,14 @@ const OpenApi3_0DereferenceVisitor = stampit({
277292

278293
// applying semantics to a referenced element
279294
if (isPrimitiveElement(referencedElement)) {
280-
referencedElement = PathItemElement.refract(referencedElement);
295+
const cacheKey = `pathItem-${toValue(identityManager.identify(referencedElement))}`;
296+
297+
if (this.refractCache.has(cacheKey)) {
298+
referencedElement = this.refractCache.get(cacheKey);
299+
} else {
300+
referencedElement = PathItemElement.refract(referencedElement);
301+
this.refractCache.set(cacheKey, referencedElement);
302+
}
281303
}
282304

283305
// detect direct or indirect reference
@@ -302,6 +324,7 @@ const OpenApi3_0DereferenceVisitor = stampit({
302324
indirections: [...this.indirections],
303325
options: this.options,
304326
ancestors: ancestorsLineage,
327+
refractCache: this.refractCache,
305328
});
306329
referencedElement = await visitAsync(referencedElement, visitor, {
307330
keyMap,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"summary": "path item summary",
3+
"description": "path item description",
4+
"get": {
5+
"callbacks": {
6+
"myCallback": {
7+
"{$request.query.queryUrl}": {
8+
"$ref": "#"
9+
}
10+
}
11+
}
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"openapi": "3.0.3",
3+
"paths": {
4+
"/path1": {
5+
"$ref": "./ex.json"
6+
}
7+
}
8+
}

packages/apidom-reference/test/dereference/strategies/openapi-3-0/path-item-object/index.ts

+19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from 'node:path';
22
import { assert } from 'chai';
33
import { toValue } from '@swagger-api/apidom-core';
44
import { mediaTypes } from '@swagger-api/apidom-ns-openapi-3-0';
5+
import { evaluate } from '@swagger-api/apidom-json-pointer';
56

67
import { loadJsonFile } from '../../../../helpers';
78
import { dereference } from '../../../../../src';
@@ -114,6 +115,24 @@ describe('dereference', function () {
114115
});
115116
});
116117

118+
context('given $ref field pointing to external cycles', function () {
119+
const fixturePath = path.join(rootFixturePath, 'external-cycle');
120+
121+
specify('should dereference', async function () {
122+
const rootFilePath = path.join(fixturePath, 'root.json');
123+
const dereferenced = await dereference(rootFilePath, {
124+
parse: { mediaType: mediaTypes.latest('json') },
125+
});
126+
const parent = evaluate('/0/paths/~1path1/get', dereferenced);
127+
const cyclicParent = evaluate(
128+
'/0/paths/~1path1/get/callbacks/myCallback/{$request.query.queryUrl}/get',
129+
dereferenced,
130+
);
131+
132+
assert.strictEqual(parent, cyclicParent);
133+
});
134+
});
135+
117136
context('given $ref field pointing to external indirections', function () {
118137
const fixturePath = path.join(rootFixturePath, 'external-indirections');
119138

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"parent": {
5+
"$ref": "#"
6+
}
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"openapi": "3.0.3",
3+
"components": {
4+
"schemas": {
5+
"externalSchema": {
6+
"$ref": "./ex.json"
7+
}
8+
}
9+
}
10+
}

packages/apidom-reference/test/dereference/strategies/openapi-3-0/reference-object/index.ts

+21
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,27 @@ describe('dereference', function () {
8484
});
8585
});
8686

87+
context('given Reference Objects pointing to external cycles', function () {
88+
const fixturePath = path.join(rootFixturePath, 'external-cycle');
89+
90+
specify('should dereference', async function () {
91+
const rootFilePath = path.join(fixturePath, 'root.json');
92+
const dereferenced = await dereference(rootFilePath, {
93+
parse: { mediaType: mediaTypes.latest('json') },
94+
});
95+
const parent = evaluate(
96+
'/0/components/schemas/externalSchema/properties',
97+
dereferenced,
98+
);
99+
const cyclicParent = evaluate(
100+
'/0/components/schemas/externalSchema/properties/parent/properties',
101+
dereferenced,
102+
);
103+
104+
assert.strictEqual(parent, cyclicParent);
105+
});
106+
});
107+
87108
context('given Reference Objects pointing to external indirections', function () {
88109
const fixturePath = path.join(rootFixturePath, 'external-indirections');
89110

0 commit comments

Comments
 (0)