@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
@@ -1,8 +1,8 @@
1
1
  import { propEq, none } from 'ramda';
2
2
  import { isElement, isStringElement, RefElement, cloneShallow, cloneDeep } from '@speclynx/apidom-datamodel';
3
- import { IdentityManager, toValue } from '@speclynx/apidom-core';
4
- import { ApiDOMError } from '@speclynx/apidom-error';
5
- import { traverseAsync } from '@speclynx/apidom-traverse';
3
+ import { toValue, toYAML } from '@speclynx/apidom-core';
4
+ import { ApiDOMStructuredError } from '@speclynx/apidom-error';
5
+ import { traverse, traverseAsync } from '@speclynx/apidom-traverse';
6
6
  import { evaluate as jsonPointerEvaluate, compile as jsonPointerCompile, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
7
7
  import { isParameterElement, isJSONSchemaElement, isBooleanJSONSchemaElement } from '@speclynx/apidom-ns-arazzo-1';
8
8
  import { parse as parseRuntimeExpression } from '@swaggerexpert/arazzo-runtime-expression';
@@ -10,6 +10,7 @@ import { isAnchor, uriToAnchor, evaluate as $anchorEvaluate } from "./selectors/
10
10
  import { evaluate as uriEvaluate } from "./selectors/uri.mjs";
11
11
  import { resolveSchema$refField } from "../openapi-3-1/util.mjs";
12
12
  import { maybeRefractToJSONSchemaElement } from "./util.mjs";
13
+ import UnresolvableReferenceError from "../../../errors/UnresolvableReferenceError.mjs";
13
14
  import MaximumDereferenceDepthError from "../../../errors/MaximumDereferenceDepthError.mjs";
14
15
  import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
15
16
  import * as url from "../../../util/url.mjs";
@@ -18,42 +19,50 @@ import Reference from "../../../Reference.mjs";
18
19
  import File from "../../../File.mjs";
19
20
  import { AncestorLineage } from "../../util.mjs";
20
21
  import EvaluationJsonSchemaUriError from "../../../errors/EvaluationJsonSchemaUriError.mjs";
21
- // initialize element identity manager
22
- const identityManager = new IdentityManager();
23
-
24
22
  /**
25
23
  * @public
26
24
  */
27
-
28
25
  /**
29
26
  * @public
30
27
  */
31
28
  class Arazzo1DereferenceVisitor {
32
29
  indirections;
33
- namespace;
34
30
  reference;
35
31
  options;
32
+
33
+ /**
34
+ * Tracks element ancestors across dive-deep traversal boundaries.
35
+ * Used for cycle detection: if a referenced element is found in
36
+ * the ancestor lineage, a circular reference is detected.
37
+ */
36
38
  ancestors;
37
39
  constructor({
38
40
  reference,
39
- namespace,
40
41
  options,
41
42
  indirections = [],
42
43
  ancestors = new AncestorLineage()
43
44
  }) {
44
45
  this.indirections = indirections;
45
- this.namespace = namespace;
46
46
  this.reference = reference;
47
47
  this.options = options;
48
48
  this.ancestors = new AncestorLineage(...ancestors);
49
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];
55
+ }
50
56
  toBaseURI(uri) {
51
57
  return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
52
58
  }
53
59
  async toReference(uri) {
54
60
  // detect maximum depth of resolution
55
61
  if (this.reference.depth >= this.options.resolve.maxDepth) {
56
- throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`);
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
+ });
57
66
  }
58
67
  const baseURI = this.toBaseURI(uri);
59
68
  const {
@@ -73,9 +82,23 @@ class Arazzo1DereferenceVisitor {
73
82
  });
74
83
 
75
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.
76
99
  const mutableReference = new Reference({
77
100
  uri: baseURI,
78
- value: cloneDeep(parseResult),
101
+ value: this.options.dereference.immutable ? cloneDeep(parseResult) : parseResult,
79
102
  depth: this.reference.depth + 1
80
103
  });
81
104
  refSet.add(mutableReference);
@@ -90,15 +113,77 @@ class Arazzo1DereferenceVisitor {
90
113
  }
91
114
  return mutableReference;
92
115
  }
93
- toAncestorLineage(path) {
94
- /**
95
- * Compute full ancestors lineage.
96
- * Ancestors are flatten to unwrap all Element instances.
97
- */
98
- const ancestorNodes = path.getAncestorNodes();
99
- const directAncestors = new Set(ancestorNodes.filter(isElement));
100
- const ancestorsLineage = new AncestorLineage(...this.ancestors, directAncestors);
101
- return [ancestorsLineage, directAncestors];
116
+
117
+ /**
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).
126
+ */
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: tree search for entry documents, visitor path for external
137
+ let location;
138
+ traverse(this.reference.value.result, {
139
+ enter: p => {
140
+ if (p.node === referencingElement) {
141
+ location = p.formatPath();
142
+ p.stop();
143
+ }
144
+ }
145
+ });
146
+ location ??= visitorPath.formatPath();
147
+ const hop = {
148
+ uri,
149
+ type,
150
+ refFieldName,
151
+ refFieldValue,
152
+ location,
153
+ codeFrame
154
+ };
155
+
156
+ // enrich existing error from nested visitor or create new one
157
+ let unresolvedError;
158
+ if (error instanceof UnresolvableReferenceError) {
159
+ // prefix relative locations for entries belonging to the referenced document
160
+ const refBaseURI = this.toBaseURI(refFieldValue);
161
+ const fragment = URIFragmentIdentifier.fromURIReference(refFieldValue);
162
+ if (fragment) {
163
+ if (refBaseURI === error.uri && error.location) {
164
+ error.location = fragment + error.location;
165
+ }
166
+ for (const h of error.trace) {
167
+ if (h.uri === refBaseURI && h.location) h.location = fragment + h.location;
168
+ }
169
+ }
170
+ // @ts-ignore
171
+ error.trace = [hop, ...error.trace];
172
+ unresolvedError = error;
173
+ } else {
174
+ unresolvedError = new UnresolvableReferenceError(message, {
175
+ cause: error,
176
+ type,
177
+ uri,
178
+ location,
179
+ codeFrame,
180
+ refFieldName,
181
+ refFieldValue,
182
+ trace: []
183
+ });
184
+ }
185
+ if (!isEntryDocument || continueOnError === false) throw unresolvedError;
186
+ if (typeof continueOnError === 'function') continueOnError(unresolvedError);
102
187
  }
103
188
  ReusableElement(path) {
104
189
  const referencingElement = path.node;
@@ -122,12 +207,16 @@ class Arazzo1DereferenceVisitor {
122
207
  tree
123
208
  } = parseRuntimeExpression(runtimeExpression);
124
209
  if (!result.success) {
125
- throw new ApiDOMError(`Invalid Reusable Object reference format: "${runtimeExpression}"`);
210
+ throw new ApiDOMStructuredError(`Invalid Reusable Object reference format: "${runtimeExpression}"`, {
211
+ runtimeExpression
212
+ });
126
213
  }
127
214
 
128
215
  // ReusableElement can only reference components
129
216
  if (tree.type !== 'ComponentsExpression') {
130
- throw new ApiDOMError(`Reusable Object reference "${runtimeExpression}" must be a components expression`);
217
+ throw new ApiDOMStructuredError(`Reusable Object reference "${runtimeExpression}" must be a components expression`, {
218
+ runtimeExpression
219
+ });
131
220
  }
132
221
 
133
222
  // evaluate runtime expression as JSON Pointer to get the referenced element
@@ -136,15 +225,15 @@ class Arazzo1DereferenceVisitor {
136
225
  try {
137
226
  referencedElement = jsonPointerEvaluate(this.reference.value.result, jsonPointer);
138
227
  } catch {
139
- throw new ApiDOMError(`Reusable Object reference "${runtimeExpression}" cannot be resolved`);
228
+ throw new ApiDOMStructuredError(`Reusable Object reference "${runtimeExpression}" cannot be resolved`, {
229
+ runtimeExpression
230
+ });
140
231
  }
141
232
 
142
233
  /**
143
234
  * Create a shallow clone of the referenced element to avoid modifying the original.
144
235
  */
145
236
  const mergedElement = cloneShallow(referencedElement);
146
- // assign unique id to merged element
147
- mergedElement.meta.set('id', identityManager.generateId());
148
237
  // annotate with info about original referencing element
149
238
  mergedElement.meta.set('ref-fields', {
150
239
  reference: runtimeExpression,
@@ -152,8 +241,7 @@ class Arazzo1DereferenceVisitor {
152
241
  });
153
242
  // annotate with info about origin
154
243
  mergedElement.meta.set('ref-origin', this.reference.uri);
155
- // annotate with info about referencing element
156
- mergedElement.meta.set('ref-referencing-element-id', identityManager.identify(referencingElement));
244
+ mergedElement.meta.set('ref-type', referencingElement.element);
157
245
 
158
246
  // override value field if present for Parameter Objects
159
247
  if (isParameterElement(mergedElement) && referencingElement.hasKey('value')) {
@@ -179,78 +267,33 @@ class Arazzo1DereferenceVisitor {
179
267
  path.skip();
180
268
  return;
181
269
  }
182
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
183
-
184
- // compute baseURI using rules around $id and $ref keywords
185
- let reference = await this.toReference(url.unsanitize(this.reference.uri));
186
- let {
187
- uri: retrievalURI
188
- } = reference;
189
- const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement);
190
- const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
191
- const file = new File({
192
- uri: $refBaseURIStrippedHash
193
- });
194
- const isUnknownURI = none(r => r.canRead(file), this.options.resolve.resolvers);
195
- const isURL = !isUnknownURI;
196
- let isInternalReference = url.stripHash(this.reference.uri) === $refBaseURI;
197
- let isExternalReference = !isInternalReference;
198
-
199
- // determining reference, proper evaluation and selection mechanism
200
- let referencedElement;
270
+ const indirectionsSize = this.indirections.length;
201
271
  try {
202
- if (isUnknownURI || isURL) {
203
- // we're dealing with canonical URI or URL with possible fragment
204
- retrievalURI = this.toBaseURI($refBaseURI);
205
- const selector = $refBaseURI;
206
- const referenceAsSchema = maybeRefractToJSONSchemaElement(reference.value.result);
207
- referencedElement = uriEvaluate(selector, referenceAsSchema);
208
- referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
209
- referencedElement.id = identityManager.identify(referencedElement);
210
-
211
- // ignore resolving internal Schema Objects
212
- if (!this.options.resolve.internal && isInternalReference) {
213
- // skip traversing this schema element but traverse all its child elements
214
- return;
215
- }
216
- // ignore resolving external Schema Objects
217
- if (!this.options.resolve.external && isExternalReference) {
218
- // skip traversing this schema element but traverse all its child elements
219
- return;
220
- }
221
- } else {
222
- // we're assuming here that we're dealing with JSON Pointer here
223
- retrievalURI = this.toBaseURI($refBaseURI);
224
- isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
225
- isExternalReference = !isInternalReference;
272
+ // compute baseURI using rules around $id and $ref keywords
273
+ let reference = await this.toReference(url.unsanitize(this.reference.uri));
274
+ let {
275
+ uri: retrievalURI
276
+ } = reference;
277
+ const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement);
278
+ const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
279
+ const file = new File({
280
+ uri: $refBaseURIStrippedHash
281
+ });
282
+ const isUnknownURI = none(r => r.canRead(file), this.options.resolve.resolvers);
283
+ const isURL = !isUnknownURI;
284
+ let isInternalReference = url.stripHash(this.reference.uri) === $refBaseURI;
285
+ let isExternalReference = !isInternalReference;
226
286
 
227
- // ignore resolving internal Schema Objects
228
- if (!this.options.resolve.internal && isInternalReference) {
229
- // skip traversing this schema element but traverse all its child elements
230
- return;
231
- }
232
- // ignore resolving external Schema Objects
233
- if (!this.options.resolve.external && isExternalReference) {
234
- // skip traversing this schema element but traverse all its child elements
235
- return;
236
- }
237
- reference = await this.toReference(url.unsanitize($refBaseURI));
238
- const selector = URIFragmentIdentifier.fromURIReference($refBaseURI);
239
- const referenceAsSchema = maybeRefractToJSONSchemaElement(reference.value.result);
240
- referencedElement = jsonPointerEvaluate(referenceAsSchema, selector);
241
- referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
242
- referencedElement.id = identityManager.identify(referencedElement);
243
- }
244
- } catch (error) {
245
- /**
246
- * JSONSchemaElement($id=URL) was not found, so we're going to try to resolve
247
- * the URL and assume the returned response is a JSON Schema.
248
- */
249
- if (isURL && error instanceof EvaluationJsonSchemaUriError) {
250
- if (isAnchor(uriToAnchor($refBaseURI))) {
251
- // we're dealing with JSON Schema $anchor here
252
- isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
253
- isExternalReference = !isInternalReference;
287
+ // determining reference, proper evaluation and selection mechanism
288
+ let referencedElement;
289
+ try {
290
+ if (isUnknownURI || isURL) {
291
+ // we're dealing with canonical URI or URL with possible fragment
292
+ retrievalURI = this.toBaseURI($refBaseURI);
293
+ const selector = $refBaseURI;
294
+ const referenceAsSchema = maybeRefractToJSONSchemaElement(reference.value.result);
295
+ referencedElement = uriEvaluate(selector, referenceAsSchema);
296
+ referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
254
297
 
255
298
  // ignore resolving internal Schema Objects
256
299
  if (!this.options.resolve.internal && isInternalReference) {
@@ -262,12 +305,6 @@ class Arazzo1DereferenceVisitor {
262
305
  // skip traversing this schema element but traverse all its child elements
263
306
  return;
264
307
  }
265
- reference = await this.toReference(url.unsanitize($refBaseURI));
266
- const selector = uriToAnchor($refBaseURI);
267
- const referenceAsSchema = maybeRefractToJSONSchemaElement(reference.value.result);
268
- referencedElement = $anchorEvaluate(selector, referenceAsSchema);
269
- referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
270
- referencedElement.id = identityManager.identify(referencedElement);
271
308
  } else {
272
309
  // we're assuming here that we're dealing with JSON Pointer here
273
310
  retrievalURI = this.toBaseURI($refBaseURI);
@@ -289,117 +326,169 @@ class Arazzo1DereferenceVisitor {
289
326
  const referenceAsSchema = maybeRefractToJSONSchemaElement(reference.value.result);
290
327
  referencedElement = jsonPointerEvaluate(referenceAsSchema, selector);
291
328
  referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
292
- referencedElement.id = identityManager.identify(referencedElement);
293
329
  }
294
- } else {
295
- throw error;
296
- }
297
- }
298
- this.indirections.push(referencingElement);
330
+ } catch (error) {
331
+ /**
332
+ * JSONSchemaElement($id=URL) was not found, so we're going to try to resolve
333
+ * the URL and assume the returned response is a JSON Schema.
334
+ */
335
+ if (isURL && error instanceof EvaluationJsonSchemaUriError) {
336
+ if (isAnchor(uriToAnchor($refBaseURI))) {
337
+ // we're dealing with JSON Schema $anchor here
338
+ isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
339
+ isExternalReference = !isInternalReference;
299
340
 
300
- // detect direct or indirect reference
301
- if (referencingElement === referencedElement) {
302
- throw new ApiDOMError('Recursive JSON Schema reference detected');
303
- }
341
+ // ignore resolving internal Schema Objects
342
+ if (!this.options.resolve.internal && isInternalReference) {
343
+ // skip traversing this schema element but traverse all its child elements
344
+ return;
345
+ }
346
+ // ignore resolving external Schema Objects
347
+ if (!this.options.resolve.external && isExternalReference) {
348
+ // skip traversing this schema element but traverse all its child elements
349
+ return;
350
+ }
351
+ reference = await this.toReference(url.unsanitize($refBaseURI));
352
+ const selector = uriToAnchor($refBaseURI);
353
+ const referenceAsSchema = maybeRefractToJSONSchemaElement(reference.value.result);
354
+ referencedElement = $anchorEvaluate(selector, referenceAsSchema);
355
+ referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
356
+ } else {
357
+ // we're assuming here that we're dealing with JSON Pointer here
358
+ retrievalURI = this.toBaseURI($refBaseURI);
359
+ isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
360
+ isExternalReference = !isInternalReference;
304
361
 
305
- // detect maximum depth of dereferencing
306
- if (this.indirections.length > this.options.dereference.maxDepth) {
307
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
308
- }
362
+ // ignore resolving internal Schema Objects
363
+ if (!this.options.resolve.internal && isInternalReference) {
364
+ // skip traversing this schema element but traverse all its child elements
365
+ return;
366
+ }
367
+ // ignore resolving external Schema Objects
368
+ if (!this.options.resolve.external && isExternalReference) {
369
+ // skip traversing this schema element but traverse all its child elements
370
+ return;
371
+ }
372
+ reference = await this.toReference(url.unsanitize($refBaseURI));
373
+ const selector = URIFragmentIdentifier.fromURIReference($refBaseURI);
374
+ const referenceAsSchema = maybeRefractToJSONSchemaElement(reference.value.result);
375
+ referencedElement = jsonPointerEvaluate(referenceAsSchema, selector);
376
+ referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
377
+ }
378
+ } else {
379
+ throw error;
380
+ }
381
+ }
382
+ this.indirections.push(referencingElement);
309
383
 
310
- // detect second deep dive into the same fragment and avoid it
311
- if (ancestorsLineage.includes(referencedElement)) {
312
- reference.refSet.circular = true;
313
- if (this.options.dereference.circular === 'error') {
314
- throw new ApiDOMError('Circular reference detected');
315
- } else if (this.options.dereference.circular === 'replace') {
316
- const refElement = new RefElement(referencedElement.id, {
317
- type: 'json-schema',
318
- uri: reference.uri,
384
+ // detect direct or indirect reference
385
+ if (referencingElement === referencedElement) {
386
+ throw new ApiDOMStructuredError('Recursive JSON Schema reference detected', {
319
387
  $ref: toValue(referencingElement.$ref)
320
388
  });
321
- const replacer = this.options.dereference.strategyOpts['arazzo-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
322
- const replacement = replacer(refElement);
323
- this.indirections.pop();
324
- path.replaceWith(replacement);
325
- return;
326
389
  }
327
- }
328
390
 
329
- /**
330
- * Dive deep into the fragment.
331
- *
332
- * Cases to consider:
333
- * 1. We're crossing document boundary
334
- * 2. Fragment is from non-root document
335
- * 3. Fragment is a JSON Schema with $ref field. We need to follow it to get the eventual value
336
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
337
- */
338
- const isNonRootDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
339
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
340
- if ((isExternalReference || isNonRootDocument || isJSONSchemaElement(referencedElement) && isStringElement(referencedElement.$ref) || shouldDetectCircular) && !ancestorsLineage.includesCycle(referencedElement)) {
341
- // append referencing reference to ancestors lineage
342
- directAncestors.add(referencingElement);
343
- const visitor = new Arazzo1DereferenceVisitor({
344
- reference,
345
- namespace: this.namespace,
346
- indirections: [...this.indirections],
347
- options: this.options,
348
- ancestors: ancestorsLineage
349
- });
350
- referencedElement = await traverseAsync(referencedElement, visitor, {
351
- mutable: true
352
- });
391
+ // detect maximum depth of dereferencing
392
+ if (this.indirections.length > this.options.dereference.maxDepth) {
393
+ throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
394
+ maxDepth: this.options.dereference.maxDepth,
395
+ uri: this.reference.uri
396
+ });
397
+ }
353
398
 
354
- // remove referencing reference from ancestors lineage
355
- directAncestors.delete(referencingElement);
356
- }
357
- this.indirections.pop();
399
+ // detect cross-boundary cycle
400
+ const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
401
+ if (ancestorsLineage.includes(referencedElement)) {
402
+ reference.refSet.circular = true;
403
+ if (this.options.dereference.circular === 'error') {
404
+ throw new ApiDOMStructuredError('Circular reference detected', {
405
+ $ref: toValue(referencingElement.$ref)
406
+ });
407
+ } else if (this.options.dereference.circular === 'replace') {
408
+ const refElement = new RefElement($refBaseURI, {
409
+ type: referencingElement.element,
410
+ uri: reference.uri,
411
+ $ref: toValue(referencingElement.$ref)
412
+ });
413
+ const replacer = this.options.dereference.strategyOpts['arazzo-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
414
+ const replacement = replacer(refElement);
415
+ path.replaceWith(replacement);
416
+ return;
417
+ }
418
+ }
358
419
 
359
- // Boolean JSON Schemas
360
- if (isBooleanJSONSchemaElement(referencedElement)) {
361
- const booleanJsonSchemaElement = cloneDeep(referencedElement);
362
- // assign unique id to merged element
363
- booleanJsonSchemaElement.meta.set('id', identityManager.generateId());
364
- // annotate referenced element with info about original referencing element
365
- booleanJsonSchemaElement.meta.set('ref-fields', {
366
- $ref: toValue(referencingElement.$ref)
367
- });
368
- // annotate referenced element with info about origin
369
- booleanJsonSchemaElement.meta.set('ref-origin', reference.uri);
370
- // annotate fragment with info about referencing element
371
- booleanJsonSchemaElement.meta.set('ref-referencing-element-id', identityManager.identify(referencingElement));
372
- path.replaceWith(booleanJsonSchemaElement);
373
- return;
374
- }
420
+ /**
421
+ * Dive deep into the fragment.
422
+ *
423
+ * Cases to consider:
424
+ * 1. We're crossing document boundary
425
+ * 2. Fragment is from non-entry document
426
+ * 3. Fragment is a JSON Schema with $ref field. We need to follow it to get the eventual value
427
+ * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
428
+ */
429
+ const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
430
+ const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
431
+ if ((isExternalReference || isNonEntryDocument || isJSONSchemaElement(referencedElement) && isStringElement(referencedElement.$ref) || shouldDetectCircular) && !ancestorsLineage.includesCycle(referencedElement)) {
432
+ // append referencing reference to ancestors lineage
433
+ directAncestors.add(referencingElement);
434
+ const visitor = new Arazzo1DereferenceVisitor({
435
+ reference,
436
+ indirections: [...this.indirections],
437
+ options: this.options,
438
+ ancestors: ancestorsLineage
439
+ });
440
+ referencedElement = await traverseAsync(referencedElement, visitor, {
441
+ mutable: true
442
+ });
375
443
 
376
- /**
377
- * Creating a new version of JSON Schema by merging fields from referenced Schema with referencing one.
378
- */
379
- if (isJSONSchemaElement(referencedElement)) {
380
- const mergedElement = cloneShallow(referencedElement);
381
- // assign unique id to merged element
382
- mergedElement.meta.set('id', identityManager.generateId());
383
- // existing keywords from referencing schema overrides ones from referenced schema
384
- referencingElement.forEach((value, keyElement, item) => {
385
- mergedElement.remove(toValue(keyElement));
386
- mergedElement.content.push(item);
387
- });
388
- mergedElement.remove('$ref');
389
- // annotate referenced element with info about original referencing element
390
- mergedElement.meta.set('ref-fields', {
391
- $ref: toValue(referencingElement.$ref)
392
- });
393
- // annotate fragment with info about origin
394
- mergedElement.meta.set('ref-origin', reference.uri);
395
- // annotate fragment with info about referencing element
396
- mergedElement.meta.set('ref-referencing-element-id', identityManager.identify(referencingElement));
397
- referencedElement = mergedElement;
444
+ // remove referencing reference from ancestors lineage
445
+ directAncestors.delete(referencingElement);
446
+ }
447
+
448
+ // Boolean JSON Schemas
449
+ if (isBooleanJSONSchemaElement(referencedElement)) {
450
+ const booleanJsonSchemaElement = cloneDeep(referencedElement);
451
+ // annotate referenced element with info about original referencing element
452
+ booleanJsonSchemaElement.meta.set('ref-fields', {
453
+ $ref: toValue(referencingElement.$ref)
454
+ });
455
+ // annotate referenced element with info about origin
456
+ booleanJsonSchemaElement.meta.set('ref-origin', reference.uri);
457
+ booleanJsonSchemaElement.meta.set('ref-type', referencingElement.element);
458
+ path.replaceWith(booleanJsonSchemaElement);
459
+ return;
460
+ }
461
+
462
+ /**
463
+ * Creating a new version of JSON Schema by merging fields from referenced Schema with referencing one.
464
+ */
465
+ if (isJSONSchemaElement(referencedElement)) {
466
+ const mergedElement = cloneShallow(referencedElement);
467
+ // existing keywords from referencing schema overrides ones from referenced schema
468
+ referencingElement.forEach((value, keyElement, item) => {
469
+ mergedElement.remove(toValue(keyElement));
470
+ mergedElement.content.push(item);
471
+ });
472
+ mergedElement.remove('$ref');
473
+ // annotate referenced element with info about original referencing element
474
+ mergedElement.meta.set('ref-fields', {
475
+ $ref: toValue(referencingElement.$ref)
476
+ });
477
+ // annotate fragment with info about origin
478
+ mergedElement.meta.set('ref-origin', reference.uri);
479
+ mergedElement.meta.set('ref-type', referencingElement.element);
480
+ referencedElement = mergedElement;
481
+ }
482
+ /**
483
+ * Transclude referencing element with merged referenced element.
484
+ */
485
+ path.replaceWith(referencedElement);
486
+ } catch (error) {
487
+ const $ref = toValue(referencingElement.$ref);
488
+ this.handleError(`Error while dereferencing Schema Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
489
+ } finally {
490
+ if (this.indirections.length > indirectionsSize) this.indirections.pop();
398
491
  }
399
- /**
400
- * Transclude referencing element with merged referenced element.
401
- */
402
- path.replaceWith(referencedElement);
403
492
  }
404
493
  }
