@speclynx/apidom-reference 6.2.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +45 -0
  3. package/dist/apidom-reference.browser.js +4701 -3478
  4. package/dist/apidom-reference.browser.min.js +1 -1
  5. package/package.json +29 -28
  6. package/src/File.cjs +5 -2
  7. package/src/File.mjs +5 -2
  8. package/src/bundle/strategies/arazzo-1/index.cjs +3 -2
  9. package/src/bundle/strategies/arazzo-1/index.mjs +3 -2
  10. package/src/bundle/strategies/asyncapi-2/index.cjs +3 -2
  11. package/src/bundle/strategies/asyncapi-2/index.mjs +3 -2
  12. package/src/bundle/strategies/openapi-2/index.cjs +3 -2
  13. package/src/bundle/strategies/openapi-2/index.mjs +3 -2
  14. package/src/bundle/strategies/openapi-3-0/index.cjs +3 -2
  15. package/src/bundle/strategies/openapi-3-0/index.mjs +3 -2
  16. package/src/bundle/strategies/openapi-3-1/index.cjs +3 -2
  17. package/src/bundle/strategies/openapi-3-1/index.mjs +3 -2
  18. package/src/dereference/strategies/DereferenceVisitor.cjs +588 -0
  19. package/src/dereference/strategies/DereferenceVisitor.mjs +580 -0
  20. package/src/dereference/strategies/arazzo-1/index.cjs +14 -39
  21. package/src/dereference/strategies/arazzo-1/index.mjs +15 -40
  22. package/src/dereference/strategies/arazzo-1/visitor.cjs +207 -331
  23. package/src/dereference/strategies/arazzo-1/visitor.mjs +215 -338
  24. package/src/dereference/strategies/asyncapi-2/index.cjs +11 -39
  25. package/src/dereference/strategies/asyncapi-2/index.mjs +11 -39
  26. package/src/dereference/strategies/asyncapi-2/visitor.cjs +133 -425
  27. package/src/dereference/strategies/asyncapi-2/visitor.mjs +137 -429
  28. package/src/dereference/strategies/openapi-2/index.cjs +12 -42
  29. package/src/dereference/strategies/openapi-2/index.mjs +12 -42
  30. package/src/dereference/strategies/openapi-2/visitor.cjs +168 -532
  31. package/src/dereference/strategies/openapi-2/visitor.mjs +172 -536
  32. package/src/dereference/strategies/openapi-3-0/index.cjs +12 -42
  33. package/src/dereference/strategies/openapi-3-0/index.mjs +12 -42
  34. package/src/dereference/strategies/openapi-3-0/visitor.cjs +194 -468
  35. package/src/dereference/strategies/openapi-3-0/visitor.mjs +199 -473
  36. package/src/dereference/strategies/openapi-3-1/index.cjs +16 -42
  37. package/src/dereference/strategies/openapi-3-1/index.mjs +16 -42
  38. package/src/dereference/strategies/openapi-3-1/visitor.cjs +350 -602
  39. package/src/dereference/strategies/openapi-3-1/visitor.mjs +357 -608
  40. package/src/dereference/strategies/overlay-1/index.cjs +3 -2
  41. package/src/dereference/strategies/overlay-1/index.mjs +3 -2
  42. package/src/index.cjs +0 -7
  43. package/src/index.mjs +0 -1
  44. package/src/parse/index.cjs +9 -1
  45. package/src/parse/index.mjs +9 -1
  46. package/src/parse/parsers/apidom-json/index.cjs +2 -1
  47. package/src/parse/parsers/apidom-json/index.mjs +2 -1
  48. package/src/parse/parsers/arazzo-json-1/index.cjs +2 -1
  49. package/src/parse/parsers/arazzo-json-1/index.mjs +2 -1
  50. package/src/parse/parsers/arazzo-yaml-1/index.cjs +2 -1
  51. package/src/parse/parsers/arazzo-yaml-1/index.mjs +2 -1
  52. package/src/parse/parsers/asyncapi-json-2/index.cjs +2 -1
  53. package/src/parse/parsers/asyncapi-json-2/index.mjs +2 -1
  54. package/src/parse/parsers/asyncapi-yaml-2/index.cjs +2 -1
  55. package/src/parse/parsers/asyncapi-yaml-2/index.mjs +2 -1
  56. package/src/parse/parsers/json/index.cjs +2 -1
  57. package/src/parse/parsers/json/index.mjs +2 -1
  58. package/src/parse/parsers/openapi-json-2/index.cjs +2 -1
  59. package/src/parse/parsers/openapi-json-2/index.mjs +2 -1
  60. package/src/parse/parsers/openapi-json-3-0/index.cjs +2 -1
  61. package/src/parse/parsers/openapi-json-3-0/index.mjs +2 -1
  62. package/src/parse/parsers/openapi-json-3-1/index.cjs +2 -1
  63. package/src/parse/parsers/openapi-json-3-1/index.mjs +2 -1
  64. package/src/parse/parsers/openapi-yaml-2/index.cjs +2 -1
  65. package/src/parse/parsers/openapi-yaml-2/index.mjs +2 -1
  66. package/src/parse/parsers/openapi-yaml-3-0/index.cjs +2 -1
  67. package/src/parse/parsers/openapi-yaml-3-0/index.mjs +2 -1
  68. package/src/parse/parsers/openapi-yaml-3-1/index.cjs +2 -1
  69. package/src/parse/parsers/openapi-yaml-3-1/index.mjs +2 -1
  70. package/src/parse/parsers/overlay-json-1/index.cjs +2 -1
  71. package/src/parse/parsers/overlay-json-1/index.mjs +2 -1
  72. package/src/parse/parsers/overlay-yaml-1/index.cjs +2 -1
  73. package/src/parse/parsers/overlay-yaml-1/index.mjs +2 -1
  74. package/src/parse/parsers/yaml-1-2/index.cjs +2 -1
  75. package/src/parse/parsers/yaml-1-2/index.mjs +2 -1
  76. package/types/apidom-reference.d.ts +0 -9
  77. package/types/dereference/strategies/DereferenceVisitor.d.ts +224 -0
  78. package/types/dereference/strategies/arazzo-1/index.d.ts +0 -1
  79. package/types/dereference/strategies/arazzo-1/visitor.d.ts +38 -38
  80. package/types/dereference/strategies/asyncapi-2/index.d.ts +0 -1
  81. package/types/dereference/strategies/asyncapi-2/visitor.d.ts +13 -29
  82. package/types/dereference/strategies/openapi-2/index.d.ts +0 -1
  83. package/types/dereference/strategies/openapi-2/visitor.d.ts +14 -29
  84. package/types/dereference/strategies/openapi-3-0/index.d.ts +0 -1
  85. package/types/dereference/strategies/openapi-3-0/visitor.d.ts +13 -28
  86. package/types/dereference/strategies/openapi-3-1/index.d.ts +0 -1
  87. package/types/dereference/strategies/openapi-3-1/visitor.d.ts +37 -33
  88. package/types/index.d.ts +0 -1
  89. package/src/dereference/util.cjs +0 -33
  90. package/src/dereference/util.mjs +0 -27
  91. package/types/dereference/util.d.ts +0 -9
