@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,53 +1,59 @@
1
1
  import { propEq, none } from 'ramda';
2
2
  import { isUndefined } from 'ramda-adjunct';
3
- import { isElement, isPrimitiveElement, isStringElement, isObjectElement, RefElement, cloneShallow, cloneDeep } from '@speclynx/apidom-datamodel';
4
- import { IdentityManager, toValue, fixedFields } from '@speclynx/apidom-core';
5
- import { ApiDOMError } from '@speclynx/apidom-error';
6
- import { traverseAsync, find } from '@speclynx/apidom-traverse';
3
+ import { isElement, isStringElement, isObjectElement, RefElement, cloneShallow, cloneDeep } from '@speclynx/apidom-datamodel';
4
+ import { toValue, fixedFields, 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 as jsonPointerEvaluate, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
8
8
  import { isReferenceLikeElement, isPathItemElement, isReferenceElement, isSchemaElement, isOperationElement, isBooleanJSONSchemaElement, refract, refractReference, refractPathItem, refractOperation } from '@speclynx/apidom-ns-openapi-3-1';
9
9
  import { isAnchor, uriToAnchor, evaluate as $anchorEvaluate } from "./selectors/$anchor.mjs";
10
10
  import { evaluate as uriEvaluate } from "./selectors/uri.mjs";
11
11
  import MaximumDereferenceDepthError from "../../../errors/MaximumDereferenceDepthError.mjs";
12
12
  import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
13
+ import UnresolvableReferenceError from "../../../errors/UnresolvableReferenceError.mjs";
14
+ import EvaluationJsonSchemaUriError from "../../../errors/EvaluationJsonSchemaUriError.mjs";
13
15
  import * as url from "../../../util/url.mjs";
14
16
  import parse from "../../../parse/index.mjs";
15
17
  import Reference from "../../../Reference.mjs";
16
18
  import File from "../../../File.mjs";
17
19
  import { resolveSchema$refField, maybeRefractToSchemaElement } from "./util.mjs";
18
20
  import { AncestorLineage } from "../../util.mjs";
19
- import EvaluationJsonSchemaUriError from "../../../errors/EvaluationJsonSchemaUriError.mjs";
20
- // initialize element identity manager
21
- const identityManager = new IdentityManager();
22
-
23
21
  /**
24
22
  * @public
25
23
  */
26
-
27
24
  /**
28
25
  * @public
29
26
  */
30
27
  class OpenAPI3_1DereferenceVisitor {
31
28
  indirections;
32
- namespace;
33
29
  reference;
34
30
  options;
35
- ancestors;
36
31
  refractCache;
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
+ */
38
+ ancestors;
37
39
  constructor({
38
40
  reference,
39
- namespace,
40
41
  options,
41
42
  indirections = [],
42
- ancestors = new AncestorLineage(),
43
- refractCache = new Map()
43
+ refractCache = new WeakMap(),
44
+ ancestors = new AncestorLineage()
44
45
  }) {
45
46
  this.indirections = indirections;
46
- this.namespace = namespace;
47
47
  this.reference = reference;
48
48
  this.options = options;
49
- this.ancestors = new AncestorLineage(...ancestors);
50
49
  this.refractCache = refractCache;
50
+ this.ancestors = new AncestorLineage(...ancestors);
51
+ }
52
+ toAncestorLineage(path) {
53
+ const ancestorNodes = path.getAncestorNodes();
54
+ const directAncestors = new Set(ancestorNodes.filter(isElement));
55
+ const ancestorsLineage = new AncestorLineage(...this.ancestors, directAncestors);
56
+ return [ancestorsLineage, directAncestors];
51
57
  }
52
58
  toBaseURI(uri) {
53
59
  return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
@@ -55,7 +61,10 @@ class OpenAPI3_1DereferenceVisitor {
55
61
  async toReference(uri) {
56
62
  // detect maximum depth of resolution
57
63
  if (this.reference.depth >= this.options.resolve.maxDepth) {
58
- throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`);
64
+ throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`, {
65
+ maxDepth: this.options.resolve.maxDepth,
66
+ uri: this.reference.uri
67
+ });
59
68
  }
60
69
  const baseURI = this.toBaseURI(uri);
61
70
  const {
@@ -75,14 +84,28 @@ class OpenAPI3_1DereferenceVisitor {
75
84
  });
76
85
 
77
86
  // register new mutable reference with a refSet
87
+ //
88
+ // NOTE(known limitation): the mutable reference is mutated in place during traversal
89
+ // (via `{ mutable: true }`). When an external document evaluates a JSON pointer back
90
+ // into this document, it may receive an already-resolved element instead of the original
91
+ // $ref. That resolved element was produced using the entry document's resolution context
92
+ // (ancestors, indirections), which may differ from the external document's context.
93
+ // This can affect cycle detection in rare cross-document circular reference patterns.
94
+ //
95
+ // Remediation: evaluate JSON pointers against the immutable (original) parse tree
96
+ // instead of the mutable working copy. The `immutable://` reference below preserves
97
+ // the original tree and could be used for pointer evaluation, ensuring every resolution
98
+ // context always sees raw, unresolved elements and processes them with its own
99
+ // ancestors/indirections. The trade-off is that elements referenced by multiple
100
+ // documents would be resolved once per context instead of being reused.
78
101
  const mutableReference = new Reference({
79
102
  uri: baseURI,
80
- value: cloneDeep(parseResult),
103
+ value: this.options.dereference.immutable ? cloneDeep(parseResult) : parseResult,
81
104
  depth: this.reference.depth + 1
82
105
  });
83
106
  refSet.add(mutableReference);
84
107
  if (this.options.dereference.immutable) {
85
- // register new immutable reference with a refSet
108
+ // register new immutable reference with original parseResult
86
109
  const immutableReference = new Reference({
87
110
  uri: `immutable://${baseURI}`,
88
111
  value: parseResult,
@@ -92,25 +115,86 @@ class OpenAPI3_1DereferenceVisitor {
92
115
  }
93
116
  return mutableReference;
94
117
  }
95
- toAncestorLineage(path) {
96
- /**
97
- * Compute full ancestors lineage.
98
- * Ancestors are flatten to unwrap all Element instances.
99
- */
100
- const ancestorNodes = path.getAncestorNodes();
101
- const directAncestors = new Set(ancestorNodes.filter(isElement));
102
- const ancestorsLineage = new AncestorLineage(...this.ancestors, directAncestors);
103
- return [ancestorsLineage, directAncestors];
118
+
119
+ /**
120
+ * Handles an error according to the continueOnError option.
121
+ *
122
+ * For new errors: wraps in UnresolvableReferenceError with structured context
123
+ * (type, uri, location, codeFrame, refFieldName, refFieldValue, trace).
124
+ * For errors already wrapped by a nested visitor: prepends the current hop to the trace.
125
+ *
126
+ * Inner/intermediate visitors always throw to let the trace accumulate.
127
+ * Only the entry document visitor respects continueOnError (callback/swallow/throw).
128
+ */
129
+ handleError(message, error, referencingElement, refFieldName, refFieldValue, visitorPath) {
130
+ const {
131
+ continueOnError
132
+ } = this.options.dereference;
133
+ const isEntryDocument = url.stripHash(this.reference.refSet?.rootRef?.uri ?? '') === this.reference.uri;
134
+ const uri = this.reference.uri;
135
+ const type = referencingElement.element;
136
+ const codeFrame = toYAML(referencingElement);
137
+
138
+ // find element location: tree search for entry documents, visitor path for external
139
+ let location;
140
+ traverse(this.reference.value.result, {
141
+ enter: p => {
142
+ if (p.node === referencingElement || this.refractCache.get(p.node) === referencingElement) {
143
+ location = p.formatPath();
144
+ p.stop();
145
+ }
146
+ }
147
+ });
148
+ location ??= visitorPath.formatPath();
149
+ const hop = {
150
+ uri,
151
+ type,
152
+ refFieldName,
153
+ refFieldValue,
154
+ location,
155
+ codeFrame
156
+ };
157
+
158
+ // enrich existing error from nested visitor or create new one
159
+ let unresolvedError;
160
+ if (error instanceof UnresolvableReferenceError) {
161
+ // prefix relative locations for entries belonging to the referenced document
162
+ const refBaseURI = this.toBaseURI(refFieldValue);
163
+ const fragment = URIFragmentIdentifier.fromURIReference(refFieldValue);
164
+ if (fragment) {
165
+ if (refBaseURI === error.uri && error.location) {
166
+ error.location = fragment + error.location;
167
+ }
168
+ for (const h of error.trace) {
169
+ if (h.uri === refBaseURI && h.location) h.location = fragment + h.location;
170
+ }
171
+ }
172
+ // @ts-ignore
173
+ error.trace = [hop, ...error.trace];
174
+ unresolvedError = error;
175
+ } else {
176
+ unresolvedError = new UnresolvableReferenceError(message, {
177
+ cause: error,
178
+ type,
179
+ uri,
180
+ location,
181
+ codeFrame,
182
+ refFieldName,
183
+ refFieldValue,
184
+ trace: []
185
+ });
186
+ }
187
+ if (!isEntryDocument || continueOnError === false) throw unresolvedError;
188
+ if (typeof continueOnError === 'function') continueOnError(unresolvedError);
104
189
  }
105
190
  async ReferenceElement(path) {
106
191
  const referencingElement = path.node;
107
192
 
108
- // skip current referencing element as it's already been access
193
+ // skip current referencing element as it's already been accessed
109
194
  if (this.indirections.includes(referencingElement)) {
110
195
  path.skip();
111
196
  return;
112
197
  }
113
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
114
198
  const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref));
115
199
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
116
200
  const isExternalReference = !isInternalReference;
@@ -127,133 +211,140 @@ class OpenAPI3_1DereferenceVisitor {
127
211
  path.skip();
128
212
  return;
129
213
  }
130
- const reference = await this.toReference(toValue(referencingElement.$ref));
131
214
  const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref));
132
- this.indirections.push(referencingElement);
133
- const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
215
+ const indirectionsSize = this.indirections.length;
216
+ try {
217
+ const reference = await this.toReference(toValue(referencingElement.$ref));
218
+ this.indirections.push(referencingElement);
219
+ const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
134
220
 
135
- // possibly non-semantic fragment
136
- let referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
137
- referencedElement.id = identityManager.identify(referencedElement);
221
+ // possibly non-semantic fragment
222
+ let referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
138
223
 
139
- // applying semantics to a fragment
140
- if (isPrimitiveElement(referencedElement)) {
224
+ // applying semantics to a fragment
141
225
  const referencedElementType = referencingElement.meta.get('referenced-element');
142
- const cacheKey = `${referencedElementType}-${identityManager.identify(referencedElement)}`;
143
- if (this.refractCache.has(cacheKey)) {
144
- referencedElement = this.refractCache.get(cacheKey);
145
- } else if (isReferenceLikeElement(referencedElement)) {
146
- // handling indirect references
147
- referencedElement = refractReference(referencedElement);
148
- referencedElement.meta.set('referenced-element', referencedElementType);
149
- this.refractCache.set(cacheKey, referencedElement);
150
- } else {
151
- // handling direct references
152
- referencedElement = refract(referencedElement, {
153
- element: referencedElementType
226
+ if (referencedElement.element !== referencedElementType && !isReferenceElement(referencedElement)) {
227
+ if (this.refractCache.has(referencedElement)) {
228
+ referencedElement = this.refractCache.get(referencedElement);
229
+ } else if (isReferenceLikeElement(referencedElement)) {
230
+ // handling generic indirect references
231
+ const sourceElement = referencedElement;
232
+ referencedElement = refractReference(referencedElement);
233
+ referencedElement.meta.set('referenced-element', referencedElementType);
234
+ this.refractCache.set(sourceElement, referencedElement);
235
+ } else {
236
+ // handling direct references
237
+ const sourceElement = referencedElement;
238
+ referencedElement = refract(referencedElement, {
239
+ element: referencedElementType
240
+ });
241
+ this.refractCache.set(sourceElement, referencedElement);
242
+ }
243
+ }
244
+
245
+ // detect direct or indirect reference
246
+ if (referencingElement === referencedElement) {
247
+ throw new ApiDOMStructuredError('Recursive Reference Object detected', {
248
+ $ref: toValue(referencingElement.$ref)
154
249
  });
155
- this.refractCache.set(cacheKey, referencedElement);
156
250
  }
157
- }
158
251
 
159
- // detect direct or indirect reference
160
- if (referencingElement === referencedElement) {
161
- throw new ApiDOMError('Recursive Reference Object detected');
162
- }
252
+ // detect maximum depth of dereferencing
253
+ if (this.indirections.length > this.options.dereference.maxDepth) {
254
+ throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
255
+ maxDepth: this.options.dereference.maxDepth,
256
+ uri: this.reference.uri
257
+ });
258
+ }
163
259
 
164
- // detect maximum depth of dereferencing
165
- if (this.indirections.length > this.options.dereference.maxDepth) {
166
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
167
- }
260
+ // detect cross-boundary cycle
261
+ const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
262
+ if (ancestorsLineage.includes(referencedElement)) {
263
+ reference.refSet.circular = true;
264
+ if (this.options.dereference.circular === 'error') {
265
+ throw new ApiDOMStructuredError('Circular reference detected', {
266
+ $ref: toValue(referencingElement.$ref)
267
+ });
268
+ } else if (this.options.dereference.circular === 'replace') {
269
+ const refElement = new RefElement($refBaseURI, {
270
+ type: referencingElement.element,
271
+ uri: reference.uri,
272
+ $ref: toValue(referencingElement.$ref)
273
+ });
274
+ const replacer = this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
275
+ const replacement = replacer(refElement);
276
+ path.replaceWith(replacement);
277
+ return;
278
+ }
279
+ }
168
280
 
169
- // detect second deep dive into the same fragment and avoid it
170
- if (ancestorsLineage.includes(referencedElement)) {
171
- reference.refSet.circular = true;
172
- if (this.options.dereference.circular === 'error') {
173
- throw new ApiDOMError('Circular reference detected');
174
- } else if (this.options.dereference.circular === 'replace') {
175
- const refElement = new RefElement(referencedElement.id, {
176
- type: 'reference',
177
- uri: reference.uri,
178
- $ref: toValue(referencingElement.$ref)
281
+ /**
282
+ * Dive deep into the fragment.
283
+ *
284
+ * Cases to consider:
285
+ * 1. We're crossing document boundary
286
+ * 2. Fragment is from non-entry document
287
+ * 3. Fragment is a Reference Object. We need to follow it to get the eventual value
288
+ * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
289
+ */
290
+ const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
291
+ const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
292
+ if ((isExternalReference || isNonEntryDocument || isReferenceElement(referencedElement) || shouldDetectCircular) && !ancestorsLineage.includesCycle(referencedElement)) {
293
+ directAncestors.add(referencingElement);
294
+ const visitor = new OpenAPI3_1DereferenceVisitor({
295
+ reference,
296
+ indirections: [...this.indirections],
297
+ options: this.options,
298
+ refractCache: this.refractCache,
299
+ ancestors: ancestorsLineage
179
300
  });
180
- const replacer = this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
181
- const replacement = replacer(refElement);
182
- this.indirections.pop();
183
- path.replaceWith(replacement);
184
- return;
301
+ referencedElement = await traverseAsync(referencedElement, visitor, {
302
+ mutable: true
303
+ });
304
+ directAncestors.delete(referencingElement);
185
305
  }
186
- }
187
306
 
188
- /**
189
- * Dive deep into the fragment.
190
- *
191
- * Cases to consider:
192
- * 1. We're crossing document boundary
193
- * 2. Fragment is from non-root document
194
- * 3. Fragment is a Reference Object. We need to follow it to get the eventual value
195
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
196
- */
197
- const isNonRootDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
198
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
199
- if ((isExternalReference || isNonRootDocument || isReferenceElement(referencedElement) || shouldDetectCircular) && !ancestorsLineage.includesCycle(referencedElement)) {
200
- // append referencing reference to ancestors lineage
201
- directAncestors.add(referencingElement);
202
- const visitor = new OpenAPI3_1DereferenceVisitor({
203
- reference,
204
- namespace: this.namespace,
205
- indirections: [...this.indirections],
206
- options: this.options,
207
- refractCache: this.refractCache,
208
- ancestors: ancestorsLineage
209
- });
210
- referencedElement = await traverseAsync(referencedElement, visitor, {
211
- mutable: true
307
+ /**
308
+ * Creating a new version of referenced element to avoid modifying the original one.
309
+ */
310
+ const mergedElement = cloneShallow(referencedElement);
311
+ // annotate fragment with info about original Reference element
312
+ mergedElement.meta.set('ref-fields', {
313
+ $ref: toValue(referencingElement.$ref),
314
+ // @ts-ignore
315
+ description: toValue(referencingElement.description),
316
+ // @ts-ignore
317
+ summary: toValue(referencingElement.summary)
212
318
  });
319
+ // annotate fragment with info about origin and type
320
+ mergedElement.meta.set('ref-origin', reference.uri);
321
+ mergedElement.meta.set('ref-type', referencingElement.element);
213
322
 
214
- // remove referencing reference from ancestors lineage
215
- directAncestors.delete(referencingElement);
216
- }
217
- this.indirections.pop();
218
-
219
- /**
220
- * Creating a new version of referenced element to avoid modifying the original one.
221
- */
222
- const mergedElement = cloneShallow(referencedElement);
223
- // assign unique id to merged element
224
- mergedElement.meta.set('id', identityManager.generateId());
225
- // annotate fragment with info about original Reference element
226
- mergedElement.meta.set('ref-fields', {
227
- $ref: toValue(referencingElement.$ref),
228
- // @ts-ignore
229
- description: toValue(referencingElement.description),
230
- // @ts-ignore
231
- summary: toValue(referencingElement.summary)
232
- });
233
- // annotate fragment with info about origin
234
- mergedElement.meta.set('ref-origin', reference.uri);
235
- // annotate fragment with info about referencing element
236
- mergedElement.meta.set('ref-referencing-element-id', identityManager.identify(referencingElement));
237
-
238
- // override description and summary (outer has higher priority then inner)
239
- if (isObjectElement(referencedElement) && isObjectElement(mergedElement)) {
240
- const fields = fixedFields(referencedElement, {
241
- indexed: true
242
- });
243
- if (referencingElement.hasKey('description') && Object.hasOwn(fields, 'description')) {
244
- mergedElement.remove('description');
245
- mergedElement.set('description', referencingElement.get('description'));
246
- }
247
- if (referencingElement.hasKey('summary') && Object.hasOwn(fields, 'summary')) {
248
- mergedElement.remove('summary');
249
- mergedElement.set('summary', referencingElement.get('summary'));
323
+ // override description and summary (outer has higher priority then inner)
324
+ if (isObjectElement(referencedElement) && isObjectElement(mergedElement)) {
325
+ const fields = fixedFields(referencedElement, {
326
+ indexed: true
327
+ });
328
+ if (referencingElement.hasKey('description') && Object.hasOwn(fields, 'description')) {
329
+ mergedElement.remove('description');
330
+ mergedElement.set('description', referencingElement.get('description'));
331
+ }
332
+ if (referencingElement.hasKey('summary') && Object.hasOwn(fields, 'summary')) {
333
+ mergedElement.remove('summary');
334
+ mergedElement.set('summary', referencingElement.get('summary'));
335
+ }
250
336
  }
251
- }
252
337
 
253
- /**
254
- * Transclude referencing element with merged referenced element.
255
- */
256
- path.replaceWith(mergedElement);
338
+ /**
339
+ * Transclude referencing element with merged referenced element.
340
+ */
341
+ path.replaceWith(mergedElement);
342
+ } catch (error) {
343
+ const $ref = toValue(referencingElement.$ref);
344
+ this.handleError(`Error while dereferencing Reference Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
345
+ } finally {
346
+ if (this.indirections.length > indirectionsSize) this.indirections.pop();
347
+ }
257
348
  }
258
349
  async PathItemElement(path) {
259
350
  const referencingElement = path.node;
@@ -263,12 +354,11 @@ class OpenAPI3_1DereferenceVisitor {
263
354
  return;
264
355
  }
265
356
 
266
- // skip current referencing element as it's already been access
357
+ // skip current referencing element as it's already been accessed
267
358
  if (this.indirections.includes(referencingElement)) {
268
359
  path.skip();
269
360
  return;
270
361
  }
271
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
272
362
  const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref));
273
363
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
274
364
  const isExternalReference = !isInternalReference;
@@ -283,117 +373,121 @@ class OpenAPI3_1DereferenceVisitor {
283
373
  // skip traversing this Path Item element but traverse all it's child elements
284
374
  return;
285
375
  }
286
- const reference = await this.toReference(toValue(referencingElement.$ref));
287
376
  const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref));
288
- this.indirections.push(referencingElement);
289
- const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
290
-
291
- // possibly non-semantic referenced element
292
- let referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
293
- referencedElement.id = identityManager.identify(referencedElement);
294
-
295
- /**
296
- * Applying semantics to a referenced element if semantics are missing.
297
- */
298
- if (isPrimitiveElement(referencedElement)) {
299
- const cacheKey = `path-item-${identityManager.identify(referencedElement)}`;
300
- if (this.refractCache.has(cacheKey)) {
301
- referencedElement = this.refractCache.get(cacheKey);
302
- } else {
303
- referencedElement = refractPathItem(referencedElement);
304
- this.refractCache.set(cacheKey, referencedElement);
305
- }
306
- }
377
+ const indirectionsSize = this.indirections.length;
378
+ try {
379
+ const reference = await this.toReference(toValue(referencingElement.$ref));
380
+ this.indirections.push(referencingElement);
381
+ const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
307
382
 
308
- // detect direct or indirect reference
309
- if (referencingElement === referencedElement) {
310
- throw new ApiDOMError('Recursive Path Item Object reference detected');
311
- }
383
+ // possibly non-semantic referenced element
384
+ let referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
312
385
 
313
- // detect maximum depth of dereferencing
314
- if (this.indirections.length > this.options.dereference.maxDepth) {
315
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
316
- }
386
+ // applying semantics to a referenced element
387
+ if (!isPathItemElement(referencedElement)) {
388
+ if (this.refractCache.has(referencedElement)) {
389
+ referencedElement = this.refractCache.get(referencedElement);
390
+ } else {
391
+ const sourceElement = referencedElement;
392
+ referencedElement = refractPathItem(referencedElement);
393
+ this.refractCache.set(sourceElement, referencedElement);
394
+ }
395
+ }
317
396
 
318
- // detect second deep dive into the same fragment and avoid it
319
- if (ancestorsLineage.includes(referencedElement)) {
320
- reference.refSet.circular = true;
321
- if (this.options.dereference.circular === 'error') {
322
- throw new ApiDOMError('Circular reference detected');
323
- } else if (this.options.dereference.circular === 'replace') {
324
- const refElement = new RefElement(referencedElement.id, {
325
- type: 'path-item',
326
- uri: reference.uri,
397
+ // detect direct or indirect reference
398
+ if (referencingElement === referencedElement) {
399
+ throw new ApiDOMStructuredError('Recursive Path Item Object reference detected', {
327
400
  $ref: toValue(referencingElement.$ref)
328
401
  });
329
- const replacer = this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
330
- const replacement = replacer(refElement);
331
- this.indirections.pop();
332
- path.replaceWith(replacement);
333
- return;
334
402
  }
335
- }
336
403
 
337
- /**
338
- * Dive deep into the fragment.
339
- *
340
- * Cases to consider:
341
- * 1. We're crossing document boundary
342
- * 2. Fragment is from non-root document
343
- * 3. Fragment is a Path Item Object with $ref field. We need to follow it to get the eventual value
344
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
345
- */
346
- const isNonRootDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
347
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
348
- if ((isExternalReference || isNonRootDocument || isPathItemElement(referencedElement) && isStringElement(referencedElement.$ref) || shouldDetectCircular) && !ancestorsLineage.includesCycle(referencedElement)) {
349
- // append referencing reference to ancestors lineage
350
- directAncestors.add(referencingElement);
351
- const visitor = new OpenAPI3_1DereferenceVisitor({
352
- reference,
353
- namespace: this.namespace,
354
- indirections: [...this.indirections],
355
- options: this.options,
356
- refractCache: this.refractCache,
357
- ancestors: ancestorsLineage
358
- });
359
- referencedElement = await traverseAsync(referencedElement, visitor, {
360
- mutable: true
361
- });
404
+ // detect maximum depth of dereferencing
405
+ if (this.indirections.length > this.options.dereference.maxDepth) {
406
+ throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
407
+ maxDepth: this.options.dereference.maxDepth,
408
+ uri: this.reference.uri
409
+ });
410
+ }
362
411
 
363
- // remove referencing reference from ancestors lineage
364
- directAncestors.delete(referencingElement);
365
- }
366
- this.indirections.pop();
412
+ // detect cross-boundary cycle
413
+ const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
414
+ if (ancestorsLineage.includes(referencedElement)) {
415
+ reference.refSet.circular = true;
416
+ if (this.options.dereference.circular === 'error') {
417
+ throw new ApiDOMStructuredError('Circular reference detected', {
418
+ $ref: toValue(referencingElement.$ref)
419
+ });
420
+ } else if (this.options.dereference.circular === 'replace') {
421
+ const refElement = new RefElement($refBaseURI, {
422
+ type: referencingElement.element,
423
+ uri: reference.uri,
424
+ $ref: toValue(referencingElement.$ref)
425
+ });
426
+ const replacer = this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
427
+ const replacement = replacer(refElement);
428
+ path.replaceWith(replacement);
429
+ return;
430
+ }
431
+ }
367
432
 
368
- /**
369
- * Creating a new version of Path Item by merging fields from referenced Path Item with referencing one.
370
- */
371
- if (isPathItemElement(referencedElement)) {
372
- const mergedElement = cloneShallow(referencedElement);
373
- // assign unique id to merged element
374
- mergedElement.meta.set('id', identityManager.generateId());
375
- // existing keywords from referencing PathItemElement overrides ones from referenced element
376
- referencingElement.forEach((value, keyElement, item) => {
377
- mergedElement.remove(toValue(keyElement));
378
- mergedElement.content.push(item);
379
- });
380
- mergedElement.remove('$ref');
433
+ /**
434
+ * Dive deep into the fragment.
435
+ *
436
+ * Cases to consider:
437
+ * 1. We're crossing document boundary
438
+ * 2. Fragment is from non-entry document
439
+ * 3. Fragment is a Path Item Object with $ref field. We need to follow it to get the eventual value
440
+ * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
441
+ */
442
+ const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
443
+ const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
444
+ if ((isExternalReference || isNonEntryDocument || isPathItemElement(referencedElement) && isStringElement(referencedElement.$ref) || shouldDetectCircular) && !ancestorsLineage.includesCycle(referencedElement)) {
445
+ directAncestors.add(referencingElement);
446
+ const visitor = new OpenAPI3_1DereferenceVisitor({
447
+ reference,
448
+ indirections: [...this.indirections],
449
+ options: this.options,
450
+ refractCache: this.refractCache,
451
+ ancestors: ancestorsLineage
452
+ });
453
+ referencedElement = await traverseAsync(referencedElement, visitor, {
454
+ mutable: true
455
+ });
456
+ directAncestors.delete(referencingElement);
457
+ }
381
458
 
382
- // annotate referenced element with info about original referencing element
383
- mergedElement.meta.set('ref-fields', {
384
- $ref: toValue(referencingElement.$ref)
385
- });
386
- // annotate referenced element with info about origin
387
- mergedElement.meta.set('ref-origin', reference.uri);
388
- // annotate fragment with info about referencing element
389
- mergedElement.meta.set('ref-referencing-element-id', identityManager.identify(referencingElement));
390
- referencedElement = mergedElement;
391
- }
459
+ /**
460
+ * Creating a new version of Path Item by merging fields from referenced Path Item with referencing one.
461
+ */
462
+ if (isPathItemElement(referencedElement)) {
463
+ const mergedElement = cloneShallow(referencedElement);
464
+ // existing keywords from referencing PathItemElement overrides ones from referenced element
465
+ referencingElement.forEach((value, keyElement, item) => {
466
+ mergedElement.remove(toValue(keyElement));
467
+ mergedElement.content.push(item);
468
+ });
469
+ mergedElement.remove('$ref');
470
+
471
+ // annotate referenced element with info about original referencing element
472
+ mergedElement.meta.set('ref-fields', {
473
+ $ref: toValue(referencingElement.$ref)
474
+ });
475
+ // annotate referenced element with info about origin and type
476
+ mergedElement.meta.set('ref-origin', reference.uri);
477
+ mergedElement.meta.set('ref-type', referencingElement.element);
478
+ referencedElement = mergedElement;
479
+ }
392
480
 
393
- /**
394
- * Transclude referencing element with merged referenced element.
395
- */
396
- path.replaceWith(referencedElement);
481
+ /**
482
+ * Transclude referencing element with merged referenced element.
483
+ */
484
+ path.replaceWith(referencedElement);
485
+ } catch (error) {
486
+ const $ref = toValue(referencingElement.$ref);
487
+ this.handleError(`Error while dereferencing Path Item Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
488
+ } finally {
489
+ if (this.indirections.length > indirectionsSize) this.indirections.pop();
490
+ }
397
491
  }
398
492
  async LinkElement(path) {
399
493
  const linkElement = path.node;
@@ -405,66 +499,78 @@ class OpenAPI3_1DereferenceVisitor {
405
499
 
406
500
  // operationRef and operationId fields are mutually exclusive
407
501
  if (isStringElement(linkElement.operationRef) && isStringElement(linkElement.operationId)) {
408
- throw new ApiDOMError('LinkElement operationRef and operationId fields are mutually exclusive');
502
+ throw new ApiDOMStructuredError('LinkElement operationRef and operationId fields are mutually exclusive', {
503
+ operationRef: toValue(linkElement.operationRef),
504
+ operationId: toValue(linkElement.operationId)
505
+ });
409
506
  }
410
- let operationElement;
411
- if (isStringElement(linkElement.operationRef)) {
412
- // possibly non-semantic referenced element
413
- const jsonPointer = URIFragmentIdentifier.fromURIReference(toValue(linkElement.operationRef));
414
- const retrievalURI = this.toBaseURI(toValue(linkElement.operationRef));
415
- const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
416
- const isExternalReference = !isInternalReference;
417
-
418
- // ignore resolving internal Operation Object reference
419
- if (!this.options.resolve.internal && isInternalReference) {
420
- // skip traversing this Link element but traverse all it's child elements
421
- return;
422
- }
423
- // ignore resolving external Operation Object reference
424
- if (!this.options.resolve.external && isExternalReference) {
425
- // skip traversing this Link element but traverse all it's child elements
507
+ try {
508
+ let operationElement;
509
+ if (isStringElement(linkElement.operationRef)) {
510
+ // possibly non-semantic referenced element
511
+ const jsonPointer = URIFragmentIdentifier.fromURIReference(toValue(linkElement.operationRef));
512
+ const retrievalURI = this.toBaseURI(toValue(linkElement.operationRef));
513
+ const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
514
+ const isExternalReference = !isInternalReference;
515
+
516
+ // ignore resolving internal Operation Object reference
517
+ if (!this.options.resolve.internal && isInternalReference) {
518
+ // skip traversing this Link element but traverse all it's child elements
519
+ return;
520
+ }
521
+ // ignore resolving external Operation Object reference
522
+ if (!this.options.resolve.external && isExternalReference) {
523
+ // skip traversing this Link element but traverse all it's child elements
524
+ return;
525
+ }
526
+ const reference = await this.toReference(toValue(linkElement.operationRef));
527
+ operationElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
528
+ // applying semantics to a referenced element
529
+ if (!isOperationElement(operationElement)) {
530
+ if (this.refractCache.has(operationElement)) {
531
+ operationElement = this.refractCache.get(operationElement);
532
+ } else {
533
+ const sourceElement = operationElement;
534
+ operationElement = refractOperation(operationElement);
535
+ this.refractCache.set(sourceElement, operationElement);
536
+ }
537
+ }
538
+ // create shallow clone to be able to annotate with metadata
539
+ operationElement = cloneShallow(operationElement);
540
+ // annotate operation element with info about origin and type
541
+ operationElement.meta.set('ref-origin', reference.uri);
542
+ operationElement.meta.set('ref-type', linkElement.element);
543
+ const linkElementCopy = cloneShallow(linkElement);
544
+ linkElementCopy.operationRef?.meta.set('operation', operationElement);
545
+
546
+ /**
547
+ * Transclude Link Object containing Operation Object in its meta.
548
+ */
549
+ path.replaceWith(linkElementCopy);
426
550
  return;
427
551
  }
428
- const reference = await this.toReference(toValue(linkElement.operationRef));
429
- operationElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
430
- // applying semantics to a referenced element
431
- if (isPrimitiveElement(operationElement)) {
432
- const cacheKey = `operation-${identityManager.identify(operationElement)}`;
433
- if (this.refractCache.has(cacheKey)) {
434
- operationElement = this.refractCache.get(cacheKey);
435
- } else {
436
- operationElement = refractOperation(operationElement);
437
- this.refractCache.set(cacheKey, operationElement);
552
+ if (isStringElement(linkElement.operationId)) {
553
+ const operationId = toValue(linkElement.operationId);
554
+ const reference = await this.toReference(url.unsanitize(this.reference.uri));
555
+ operationElement = find(reference.value.result, e => isOperationElement(e) && isElement(e.operationId) && e.operationId.equals(operationId));
556
+ // OperationElement not found by its operationId
557
+ if (isUndefined(operationElement)) {
558
+ throw new ApiDOMStructuredError(`OperationElement(operationId=${operationId}) not found`, {
559
+ operationId
560
+ });
438
561
  }
439
- }
440
- // create shallow clone to be able to annotate with metadata
441
- operationElement = cloneShallow(operationElement);
442
- // annotate operation element with info about origin
443
- operationElement.meta.set('ref-origin', reference.uri);
444
- const linkElementCopy = cloneShallow(linkElement);
445
- linkElementCopy.operationRef?.meta.set('operation', operationElement);
562
+ const linkElementCopy = cloneShallow(linkElement);
563
+ linkElementCopy.operationId?.meta.set('operation', operationElement);
446
564
 
447
- /**
448
- * Transclude Link Object containing Operation Object in its meta.
449
- */
450
- path.replaceWith(linkElementCopy);
451
- return;
452
- }
453
- if (isStringElement(linkElement.operationId)) {
454
- const operationId = toValue(linkElement.operationId);
455
- const reference = await this.toReference(url.unsanitize(this.reference.uri));
456
- operationElement = find(reference.value.result, e => isOperationElement(e) && isElement(e.operationId) && e.operationId.equals(operationId));
457
- // OperationElement not found by its operationId
458
- if (isUndefined(operationElement)) {
459
- throw new ApiDOMError(`OperationElement(operationId=${operationId}) not found`);
565
+ /**
566
+ * Transclude Link Object containing Operation Object in its meta.
567
+ */
568
+ path.replaceWith(linkElementCopy);
460
569
  }
461
- const linkElementCopy = cloneShallow(linkElement);
462
- linkElementCopy.operationId?.meta.set('operation', operationElement);
463
-
464
- /**
465
- * Transclude Link Object containing Operation Object in its meta.
466
- */
467
- path.replaceWith(linkElementCopy);
570
+ } catch (error) {
571
+ const refFieldName = isStringElement(linkElement.operationRef) ? 'operationRef' : 'operationId';
572
+ const refFieldValue = isStringElement(linkElement.operationRef) ? toValue(linkElement.operationRef) : toValue(linkElement.operationId);
573
+ this.handleError(`Error while dereferencing Link Object. Cannot resolve ${refFieldName} "${refFieldValue}": ${error.message}`, error, linkElement, refFieldName, refFieldValue, path);
468
574
  }
469
575
  }
470
576
  async ExampleElement(path) {
@@ -477,7 +583,10 @@ class OpenAPI3_1DereferenceVisitor {
477
583
 
478
584
  // value and externalValue fields are mutually exclusive
479
585
  if (exampleElement.hasKey('value') && isStringElement(exampleElement.externalValue)) {
480
- throw new ApiDOMError('ExampleElement value and externalValue fields are mutually exclusive');
586
+ throw new ApiDOMStructuredError('ExampleElement value and externalValue fields are mutually exclusive', {
587
+ value: toValue(exampleElement.value),
588
+ externalValue: toValue(exampleElement.externalValue)
589
+ });
481
590
  }
482
591
  const retrievalURI = this.toBaseURI(toValue(exampleElement.externalValue));
483
592
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
@@ -493,19 +602,25 @@ class OpenAPI3_1DereferenceVisitor {
493
602
  // skip traversing this Example element but traverse all it's child elements
494
603
  return;
495
604
  }
496
- const reference = await this.toReference(toValue(exampleElement.externalValue));
497
-
498
- // shallow clone of the referenced element
499
- const valueElement = cloneShallow(reference.value.result);
500
- // annotate operation element with info about origin
501
- valueElement.meta.set('ref-origin', reference.uri);
502
- const exampleElementCopy = cloneShallow(exampleElement);
503
- exampleElementCopy.value = valueElement;
504
-
505
- /**
506
- * Transclude Example Object containing external value.
507
- */
508
- path.replaceWith(exampleElementCopy);
605
+ try {
606
+ const reference = await this.toReference(toValue(exampleElement.externalValue));
607
+
608
+ // shallow clone of the referenced element
609
+ const valueElement = cloneShallow(reference.value.result);
610
+ // annotate element with info about origin and type
611
+ valueElement.meta.set('ref-origin', reference.uri);
612
+ valueElement.meta.set('ref-type', exampleElement.element);
613
+ const exampleElementCopy = cloneShallow(exampleElement);
614
+ exampleElementCopy.value = valueElement;
615
+
616
+ /**
617
+ * Transclude Example Object containing external value.
618
+ */
619
+ path.replaceWith(exampleElementCopy);
620
+ } catch (error) {
621
+ const externalValue = toValue(exampleElement.externalValue);
622
+ this.handleError(`Error while dereferencing Example Object. Cannot resolve externalValue "${externalValue}": ${error.message}`, error, exampleElement, 'externalValue', externalValue, path);
623
+ }
509
624
  }
510
625
  async SchemaElement(path) {
511
626
  const referencingElement = path.node;
@@ -515,83 +630,38 @@ class OpenAPI3_1DereferenceVisitor {
515
630
  return;
516
631
  }
517
632
 
518
- // skip current referencing element as it's already been access
633
+ // skip current referencing element as it's already been accessed
519
634
  if (this.indirections.includes(referencingElement)) {
520
635
  path.skip();
521
636
  return;
522
637
  }
523
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
524
-
525
- // compute baseURI using rules around $id and $ref keywords
526
- let reference = await this.toReference(url.unsanitize(this.reference.uri));
527
- let {
528
- uri: retrievalURI
529
- } = reference;
530
- const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement);
531
- const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
532
- const file = new File({
533
- uri: $refBaseURIStrippedHash
534
- });
535
- const isUnknownURI = none(r => r.canRead(file), this.options.resolve.resolvers);
536
- const isURL = !isUnknownURI;
537
- let isInternalReference = url.stripHash(this.reference.uri) === $refBaseURI;
538
- let isExternalReference = !isInternalReference;
539
-
540
- // determining reference, proper evaluation and selection mechanism
541
- let referencedElement;
638
+ const indirectionsSize = this.indirections.length;
542
639
  try {
543
- if (isUnknownURI || isURL) {
544
- // we're dealing with canonical URI or URL with possible fragment
545
- retrievalURI = this.toBaseURI($refBaseURI);
546
- const selector = $refBaseURI;
547
- const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result);
548
- referencedElement = uriEvaluate(selector, referenceAsSchema);
549
- referencedElement = maybeRefractToSchemaElement(referencedElement);
550
- referencedElement.id = identityManager.identify(referencedElement);
551
-
552
- // ignore resolving internal Schema Objects
553
- if (!this.options.resolve.internal && isInternalReference) {
554
- // skip traversing this schema element but traverse all it's child elements
555
- return;
556
- }
557
- // ignore resolving external Schema Objects
558
- if (!this.options.resolve.external && isExternalReference) {
559
- // skip traversing this schema element but traverse all it's child elements
560
- return;
561
- }
562
- } else {
563
- // we're assuming here that we're dealing with JSON Pointer here
564
- retrievalURI = this.toBaseURI($refBaseURI);
565
- isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
566
- isExternalReference = !isInternalReference;
567
-
568
- // ignore resolving internal Schema Objects
569
- if (!this.options.resolve.internal && isInternalReference) {
570
- // skip traversing this schema element but traverse all it's child elements
571
- return;
572
- }
573
- // ignore resolving external Schema Objects
574
- if (!this.options.resolve.external && isExternalReference) {
575
- // skip traversing this schema element but traverse all it's child elements
576
- return;
577
- }
578
- reference = await this.toReference(url.unsanitize($refBaseURI));
579
- const selector = URIFragmentIdentifier.fromURIReference($refBaseURI);
580
- const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result);
581
- referencedElement = jsonPointerEvaluate(referenceAsSchema, selector);
582
- referencedElement = maybeRefractToSchemaElement(referencedElement);
583
- referencedElement.id = identityManager.identify(referencedElement);
584
- }
585
- } catch (error) {
586
- /**
587
- * SchemaElement($id=URL) was not found, so we're going to try to resolve
588
- * the URL and assume the returned response is a JSON Schema.
589
- */
590
- if (isURL && error instanceof EvaluationJsonSchemaUriError) {
591
- if (isAnchor(uriToAnchor($refBaseURI))) {
592
- // we're dealing with JSON Schema $anchor here
593
- isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
594
- isExternalReference = !isInternalReference;
640
+ // compute baseURI using rules around $id and $ref keywords
641
+ let reference = await this.toReference(url.unsanitize(this.reference.uri));
642
+ let {
643
+ uri: retrievalURI
644
+ } = reference;
645
+ const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement);
646
+ const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
647
+ const file = new File({
648
+ uri: $refBaseURIStrippedHash
649
+ });
650
+ const isUnknownURI = none(r => r.canRead(file), this.options.resolve.resolvers);
651
+ const isURL = !isUnknownURI;
652
+ let isInternalReference = url.stripHash(this.reference.uri) === $refBaseURI;
653
+ let isExternalReference = !isInternalReference;
654
+
655
+ // determining reference, proper evaluation and selection mechanism
656
+ let referencedElement;
657
+ try {
658
+ if (isUnknownURI || isURL) {
659
+ // we're dealing with canonical URI or URL with possible fragment
660
+ retrievalURI = this.toBaseURI($refBaseURI);
661
+ const selector = $refBaseURI;
662
+ const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result);
663
+ referencedElement = uriEvaluate(selector, referenceAsSchema);
664
+ referencedElement = maybeRefractToSchemaElement(referencedElement);
595
665
 
596
666
  // ignore resolving internal Schema Objects
597
667
  if (!this.options.resolve.internal && isInternalReference) {
@@ -603,12 +673,6 @@ class OpenAPI3_1DereferenceVisitor {
603
673
  // skip traversing this schema element but traverse all it's child elements
604
674
  return;
605
675
  }
606
- reference = await this.toReference(url.unsanitize($refBaseURI));
607
- const selector = uriToAnchor($refBaseURI);
608
- const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result);
609
- referencedElement = $anchorEvaluate(selector, referenceAsSchema);
610
- referencedElement = maybeRefractToSchemaElement(referencedElement);
611
- referencedElement.id = identityManager.identify(referencedElement);
612
676
  } else {
613
677
  // we're assuming here that we're dealing with JSON Pointer here
614
678
  retrievalURI = this.toBaseURI($refBaseURI);
@@ -630,118 +694,167 @@ class OpenAPI3_1DereferenceVisitor {
630
694
  const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result);
631
695
  referencedElement = jsonPointerEvaluate(referenceAsSchema, selector);
632
696
  referencedElement = maybeRefractToSchemaElement(referencedElement);
633
- referencedElement.id = identityManager.identify(referencedElement);
634
697
  }
635
- } else {
636
- throw error;
698
+ } catch (error) {
699
+ /**
700
+ * SchemaElement($id=URL) was not found, so we're going to try to resolve
701
+ * the URL and assume the returned response is a JSON Schema.
702
+ */
703
+ if (isURL && error instanceof EvaluationJsonSchemaUriError) {
704
+ if (isAnchor(uriToAnchor($refBaseURI))) {
705
+ // we're dealing with JSON Schema $anchor here
706
+ isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
707
+ isExternalReference = !isInternalReference;
708
+
709
+ // ignore resolving internal Schema Objects
710
+ if (!this.options.resolve.internal && isInternalReference) {
711
+ // skip traversing this schema element but traverse all it's child elements
712
+ return;
713
+ }
714
+ // ignore resolving external Schema Objects
715
+ if (!this.options.resolve.external && isExternalReference) {
716
+ // skip traversing this schema element but traverse all it's child elements
717
+ return;
718
+ }
719
+ reference = await this.toReference(url.unsanitize($refBaseURI));
720
+ const selector = uriToAnchor($refBaseURI);
721
+ const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result);
722
+ referencedElement = $anchorEvaluate(selector, referenceAsSchema);
723
+ referencedElement = maybeRefractToSchemaElement(referencedElement);
724
+ } else {
725
+ // we're assuming here that we're dealing with JSON Pointer here
726
+ retrievalURI = this.toBaseURI($refBaseURI);
727
+ isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
728
+ isExternalReference = !isInternalReference;
729
+
730
+ // ignore resolving internal Schema Objects
731
+ if (!this.options.resolve.internal && isInternalReference) {
732
+ // skip traversing this schema element but traverse all it's child elements
733
+ return;
734
+ }
735
+ // ignore resolving external Schema Objects
736
+ if (!this.options.resolve.external && isExternalReference) {
737
+ // skip traversing this schema element but traverse all it's child elements
738
+ return;
739
+ }
740
+ reference = await this.toReference(url.unsanitize($refBaseURI));
741
+ const selector = URIFragmentIdentifier.fromURIReference($refBaseURI);
742
+ const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result);
743
+ referencedElement = jsonPointerEvaluate(referenceAsSchema, selector);
744
+ referencedElement = maybeRefractToSchemaElement(referencedElement);
745
+ }
746
+ } else {
747
+ throw error;
748
+ }
637
749
  }
638
- }
639
- this.indirections.push(referencingElement);
750
+ this.indirections.push(referencingElement);
640
751
 
641
- // detect direct or indirect reference
642
- if (referencingElement === referencedElement) {
643
- throw new ApiDOMError('Recursive Schema Object reference detected');
644
- }
752
+ // detect direct or indirect reference
753
+ if (referencingElement === referencedElement) {
754
+ throw new ApiDOMStructuredError('Recursive Schema Object reference detected', {
755
+ $ref: toValue(referencingElement.$ref)
756
+ });
757
+ }
645
758
 
646
- // detect maximum depth of dereferencing
647
- if (this.indirections.length > this.options.dereference.maxDepth) {
648
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
649
- }
759
+ // detect maximum depth of dereferencing
760
+ if (this.indirections.length > this.options.dereference.maxDepth) {
761
+ throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
762
+ maxDepth: this.options.dereference.maxDepth,
763
+ uri: this.reference.uri
764
+ });
765
+ }
766
+
767
+ // detect cross-boundary cycle
768
+ const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
769
+ if (ancestorsLineage.includes(referencedElement)) {
770
+ reference.refSet.circular = true;
771
+ if (this.options.dereference.circular === 'error') {
772
+ throw new ApiDOMStructuredError('Circular reference detected', {
773
+ $ref: toValue(referencingElement.$ref)
774
+ });
775
+ } else if (this.options.dereference.circular === 'replace') {
776
+ const refElement = new RefElement($refBaseURI, {
777
+ type: referencingElement.element,
778
+ uri: reference.uri,
779
+ $ref: toValue(referencingElement.$ref)
780
+ });
781
+ const replacer = this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
782
+ const replacement = replacer(refElement);
783
+ path.replaceWith(replacement);
784
+ return;
785
+ }
786
+ }
787
+
788
+ /**
789
+ * Dive deep into the fragment.
790
+ *
791
+ * Cases to consider:
792
+ * 1. We're crossing document boundary
793
+ * 2. Fragment is from non-entry document
794
+ * 3. Fragment is a Schema Object with $ref field. We need to follow it to get the eventual value
795
+ * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
796
+ */
797
+ const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
798
+ const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
799
+ if ((isExternalReference || isNonEntryDocument || isSchemaElement(referencedElement) && isStringElement(referencedElement.$ref) || shouldDetectCircular) && !ancestorsLineage.includesCycle(referencedElement)) {
800
+ directAncestors.add(referencingElement);
801
+ const visitor = new OpenAPI3_1DereferenceVisitor({
802
+ reference,
803
+ indirections: [...this.indirections],
804
+ options: this.options,
805
+ refractCache: this.refractCache,
806
+ ancestors: ancestorsLineage
807
+ });
808
+ referencedElement = await traverseAsync(referencedElement, visitor, {
809
+ mutable: true
810
+ });
811
+ directAncestors.delete(referencingElement);
812
+ }
650
813
 
651
- // detect second deep dive into the same fragment and avoid it
652
- if (ancestorsLineage.includes(referencedElement)) {
653
- reference.refSet.circular = true;
654
- if (this.options.dereference.circular === 'error') {
655
- throw new ApiDOMError('Circular reference detected');
656
- } else if (this.options.dereference.circular === 'replace') {
657
- const refElement = new RefElement(referencedElement.id, {
658
- type: 'json-schema',
659
- uri: reference.uri,
814
+ // Boolean JSON Schemas
815
+ if (isBooleanJSONSchemaElement(referencedElement)) {
816
+ const booleanJsonSchemaElement = cloneDeep(referencedElement);
817
+ // annotate referenced element with info about original referencing element
818
+ booleanJsonSchemaElement.meta.set('ref-fields', {
660
819
  $ref: toValue(referencingElement.$ref)
661
820
  });
662
- const replacer = this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
663
- const replacement = replacer(refElement);
664
- this.indirections.pop();
665
- path.replaceWith(replacement);
821
+ // annotate referenced element with info about origin and type
822
+ booleanJsonSchemaElement.meta.set('ref-origin', reference.uri);
823
+ booleanJsonSchemaElement.meta.set('ref-type', referencingElement.element);
824
+ path.replaceWith(booleanJsonSchemaElement);
666
825
  return;
667
826
  }
668
- }
669
-
670
- /**
671
- * Dive deep into the fragment.
672
- *
673
- * Cases to consider:
674
- * 1. We're crossing document boundary
675
- * 2. Fragment is from non-root document
676
- * 3. Fragment is a Schema Object with $ref field. We need to follow it to get the eventual value
677
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
678
- */
679
- const isNonRootDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
680
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
681
- if ((isExternalReference || isNonRootDocument || isSchemaElement(referencedElement) && isStringElement(referencedElement.$ref) || shouldDetectCircular) && !ancestorsLineage.includesCycle(referencedElement)) {
682
- // append referencing reference to ancestors lineage
683
- directAncestors.add(referencingElement);
684
- const visitor = new OpenAPI3_1DereferenceVisitor({
685
- reference,
686
- namespace: this.namespace,
687
- indirections: [...this.indirections],
688
- options: this.options,
689
- refractCache: this.refractCache,
690
- ancestors: ancestorsLineage
691
- });
692
- referencedElement = await traverseAsync(referencedElement, visitor, {
693
- mutable: true
694
- });
695
827
 
696
- // remove referencing reference from ancestors lineage
697
- directAncestors.delete(referencingElement);
698
- }
699
- this.indirections.pop();
700
-
701
- // Boolean JSON Schemas
702
- if (isBooleanJSONSchemaElement(referencedElement)) {
703
- const booleanJsonSchemaElement = cloneDeep(referencedElement);
704
- // assign unique id to merged element
705
- booleanJsonSchemaElement.meta.set('id', identityManager.generateId());
706
- // annotate referenced element with info about original referencing element
707
- booleanJsonSchemaElement.meta.set('ref-fields', {
708
- $ref: toValue(referencingElement.$ref)
709
- });
710
- // annotate referenced element with info about origin
711
- booleanJsonSchemaElement.meta.set('ref-origin', reference.uri);
712
- // annotate fragment with info about referencing element
713
- booleanJsonSchemaElement.meta.set('ref-referencing-element-id', identityManager.identify(referencingElement));
714
- path.replaceWith(booleanJsonSchemaElement);
715
- return;
716
- }
717
-
718
- /**
719
- * Creating a new version of Schema Object by merging fields from referenced Schema Object with referencing one.
720
- */
721
- if (isSchemaElement(referencedElement)) {
722
- const mergedElement = cloneShallow(referencedElement);
723
- // assign unique id to merged element
724
- mergedElement.meta.set('id', identityManager.generateId());
725
- // existing keywords from referencing schema overrides ones from referenced schema
726
- referencingElement.forEach((value, keyElement, item) => {
727
- mergedElement.remove(toValue(keyElement));
728
- mergedElement.content.push(item);
729
- });
730
- mergedElement.remove('$ref');
731
- // annotate referenced element with info about original referencing element
732
- mergedElement.meta.set('ref-fields', {
733
- $ref: toValue(referencingElement.$ref)
734
- });
735
- // annotate fragment with info about origin
736
- mergedElement.meta.set('ref-origin', reference.uri);
737
- // annotate fragment with info about referencing element
738
- mergedElement.meta.set('ref-referencing-element-id', identityManager.identify(referencingElement));
739
- referencedElement = mergedElement;
828
+ /**
829
+ * Creating a new version of Schema Object by merging fields from referenced Schema Object with referencing one.
830
+ */
831
+ if (isSchemaElement(referencedElement)) {
832
+ const mergedElement = cloneShallow(referencedElement);
833
+ // existing keywords from referencing schema overrides ones from referenced schema
834
+ referencingElement.forEach((value, keyElement, item) => {
835
+ mergedElement.remove(toValue(keyElement));
836
+ mergedElement.content.push(item);
837
+ });
838
+ mergedElement.remove('$ref');
839
+ // annotate referenced element with info about original referencing element
840
+ mergedElement.meta.set('ref-fields', {
841
+ $ref: toValue(referencingElement.$ref)
842
+ });
843
+ // annotate fragment with info about origin and type
844
+ mergedElement.meta.set('ref-origin', reference.uri);
845
+ mergedElement.meta.set('ref-type', referencingElement.element);
846
+ referencedElement = mergedElement;
847
+ }
848
+ /**
849
+ * Transclude referencing element with merged referenced element.
850
+ */
851
+ path.replaceWith(referencedElement);
852
+ } catch (error) {
853
+ const $ref = toValue(referencingElement.$ref);
854
+ this.handleError(`Error while dereferencing Schema Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
855
+ } finally {
856
+ if (this.indirections.length > indirectionsSize) this.indirections.pop();
740
857
  }
741
- /**
742
- * Transclude referencing element with merged referenced element.
743
- */
744
- path.replaceWith(referencedElement);
745
858
  }
746
859
  }
747
860
  export default OpenAPI3_1DereferenceVisitor;