405
494
  export default Arazzo1DereferenceVisitor;
@@ -1,12 +1,11 @@
1
1
  "use strict";
2
2
 
3
3
  var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
4
- var _interopRequireWildcard = require("@babel/runtime-corejs3/helpers/interopRequireWildcard").default;
5
4
  exports.__esModule = true;
6
5
  exports.default = void 0;
7
6
  var _apidomDatamodel = require("@speclynx/apidom-datamodel");
8
7
  var _apidomTraverse = require("@speclynx/apidom-traverse");
9
- var _apidomNsAsyncapi = _interopRequireWildcard(require("@speclynx/apidom-ns-asyncapi-2"));
8
+ var _apidomNsAsyncapi = require("@speclynx/apidom-ns-asyncapi-2");
10
9
  var _DereferenceStrategy = _interopRequireDefault(require("../DereferenceStrategy.cjs"));
11
10
  var _Reference = _interopRequireDefault(require("../../../Reference.cjs"));
12
11
  var _ReferenceSet = _interopRequireDefault(require("../../../ReferenceSet.cjs"));
@@ -36,7 +35,6 @@ class AsyncAPI2DereferenceStrategy extends _DereferenceStrategy.default {
36
35
  return (0, _apidomNsAsyncapi.isAsyncApi2Element)(file.parseResult?.result);
37
36
  }
38
37
  async dereference(file, options) {
39
- const namespace = new _apidomDatamodel.Namespace().use(_apidomNsAsyncapi.default);
40
38
  const immutableRefSet = options.dereference.refSet ?? new _ReferenceSet.default();
41
39
  const mutableRefSet = new _ReferenceSet.default();
42
40
  let refSet = immutableRefSet;
@@ -66,7 +64,6 @@ class AsyncAPI2DereferenceStrategy extends _DereferenceStrategy.default {
66
64
  }
67
65
  const visitor = new _visitor.default({
68
66
  reference,
69
- namespace,
70
67
  options
71
68
  });
72
69
  const dereferencedElement = await (0, _apidomTraverse.traverseAsync)(refSet.rootRef.value, visitor, {