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