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