@@ -1,363 +1,147 @@
1
- import { propEq } from 'ramda';
2
- import { RefElement, isStringElement, isElement, cloneShallow, cloneDeep } from '@speclynx/apidom-datamodel';
3
- import { toValue, toYAML } from '@speclynx/apidom-core';
1
+ import { isStringElement, cloneShallow } from '@speclynx/apidom-datamodel';
2
+ import { toValue } from '@speclynx/apidom-core';
4
3
  import { ApiDOMStructuredError } from '@speclynx/apidom-error';
5
- import { traverse, traverseAsync } from '@speclynx/apidom-traverse';
6
- import { evaluate, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
7
- import { isReferenceElement, isJSONReferenceElement, isPathItemElement, isReferenceLikeElement, isJSONReferenceLikeElement, refract, refractReference, refractPathItem, refractJSONReference } from '@speclynx/apidom-ns-openapi-2';
8
- import UnresolvableReferenceError from "../../../errors/UnresolvableReferenceError.mjs";
9
- import MaximumDereferenceDepthError from "../../../errors/MaximumDereferenceDepthError.mjs";
10
- import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
11
- import { AncestorLineage } from "../../util.mjs";
4
+ import { evaluate as jsonPointerEvaluate, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
5
+ import { isReferenceLikeElement, isJSONReferenceLikeElement, isPathItemElement, isReferenceElement, isJSONReferenceElement, refract, refractReference, refractJSONReference } from '@speclynx/apidom-ns-openapi-2';
6
+ import DereferenceVisitor from "../DereferenceVisitor.mjs";
12
7
  import * as url from "../../../util/url.mjs";
13
- import parse from "../../../parse/index.mjs";
14
- import Reference from "../../../Reference.mjs";
15
8
  /**
16
9
  * @public
17
10
  */
18
11
  /**
12
+ * Dereferences an OpenAPI 2.0 document. Reference Objects and JSON Reference Objects expect
13
+ * the type of their `referenced-element` meta, Path Item `$ref`s a Path Item Object.
14
+ *
19
15
  * @public
20
16
  */
21
- class OpenAPI2DereferenceVisitor {
22
- indirections;
23
- reference;
24
- options;
25
- refractCache;
26
- visited;
27
-
28
- /**
29
- * Tracks element ancestors across dive-deep traversal boundaries.
30
- * Used for cycle detection: if a referenced element is found in
31
- * the ancestor lineage, a circular reference is detected.
32
- */
33
- ancestors;
34
- constructor({
35
- reference,
36
- options,
37
- indirections = [],
38
- ancestors = new AncestorLineage(),
39
- refractCache = new WeakMap(),
40
- visited = new WeakSet()
41
- }) {
42
- this.indirections = indirections;
43
- this.reference = reference;
44
- this.options = options;
45
- this.refractCache = refractCache;
46
- this.ancestors = new AncestorLineage(...ancestors);
47
- this.visited = visited;
48
- }
49
- toAncestorLineage(path) {
50
- const ancestorNodes = path.getAncestorNodes();
51
- const directAncestors = new Set(ancestorNodes.filter(isElement));
52
- const ancestorsLineage = new AncestorLineage(...this.ancestors, directAncestors);
53
- return [ancestorsLineage, directAncestors];
17
+ class OpenAPI2DereferenceVisitor extends DereferenceVisitor {
18
+ get strategyName() {
19
+ return 'openapi-2';
54
20
  }
55
- toBaseURI(uri) {
56
- return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
57
- }
58
- async toReference(uri) {
59
- // detect maximum depth of resolution
60
- if (this.reference.depth >= this.options.resolve.maxDepth) {
61
- throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`, {
62
- maxDepth: this.options.resolve.maxDepth,
63
- uri: this.reference.uri
64
- });
65
- }
66
- const baseURI = this.toBaseURI(uri);
67
- const {
68
- refSet
69
- } = this.reference;
70
-
71
- // we've already processed this Reference in past
72
- if (refSet.has(baseURI)) {
73
- return refSet.find(propEq(baseURI, 'uri'));
74
- }
75
- const parseResult = await parse(url.unsanitize(baseURI), {
76
- ...this.options,
77
- parse: {
78
- ...this.options.parse,
79
- mediaType: 'text/plain'
80
- }
81
- });
82
-
83
- // register new mutable reference with a refSet
84
- //
85
- // NOTE(known limitation): the mutable reference is mutated in place during traversal
86
- // (via `{ mutable: true }`). When an external document evaluates a JSON pointer back
87
- // into this document, it may receive an already-resolved element instead of the original
88
- // $ref. That resolved element was produced using the entry document's resolution context
89
- // (ancestors, indirections), which may differ from the external document's context.
90
- // This can affect cycle detection in rare cross-document circular reference patterns.
91
- //
92
- // Remediation: evaluate JSON pointers against the immutable (original) parse tree
93
- // instead of the mutable working copy. The `immutable://` reference below preserves
94
- // the original tree and could be used for pointer evaluation, ensuring every resolution
95
- // context always sees raw, unresolved elements and processes them with its own
96
- // ancestors/indirections. The trade-off is that elements referenced by multiple
97
- // documents would be resolved once per context instead of being reused.
98
- const mutableReference = new Reference({
99
- uri: baseURI,
100
- value: this.options.dereference.immutable ? cloneDeep(parseResult) : parseResult,
101
- depth: this.reference.depth + 1
21
+ createVisitor(reference) {
22
+ return new OpenAPI2DereferenceVisitor({
23
+ reference,
24
+ options: this.options
102
25
  });
103
- refSet.add(mutableReference);
104
- if (this.options.dereference.immutable) {
105
- // register new immutable reference with a refSet
106
- const immutableReference = new Reference({
107
- uri: `immutable://${baseURI}`,
108
- value: parseResult,
109
- depth: this.reference.depth + 1
110
- });
111
- refSet.add(immutableReference);
112
- }
113
- return mutableReference;
114
26
  }
115
27
 
116
28
  /**
117
- * Handles an error according to the continueOnError option.
118
- *
119
- * For new errors: wraps in UnresolvableReferenceError with structured context
120
- * (type, uri, location, codeFrame, refFieldName, refFieldValue, trace).
121
- * For errors already wrapped by a nested visitor: prepends the current hop to the trace.
122
- *
123
- * Inner/intermediate visitors always throw to let the trace accumulate.
124
- * Only the entry document visitor respects continueOnError (callback/swallow/throw).
29
+ * A (JSON) Reference Object stands for the type its reference expects: it's dereferenced as that type.
125
30
  */
126
- handleError(message, error, referencingElement, refFieldName, refFieldValue, visitorPath) {
127
- const {
128
- continueOnError
129
- } = this.options.dereference;
130
- const isEntryDocument = url.stripHash(this.reference.refSet?.rootRef?.uri ?? '') === this.reference.uri;
131
- const uri = this.reference.uri;
132
- const type = referencingElement.element;
133
- const codeFrame = toYAML(referencingElement);
31
+ isOfType(element, type) {
32
+ return isReferenceElement(element) || isJSONReferenceElement(element) || element.element === type;
33
+ }
134
34
 
135
- // find element location by identity in the document tree.
136
- // guarded: this.reference.value may not be a ParseResultElement or may lack a result.
137
- // falls back to visitorPath which may produce an incomplete path when
138
- // dereferenceApiDOM is called with a fragment (cloneShallow creates a new root identity).
139
- let location;
140
- const root = this.reference.value.result;
141
- if (isElement(root)) {
142
- traverse(root, {
143
- enter: p => {
144
- if (p.node === referencingElement || this.refractCache.get(p.node) === referencingElement) {
145
- location = p.formatPath();
146
- p.stop();
147
- }
148
- }
149
- });
35
+ /**
36
+ * A generic structure holding a `$ref` is a reference to the expected type: a JSON Reference
37
+ * Object for a Schema Object, a Reference Object otherwise, unless the type has its own `$ref`
38
+ * field (Path Item Object).
39
+ */
40
+ refractTo(element, type) {
41
+ if (type === 'schema' && isJSONReferenceLikeElement(element)) {
42
+ const referenceElement = refractJSONReference(element);
43
+ referenceElement.meta.set('referenced-element', type);
44
+ return referenceElement;
150
45
  }
151
- location ??= visitorPath.formatPath();
152
- const hop = {
153
- uri,
154
- type,
155
- refFieldName,
156
- refFieldValue,
157
- location,
158
- codeFrame
159
- };
160
-
161
- // enrich existing error from nested visitor or create new one
162
- let unresolvedError;
163
- if (error instanceof UnresolvableReferenceError) {
164
- // prefix relative locations for entries belonging to the referenced document
165
- const refBaseURI = this.toBaseURI(refFieldValue);
166
- const fragment = URIFragmentIdentifier.fromURIReference(refFieldValue);
167
- if (fragment) {
168
- if (refBaseURI === error.uri && error.location) {
169
- error.location = fragment + error.location;
170
- }
171
- for (const h of error.trace) {
172
- if (h.uri === refBaseURI && h.location) h.location = fragment + h.location;
173
- }
174
- }
175
- // @ts-ignore
176
- error.trace = [hop, ...error.trace];
177
- unresolvedError = error;
178
- } else {
179
- unresolvedError = new UnresolvableReferenceError(message, {
180
- cause: error,
181
- type,
182
- uri,
183
- location,
184
- codeFrame,
185
- refFieldName,
186
- refFieldValue,
187
- trace: []
188
- });
46
+ if (type !== 'schema' && type !== 'pathItem' && isReferenceLikeElement(element)) {
47
+ const referenceElement = refractReference(element);
48
+ referenceElement.meta.set('referenced-element', type);
49
+ return referenceElement;
189
50
  }
190
- if (!isEntryDocument || continueOnError === false) throw unresolvedError;
191
- if (typeof continueOnError === 'function') continueOnError(unresolvedError);
51
+ return refract(element, {
52
+ element: type
53
+ });
192
54
  }
193
55
  async ReferenceElement(path) {
194
56
  const referencingElement = path.node;
195
57
 
196
- // ignore Reference Object without a string $ref, e.g. an empty YAML value
197
- if (!isStringElement(referencingElement.$ref)) {
198
- return;
199
- }
58
+ // the fields of a Reference Object hold no references
59
+ path.skip();
200
60
 
201
- // skip current referencing element as it's already been access
202
- if (this.indirections.includes(referencingElement)) {
203
- path.skip();
61
+ // ignore Reference Object without a string $ref, e.g. an empty YAML value
62
+ if (this.isTranscluded(referencingElement) || !isStringElement(referencingElement.$ref)) {
204
63
  return;
205
64
  }
206
- const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref));
65
+ const $ref = toValue(referencingElement.$ref);
66
+ const retrievalURI = this.toBaseURI($ref);
207
67
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
208
68
  const isExternalReference = !isInternalReference;
209
69
 
210
70
  // ignore resolving internal Reference Objects
211
71
  if (!this.options.resolve.internal && isInternalReference) {
212
- // skip traversing this reference element and all it's child elements
213
- path.skip();
214
72
  return;
215
73
  }
216
74
  // ignore resolving external Reference Objects
217
75
  if (!this.options.resolve.external && isExternalReference) {
218
- // skip traversing this reference element and all it's child elements
219
- path.skip();
220
76
  return;
221
77
  }
222
- const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref));
223
- const indirectionsSize = this.indirections.length;
78
+ const $refBaseURI = url.resolve(retrievalURI, $ref);
224
79
  try {
225
- const reference = await this.toReference(toValue(referencingElement.$ref));
226
- this.indirections.push(referencingElement);
80
+ const reference = await this.toReference($ref);
227
81
  const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
228
-
229
82
  // possibly non-semantic fragment
230
- let referencedElement = evaluate(reference.value.result, jsonPointer);
83
+ const referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
231
84
 
232
- // applying semantics to a fragment
233
- const referencedElementType = referencingElement.meta.get('referenced-element');
234
- if (referencedElement.element !== referencedElementType && !isReferenceElement(referencedElement)) {
235
- if (this.refractCache.has(referencedElement)) {
236
- referencedElement = this.refractCache.get(referencedElement);
237
- } else if (isReferenceLikeElement(referencedElement)) {
238
- // handling generic indirect references
239
- const sourceElement = referencedElement;
240
- referencedElement = refractReference(referencedElement);
241
- referencedElement.meta.set('referenced-element', referencedElementType);
242
- this.refractCache.set(sourceElement, referencedElement);
243
- } else {
244
- // handling direct references
245
- const sourceElement = referencedElement;
246
- referencedElement = refract(referencedElement, {
247
- element: referencedElementType
248
- });
249
- this.refractCache.set(sourceElement, referencedElement);
250
- }
251
- }
252
-
253
- // detect direct or indirect reference
85
+ // detect direct reference
254
86
  if (referencingElement === referencedElement) {
255
87
  throw new ApiDOMStructuredError('Recursive Reference Object detected', {
256
- $ref: toValue(referencingElement.$ref)
88
+ $ref
257
89
  });
258
90
  }
259
91
 
260
- // detect maximum depth of dereferencing
261
- if (this.indirections.length > this.options.dereference.maxDepth) {
262
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
263
- maxDepth: this.options.dereference.maxDepth,
264
- uri: this.reference.uri
265
- });
266
- }
267
-
268
- // detect cross-boundary cycle
269
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
270
- if (ancestorsLineage.includes(referencedElement)) {
271
- reference.refSet.circular = true;
272
- if (this.options.dereference.circular === 'error') {
273
- throw new ApiDOMStructuredError('Circular reference detected', {
274
- $ref: toValue(referencingElement.$ref)
92
+ // a reference without an expected type reads its target as its own type
93
+ const type = toValue(referencingElement.meta.get('referenced-element')) ?? referencedElement.element;
94
+ await this.addTransclusion(path, {
95
+ source: referencedElement,
96
+ type,
97
+ reference,
98
+ context: undefined
99
+ }, {
100
+ toTranscludedElement: (element, target) => {
101
+ // a chain of Reference Objects leading back to itself never reaches a value
102
+ if (isReferenceElement(target)) return element;
103
+
104
+ /**
105
+ * Creating a new version of referenced element to avoid modifying the original one.
106
+ */
107
+ const mergedElement = cloneShallow(target);
108
+ // annotate referenced element with info about original referencing element
109
+ mergedElement.meta.set('ref-fields', {
110
+ $ref
275
111
  });
276
- } else if (this.options.dereference.circular === 'replace') {
277
- const refElement = new RefElement($refBaseURI, {
278
- type: referencingElement.element,
112
+ // annotate fragment with info about origin
113
+ mergedElement.meta.set('ref-origin', reference.uri);
114
+ mergedElement.meta.set('ref-type', element.element);
115
+ return mergedElement;
116
+ },
117
+ circular: {
118
+ uri: $refBaseURI,
119
+ meta: {
279
120
  uri: reference.uri,
280
- $ref: toValue(referencingElement.$ref)
281
- });
282
- const replacer = this.options.dereference.strategyOpts['openapi-2']?.circularReplacer ?? this.options.dereference.circularReplacer;
283
- const replacement = replacer(refElement);
284
- path.replaceWith(replacement);
285
- path.skip();
286
- return;
287
- }
288
- }
289
-
290
- /**
291
- * Dive deep into the fragment.
292
- *
293
- * Cases to consider:
294
- * 1. We're crossing document boundary
295
- * 2. Fragment is from non-entry document
296
- * 3. Fragment is a Reference Object. We need to follow it to get the eventual value
297
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
298
- */
299
- const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
300
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
301
- if ((isExternalReference || isNonEntryDocument || isReferenceElement(referencedElement) || shouldDetectCircular && !this.visited.has(referencedElement)) && !ancestorsLineage.includesCycle(referencedElement)) {
302
- if (shouldDetectCircular) {
303
- this.visited.add(referencedElement);
121
+ $ref
122
+ },
123
+ details: {
124
+ $ref
125
+ }
304
126
  }
305
-
306
- // append referencing reference to ancestors lineage
307
- directAncestors.add(referencingElement);
308
- const visitor = new OpenAPI2DereferenceVisitor({
309
- reference,
310
- indirections: [...this.indirections],
311
- options: this.options,
312
- refractCache: this.refractCache,
313
- visited: this.visited,
314
- ancestors: ancestorsLineage
315
- });
316
- referencedElement = await traverseAsync(referencedElement, visitor, {
317
- mutable: true,
318
- skipVisited: 'skip'
319
- });
320
- directAncestors.delete(referencingElement);
321
- }
322
-
323
- /**
324
- * Creating a new version of referenced element to avoid modifying the original one.
325
- */
326
- const mergedElement = cloneShallow(referencedElement);
327
- // annotate referenced element with info about original referencing element
328
- mergedElement.meta.set('ref-fields', {
329
- $ref: toValue(referencingElement.$ref)
330
127
  });
331
- // annotate fragment with info about origin
332
- mergedElement.meta.set('ref-origin', reference.uri);
333
- mergedElement.meta.set('ref-type', referencingElement.element);
334
-
335
- /**
336
- * Transclude referencing element with merged referenced element.
337
- */
338
- path.replaceWith(mergedElement);
339
- path.skip();
340
128
  } catch (error) {
341
- const $ref = toValue(referencingElement.$ref);
342
129
  this.handleError(`Error while dereferencing Reference Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
343
- } finally {
344
- if (this.indirections.length > indirectionsSize) this.indirections.pop();
345
130
  }
346
131
  }
347
132
  async PathItemElement(path) {
348
133
  const referencingElement = path.node;
349
-
350
- // ignore PathItemElement without $ref field
351
- if (!isStringElement(referencingElement.$ref)) {
134
+ if (this.isTranscluded(referencingElement)) {
135
+ path.skip();
352
136
  return;
353
137
  }
354
138
 
355
- // skip current referencing element as it's already been access
356
- if (this.indirections.includes(referencingElement)) {
357
- path.skip();
139
+ // ignore PathItemElement without $ref field
140
+ if (!isStringElement(referencingElement.$ref)) {
358
141
  return;
359
142
  }
360
- const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref));
143
+ const $ref = toValue(referencingElement.$ref);
144
+ const retrievalURI = this.toBaseURI($ref);
361
145
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
362
146
  const isExternalReference = !isInternalReference;
363
147
 
@@ -371,287 +155,139 @@ class OpenAPI2DereferenceVisitor {
371
155
  // skip traversing this Path Item element but traverse all it's child elements
372
156
  return;
373
157
  }
374
- const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref));
375
- const indirectionsSize = this.indirections.length;
158
+ const $refBaseURI = url.resolve(retrievalURI, $ref);
376
159
  try {
377
- const reference = await this.toReference(toValue(referencingElement.$ref));
378
- this.indirections.push(referencingElement);
160
+ const reference = await this.toReference($ref);
379
161
  const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
380
-
381
162
  // possibly non-semantic referenced element
382
- let referencedElement = evaluate(reference.value.result, jsonPointer);
383
-
384
- // applying semantics to a referenced element
385
- if (!isPathItemElement(referencedElement)) {
386
- if (this.refractCache.has(referencedElement)) {
387
- referencedElement = this.refractCache.get(referencedElement);
388
- } else {
389
- const sourceElement = referencedElement;
390
- referencedElement = refractPathItem(referencedElement);
391
- this.refractCache.set(sourceElement, referencedElement);
392
- }
393
- }
163
+ const referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
394
164
 
395
- // detect direct or indirect reference
165
+ // detect direct reference
396
166
  if (referencingElement === referencedElement) {
397
167
  throw new ApiDOMStructuredError('Recursive Path Item Object reference detected', {
398
- $ref: toValue(referencingElement.$ref)
399
- });
400
- }
401
-
402
- // detect maximum depth of dereferencing
403
- if (this.indirections.length > this.options.dereference.maxDepth) {
404
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
405
- maxDepth: this.options.dereference.maxDepth,
406
- uri: this.reference.uri
168
+ $ref
407
169
  });
408
170
  }
171
+ await this.addTransclusion(path, {
172
+ source: referencedElement,
173
+ type: 'pathItem',
174
+ reference,
175
+ context: undefined
176
+ }, {
177
+ toTranscludedElement: (element, target) => {
178
+ if (!isPathItemElement(target)) return target;
179
+
180
+ /**
181
+ * Creating a new version of Path Item by merging fields from referenced Path Item with referencing one.
182
+ */
183
+ const mergedElement = cloneShallow(target);
184
+ // existing keywords from referencing PathItemElement overrides ones from referenced element
185
+ element.forEach((value, keyElement, item) => {
186
+ mergedElement.remove(toValue(keyElement));
187
+ mergedElement.content.push(item);
188
+ });
189
+ mergedElement.remove('$ref');
409
190
 
410
- // detect cross-boundary cycle
411
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
412
- if (ancestorsLineage.includes(referencedElement)) {
413
- reference.refSet.circular = true;
414
- if (this.options.dereference.circular === 'error') {
415
- throw new ApiDOMStructuredError('Circular reference detected', {
416
- $ref: toValue(referencingElement.$ref)
191
+ // annotate referenced element with info about original referencing element
192
+ mergedElement.meta.set('ref-fields', {
193
+ $ref
417
194
  });
418
- } else if (this.options.dereference.circular === 'replace') {
419
- const refElement = new RefElement($refBaseURI, {
420
- type: referencingElement.element,
195
+ // annotate referenced element with info about origin and type
196
+ mergedElement.meta.set('ref-origin', reference.uri);
197
+ mergedElement.meta.set('ref-type', element.element);
198
+ return mergedElement;
199
+ },
200
+ aliasField: '$ref',
201
+ circular: {
202
+ uri: $refBaseURI,
203
+ meta: {
421
204
  uri: reference.uri,
422
- $ref: toValue(referencingElement.$ref)
423
- });
424
- const replacer = this.options.dereference.strategyOpts['openapi-2']?.circularReplacer ?? this.options.dereference.circularReplacer;
425
- const replacement = replacer(refElement);
426
- path.replaceWith(replacement);
427
- path.skip();
428
- return;
429
- }
430
- }
431
-
432
- /**
433
- * Dive deep into the fragment.
434
- *
435
- * Cases to consider:
436
- * 1. We're crossing document boundary
437
- * 2. Fragment is from non-entry document
438
- * 3. Fragment is a Path Item Object with $ref field. We need to follow it to get the eventual value
439
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
440
- */
441
- const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
442
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
443
- if ((isExternalReference || isNonEntryDocument || isPathItemElement(referencedElement) && isStringElement(referencedElement.$ref) || shouldDetectCircular && !this.visited.has(referencedElement)) && !ancestorsLineage.includesCycle(referencedElement)) {
444
- if (shouldDetectCircular) {
445
- this.visited.add(referencedElement);
205
+ $ref
206
+ },
207
+ details: {
208
+ $ref
209
+ }
446
210
  }
447
-
448
- // append referencing reference to ancestors lineage
449
- directAncestors.add(referencingElement);
450
- const visitor = new OpenAPI2DereferenceVisitor({
451
- reference,
452
- indirections: [...this.indirections],
453
- options: this.options,
454
- refractCache: this.refractCache,
455
- visited: this.visited,
456
- ancestors: ancestorsLineage
457
- });
458
- referencedElement = await traverseAsync(referencedElement, visitor, {
459
- mutable: true,
460
- skipVisited: 'skip'
461
- });
462
-
463
- // remove referencing reference from ancestors lineage
464
- directAncestors.delete(referencingElement);
465
- }
466
-
467
- /**
468
- * Creating a new version of Path Item by merging fields from referenced Path Item with referencing one.
469
- */
470
- if (isPathItemElement(referencedElement)) {
471
- const mergedElement = cloneShallow(referencedElement);
472
- // existing keywords from referencing PathItemElement overrides ones from referenced element
473
- referencingElement.forEach((value, keyElement, item) => {
474
- mergedElement.remove(toValue(keyElement));
475
- mergedElement.content.push(item);
476
- });
477
- mergedElement.remove('$ref');
478
-
479
- // annotate referenced element with info about original referencing element
480
- mergedElement.meta.set('ref-fields', {
481
- $ref: toValue(referencingElement.$ref)
482
- });
483
- // annotate referenced element with info about origin and type
484
- mergedElement.meta.set('ref-origin', reference.uri);
485
- mergedElement.meta.set('ref-type', referencingElement.element);
486
- referencedElement = mergedElement;
487
- }
488
-
489
- /**
490
- * Transclude referencing element with merged referenced element.
491
- */
492
- path.replaceWith(referencedElement);
493
- return;
211
+ });
494
212
  } catch (error) {
495
- const $ref = toValue(referencingElement.$ref);
496
213
  this.handleError(`Error while dereferencing Path Item Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
497
- } finally {
498
- if (this.indirections.length > indirectionsSize) this.indirections.pop();
499
214
  }
500
215
  }
501
216
  async JSONReferenceElement(path) {
502
217
  const referencingElement = path.node;
503
218
 
504
- // ignore Reference Object without a string $ref, e.g. an empty YAML value
505
- if (!isStringElement(referencingElement.$ref)) {
506
- return;
507
- }
219
+ // the fields of a JSON Reference Object hold no references
220
+ path.skip();
508
221
 
509
- // skip current referencing element as it's already been access
510
- if (this.indirections.includes(referencingElement)) {
511
- path.skip();
222
+ // ignore JSON Reference Object without a string $ref, e.g. an empty YAML value
223
+ if (this.isTranscluded(referencingElement) || !isStringElement(referencingElement.$ref)) {
512
224
  return;
513
225
  }
514
- const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref));
226
+ const $ref = toValue(referencingElement.$ref);
227
+ const retrievalURI = this.toBaseURI($ref);
515
228
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
516
229
  const isExternalReference = !isInternalReference;
517
230
 
518
- // ignore resolving internal JSONReference Objects
231
+ // ignore resolving internal JSON Reference Objects
519
232
  if (!this.options.resolve.internal && isInternalReference) {
520
- // skip traversing this JSONReference element and all it's child elements
521
- path.skip();
522
233
  return;
523
234
  }
524
- // ignore resolving external JSONReference Objects
235
+ // ignore resolving external JSON Reference Objects
525
236
  if (!this.options.resolve.external && isExternalReference) {
526
- // skip traversing this JSONReference element and all it's child elements
527
- path.skip();
528
237
  return;
529
238
  }
530
- const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref));
531
- const indirectionsSize = this.indirections.length;
239
+ const $refBaseURI = url.resolve(retrievalURI, $ref);
532
240
  try {
533
- const reference = await this.toReference(toValue(referencingElement.$ref));
534
- this.indirections.push(referencingElement);
241
+ const reference = await this.toReference($ref);
535
242
  const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
536
-
537
243
  // possibly non-semantic fragment
538
- let referencedElement = evaluate(reference.value.result, jsonPointer);
244
+ const referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
539
245
 
540
- // applying semantics to a fragment
541
- const referencedElementType = referencingElement.meta.get('referenced-element');
542
- if (referencedElement.element !== referencedElementType && !isJSONReferenceElement(referencedElement)) {
543
- if (this.refractCache.has(referencedElement)) {
544
- referencedElement = this.refractCache.get(referencedElement);
545
- } else if (isJSONReferenceLikeElement(referencedElement)) {
546
- // handling generic indirect references
547
- const sourceElement = referencedElement;
548
- referencedElement = refractJSONReference(referencedElement);
549
- referencedElement.meta.set('referenced-element', referencedElementType);
550
- this.refractCache.set(sourceElement, referencedElement);
551
- } else {
552
- // handling direct references
553
- const sourceElement = referencedElement;
554
- referencedElement = refract(referencedElement, {
555
- element: referencedElementType
556
- });
557
- this.refractCache.set(sourceElement, referencedElement);
558
- }
559
- }
560
-
561
- // detect direct or indirect reference
246
+ // detect direct reference
562
247
  if (referencingElement === referencedElement) {
563
248
  throw new ApiDOMStructuredError('Recursive JSON Reference Object detected', {
564
- $ref: toValue(referencingElement.$ref)
565
- });
566
- }
567
-
568
- // detect maximum depth of dereferencing
569
- if (this.indirections.length > this.options.dereference.maxDepth) {
570
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
571
- maxDepth: this.options.dereference.maxDepth,
572
- uri: this.reference.uri
249
+ $ref
573
250
  });
574
251
  }
575
252
 
576
- // detect cross-boundary cycle
577
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
578
- if (ancestorsLineage.includes(referencedElement)) {
579
- reference.refSet.circular = true;
580
- if (this.options.dereference.circular === 'error') {
581
- throw new ApiDOMStructuredError('Circular reference detected', {
582
- $ref: toValue(referencingElement.$ref)
253
+ // a reference without an expected type reads its target as its own type
254
+ const type = toValue(referencingElement.meta.get('referenced-element')) ?? referencedElement.element;
255
+ await this.addTransclusion(path, {
256
+ source: referencedElement,
257
+ type,
258
+ reference,
259
+ context: undefined
260
+ }, {
261
+ toTranscludedElement: (element, target) => {
262
+ // a chain of JSON Reference Objects leading back to itself never reaches a value
263
+ if (isJSONReferenceElement(target)) return element;
264
+
265
+ /**
266
+ * Creating a new version of referenced element to avoid modifying the original one.
267
+ */
268
+ const mergedElement = cloneShallow(target);
269
+ // annotate referenced element with info about original referencing element
270
+ mergedElement.meta.set('ref-fields', {
271
+ $ref
583
272
  });
584
- } else if (this.options.dereference.circular === 'replace') {
585
- const refElement = new RefElement($refBaseURI, {
586
- type: referencingElement.element,
273
+ // annotate fragment with info about origin
274
+ mergedElement.meta.set('ref-origin', reference.uri);
275
+ mergedElement.meta.set('ref-type', element.element);
276
+ return mergedElement;
277
+ },
278
+ circular: {
279
+ uri: $refBaseURI,
280
+ meta: {
587
281
  uri: reference.uri,
588
- $ref: toValue(referencingElement.$ref)
589
- });
590
- const replacer = this.options.dereference.strategyOpts['openapi-2']?.circularReplacer ?? this.options.dereference.circularReplacer;
591
- const replacement = replacer(refElement);
592
- path.replaceWith(replacement);
593
- path.skip();
594
- return;
595
- }
596
- }
597
-
598
- /**
599
- * Dive deep into the fragment.
600
- *
601
- * Cases to consider:
602
- * 1. We're crossing document boundary
603
- 2. Fragment is from non-entry document
604
- * 3. Fragment is a JSON Reference Object. We need to follow it to get the eventual value
605
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
606
- */
607
- const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
608
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
609
- if ((isExternalReference || isNonEntryDocument || isJSONReferenceElement(referencedElement) || shouldDetectCircular && !this.visited.has(referencedElement)) && !ancestorsLineage.includesCycle(referencedElement)) {
610
- if (shouldDetectCircular) {
611
- this.visited.add(referencedElement);
282
+ $ref
283
+ },
284
+ details: {
285
+ $ref
286
+ }
612
287
  }
613
-
614
- // append referencing reference to ancestors lineage
615
- directAncestors.add(referencingElement);
616
- const visitor = new OpenAPI2DereferenceVisitor({
617
- reference,
618
- indirections: [...this.indirections],
619
- options: this.options,
620
- refractCache: this.refractCache,
621
- visited: this.visited,
622
- ancestors: ancestorsLineage
623
- });
624
- referencedElement = await traverseAsync(referencedElement, visitor, {
625
- mutable: true,
626
- skipVisited: 'skip'
627
- });
628
-
629
- // remove referencing reference from ancestors lineage
630
- directAncestors.delete(referencingElement);
631
- }
632
-
633
- /**
634
- * Creating a new version of referenced element to avoid modifying the original one.
635
- */
636
- const mergedElement = cloneShallow(referencedElement);
637
- // annotate referenced element with info about original referencing element
638
- mergedElement.meta.set('ref-fields', {
639
- $ref: toValue(referencingElement.$ref)
640
288
  });
641
- // annotate fragment with info about origin
642
- mergedElement.meta.set('ref-origin', reference.uri);
643
- mergedElement.meta.set('ref-type', referencingElement.element);
644
-
645
- /**
646
- * Transclude referencing element with merged referenced element.
647
- */
648
- path.replaceWith(mergedElement);
649
- path.skip();
650
289
  } catch (error) {
651
- const $ref = toValue(referencingElement.$ref);
652
290
  this.handleError(`Error while dereferencing JSON Reference Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
653
- } finally {
654
- if (this.indirections.length > indirectionsSize) this.indirections.pop();
655
291
  }
656
292
  }
657
293
  }