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