@speclynx/apidom-reference 4.0.3 → 4.0.5

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 (95) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/apidom-reference.browser.js +43 -19
  3. package/dist/apidom-reference.browser.min.js +1 -1
  4. package/package.json +27 -26
  5. package/src/File.ts +0 -63
  6. package/src/Reference.ts +0 -38
  7. package/src/ReferenceSet.ts +0 -73
  8. package/src/bundle/index.ts +0 -57
  9. package/src/bundle/strategies/BundleStrategy.ts +0 -27
  10. package/src/bundle/strategies/openapi-3-1/index.ts +0 -57
  11. package/src/configuration/empty.ts +0 -1
  12. package/src/configuration/saturated.ts +0 -72
  13. package/src/dereference/index.ts +0 -96
  14. package/src/dereference/strategies/DereferenceStrategy.ts +0 -27
  15. package/src/dereference/strategies/apidom/index.ts +0 -128
  16. package/src/dereference/strategies/apidom/selectors/element-id.ts +0 -48
  17. package/src/dereference/strategies/apidom/visitor.ts +0 -316
  18. package/src/dereference/strategies/arazzo-1/index.ts +0 -158
  19. package/src/dereference/strategies/arazzo-1/selectors/$anchor.ts +0 -9
  20. package/src/dereference/strategies/arazzo-1/selectors/uri.ts +0 -5
  21. package/src/dereference/strategies/arazzo-1/source-descriptions.ts +0 -317
  22. package/src/dereference/strategies/arazzo-1/util.ts +0 -33
  23. package/src/dereference/strategies/arazzo-1/visitor.ts +0 -574
  24. package/src/dereference/strategies/asyncapi-2/index.ts +0 -133
  25. package/src/dereference/strategies/asyncapi-2/visitor.ts +0 -589
  26. package/src/dereference/strategies/openapi-2/index.ts +0 -136
  27. package/src/dereference/strategies/openapi-2/visitor.ts +0 -745
  28. package/src/dereference/strategies/openapi-3-0/index.ts +0 -134
  29. package/src/dereference/strategies/openapi-3-0/visitor.ts +0 -760
  30. package/src/dereference/strategies/openapi-3-1/index.ts +0 -141
  31. package/src/dereference/strategies/openapi-3-1/selectors/$anchor.ts +0 -64
  32. package/src/dereference/strategies/openapi-3-1/selectors/uri.ts +0 -54
  33. package/src/dereference/strategies/openapi-3-1/util.ts +0 -83
  34. package/src/dereference/strategies/openapi-3-1/visitor.ts +0 -1053
  35. package/src/dereference/util.ts +0 -29
  36. package/src/errors/BundleError.ts +0 -8
  37. package/src/errors/DereferenceError.ts +0 -8
  38. package/src/errors/EvaluationElementIdError.ts +0 -8
  39. package/src/errors/EvaluationJsonSchema$anchorError.ts +0 -8
  40. package/src/errors/EvaluationJsonSchemaUriError.ts +0 -8
  41. package/src/errors/InvalidJsonSchema$anchorError.ts +0 -12
  42. package/src/errors/JsonSchema$anchorError.ts +0 -8
  43. package/src/errors/JsonSchemaUriError.ts +0 -8
  44. package/src/errors/MaximumBundleDepthError.ts +0 -8
  45. package/src/errors/MaximumDereferenceDepthError.ts +0 -8
  46. package/src/errors/MaximumResolveDepthError.ts +0 -8
  47. package/src/errors/ParseError.ts +0 -8
  48. package/src/errors/ParserError.ts +0 -8
  49. package/src/errors/PluginError.ts +0 -15
  50. package/src/errors/ResolveError.ts +0 -8
  51. package/src/errors/ResolverError.ts +0 -8
  52. package/src/errors/UnmatchedBundleStrategyError.ts +0 -8
  53. package/src/errors/UnmatchedDereferenceStrategyError.ts +0 -8
  54. package/src/errors/UnmatchedParserError.ts +0 -8
  55. package/src/errors/UnmatchedResolveStrategyError.ts +0 -8
  56. package/src/errors/UnmatchedResolverError.ts +0 -8
  57. package/src/errors/UnresolvableReferenceError.ts +0 -8
  58. package/src/index.ts +0 -135
  59. package/src/options/index.ts +0 -239
  60. package/src/options/util.ts +0 -22
  61. package/src/parse/index.ts +0 -67
  62. package/src/parse/parsers/Parser.ts +0 -80
  63. package/src/parse/parsers/apidom-json/index.ts +0 -78
  64. package/src/parse/parsers/arazzo-json-1/index.ts +0 -76
  65. package/src/parse/parsers/arazzo-json-1/source-descriptions.ts +0 -280
  66. package/src/parse/parsers/arazzo-yaml-1/index.ts +0 -77
  67. package/src/parse/parsers/arazzo-yaml-1/source-descriptions.ts +0 -16
  68. package/src/parse/parsers/asyncapi-json-2/index.ts +0 -58
  69. package/src/parse/parsers/asyncapi-yaml-2/index.ts +0 -58
  70. package/src/parse/parsers/binary/index-browser.ts +0 -60
  71. package/src/parse/parsers/binary/index-node.ts +0 -57
  72. package/src/parse/parsers/json/index.ts +0 -52
  73. package/src/parse/parsers/openapi-json-2/index.ts +0 -58
  74. package/src/parse/parsers/openapi-json-3-0/index.ts +0 -59
  75. package/src/parse/parsers/openapi-json-3-1/index.ts +0 -59
  76. package/src/parse/parsers/openapi-yaml-2/index.ts +0 -58
  77. package/src/parse/parsers/openapi-yaml-3-0/index.ts +0 -59
  78. package/src/parse/parsers/openapi-yaml-3-1/index.ts +0 -59
  79. package/src/parse/parsers/yaml-1-2/index.ts +0 -60
  80. package/src/resolve/index.ts +0 -75
  81. package/src/resolve/resolvers/HTTPResolver.ts +0 -58
  82. package/src/resolve/resolvers/Resolver.ts +0 -25
  83. package/src/resolve/resolvers/file/index-browser.ts +0 -24
  84. package/src/resolve/resolvers/file/index-node.ts +0 -55
  85. package/src/resolve/resolvers/http-axios/cache/MemoryCache.ts +0 -46
  86. package/src/resolve/resolvers/http-axios/index.ts +0 -130
  87. package/src/resolve/strategies/ResolveStrategy.ts +0 -26
  88. package/src/resolve/strategies/apidom/index.ts +0 -78
  89. package/src/resolve/strategies/asyncapi-2/index.ts +0 -78
  90. package/src/resolve/strategies/openapi-2/index.ts +0 -78
  91. package/src/resolve/strategies/openapi-3-0/index.ts +0 -78
  92. package/src/resolve/strategies/openapi-3-1/index.ts +0 -78
  93. package/src/resolve/util.ts +0 -39
  94. package/src/util/plugins.ts +0 -37
  95. package/src/util/url.ts +0 -285
@@ -1,745 +0,0 @@
1
- import { propEq } from 'ramda';
2
- import {
3
- Element,
4
- RefElement,
5
- ParseResultElement,
6
- isStringElement,
7
- isElement,
8
- cloneShallow,
9
- cloneDeep,
10
- } from '@speclynx/apidom-datamodel';
11
- import { toValue, toYAML } from '@speclynx/apidom-core';
12
- import { ApiDOMStructuredError } from '@speclynx/apidom-error';
13
- import { traverse, traverseAsync, type Path } from '@speclynx/apidom-traverse';
14
- import { evaluate, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
15
- import {
16
- isReferenceElement,
17
- isJSONReferenceElement,
18
- isPathItemElement,
19
- isReferenceLikeElement,
20
- isJSONReferenceLikeElement,
21
- ReferenceElement,
22
- PathItemElement,
23
- JSONReferenceElement,
24
- refract,
25
- refractReference,
26
- refractPathItem,
27
- refractJSONReference,
28
- } from '@speclynx/apidom-ns-openapi-2';
29
-
30
- import UnresolvableReferenceError from '../../../errors/UnresolvableReferenceError.ts';
31
- import MaximumDereferenceDepthError from '../../../errors/MaximumDereferenceDepthError.ts';
32
- import MaximumResolveDepthError from '../../../errors/MaximumResolveDepthError.ts';
33
- import { AncestorLineage } from '../../util.ts';
34
- import * as url from '../../../util/url.ts';
35
- import parse from '../../../parse/index.ts';
36
- import Reference from '../../../Reference.ts';
37
- import ReferenceSet from '../../../ReferenceSet.ts';
38
- import type { ReferenceOptions } from '../../../options/index.ts';
39
-
40
- /**
41
- * @public
42
- */
43
- export interface OpenAPI2DereferenceVisitorOptions {
44
- readonly reference: Reference;
45
- readonly options: ReferenceOptions;
46
- readonly indirections?: Element[];
47
- readonly refractCache?: WeakMap<Element, Element>;
48
- readonly ancestors?: AncestorLineage<Element>;
49
- }
50
-
51
- /**
52
- * @public
53
- */
54
- class OpenAPI2DereferenceVisitor {
55
- protected readonly indirections: Element[];
56
-
57
- protected readonly reference: Reference;
58
-
59
- protected readonly options: ReferenceOptions;
60
-
61
- protected readonly refractCache: WeakMap<Element, Element>;
62
-
63
- /**
64
- * Tracks element ancestors across dive-deep traversal boundaries.
65
- * Used for cycle detection: if a referenced element is found in
66
- * the ancestor lineage, a circular reference is detected.
67
- */
68
- protected readonly ancestors: AncestorLineage<Element>;
69
-
70
- constructor({
71
- reference,
72
- options,
73
- indirections = [],
74
- ancestors = new AncestorLineage(),
75
- refractCache = new WeakMap(),
76
- }: OpenAPI2DereferenceVisitorOptions) {
77
- this.indirections = indirections;
78
- this.reference = reference;
79
- this.options = options;
80
- this.refractCache = refractCache;
81
- this.ancestors = new AncestorLineage(...ancestors);
82
- }
83
-
84
- protected toAncestorLineage(path: Path<Element>): [AncestorLineage<Element>, Set<Element>] {
85
- const ancestorNodes = path.getAncestorNodes();
86
- const directAncestors = new Set<Element>(ancestorNodes.filter(isElement));
87
- const ancestorsLineage = new AncestorLineage<Element>(...this.ancestors, directAncestors);
88
- return [ancestorsLineage, directAncestors];
89
- }
90
-
91
- protected toBaseURI(uri: string): string {
92
- return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
93
- }
94
-
95
- protected async toReference(uri: string): Promise<Reference> {
96
- // detect maximum depth of resolution
97
- if (this.reference.depth >= this.options.resolve.maxDepth) {
98
- throw new MaximumResolveDepthError(
99
- `Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`,
100
- { maxDepth: this.options.resolve.maxDepth, uri: this.reference.uri },
101
- );
102
- }
103
-
104
- const baseURI = this.toBaseURI(uri);
105
- const { refSet } = this.reference as { refSet: ReferenceSet };
106
-
107
- // we've already processed this Reference in past
108
- if (refSet.has(baseURI)) {
109
- return refSet.find(propEq(baseURI, 'uri'))!;
110
- }
111
-
112
- const parseResult = await parse(url.unsanitize(baseURI), {
113
- ...this.options,
114
- parse: { ...this.options.parse, mediaType: 'text/plain' },
115
- });
116
-
117
- // register new mutable reference with a refSet
118
- //
119
- // NOTE(known limitation): the mutable reference is mutated in place during traversal
120
- // (via `{ mutable: true }`). When an external document evaluates a JSON pointer back
121
- // into this document, it may receive an already-resolved element instead of the original
122
- // $ref. That resolved element was produced using the entry document's resolution context
123
- // (ancestors, indirections), which may differ from the external document's context.
124
- // This can affect cycle detection in rare cross-document circular reference patterns.
125
- //
126
- // Remediation: evaluate JSON pointers against the immutable (original) parse tree
127
- // instead of the mutable working copy. The `immutable://` reference below preserves
128
- // the original tree and could be used for pointer evaluation, ensuring every resolution
129
- // context always sees raw, unresolved elements and processes them with its own
130
- // ancestors/indirections. The trade-off is that elements referenced by multiple
131
- // documents would be resolved once per context instead of being reused.
132
- const mutableReference = new Reference({
133
- uri: baseURI,
134
- value: this.options.dereference.immutable ? cloneDeep(parseResult) : parseResult,
135
- depth: this.reference.depth + 1,
136
- });
137
- refSet.add(mutableReference);
138
-
139
- if (this.options.dereference.immutable) {
140
- // register new immutable reference with a refSet
141
- const immutableReference = new Reference({
142
- uri: `immutable://${baseURI}`,
143
- value: parseResult,
144
- depth: this.reference.depth + 1,
145
- });
146
- refSet.add(immutableReference);
147
- }
148
-
149
- return mutableReference;
150
- }
151
-
152
- /**
153
- * Handles an error according to the continueOnError option.
154
- *
155
- * For new errors: wraps in UnresolvableReferenceError with structured context
156
- * (type, uri, location, codeFrame, refFieldName, refFieldValue, trace).
157
- * For errors already wrapped by a nested visitor: prepends the current hop to the trace.
158
- *
159
- * Inner/intermediate visitors always throw to let the trace accumulate.
160
- * Only the entry document visitor respects continueOnError (callback/swallow/throw).
161
- */
162
- protected handleError(
163
- message: string,
164
- error: Error,
165
- referencingElement: Element,
166
- refFieldName: string,
167
- refFieldValue: string,
168
- visitorPath: Path<Element>,
169
- ): void {
170
- const { continueOnError } = this.options.dereference;
171
- const isEntryDocument =
172
- url.stripHash(this.reference.refSet?.rootRef?.uri ?? '') === this.reference.uri;
173
- const uri = this.reference.uri;
174
- const type = referencingElement.element as string;
175
- const codeFrame = toYAML(referencingElement);
176
-
177
- // find element location by identity in the document tree.
178
- // guarded: this.reference.value may not be a ParseResultElement or may lack a result.
179
- // falls back to visitorPath which may produce an incomplete path when
180
- // dereferenceApiDOM is called with a fragment (cloneShallow creates a new root identity).
181
- let location: string | undefined;
182
- const root = (this.reference.value as ParseResultElement).result;
183
- if (isElement(root)) {
184
- traverse(root, {
185
- enter: (p: Path<Element>) => {
186
- if (
187
- p.node === referencingElement ||
188
- this.refractCache.get(p.node) === referencingElement
189
- ) {
190
- location = p.formatPath();
191
- p.stop();
192
- }
193
- },
194
- });
195
- }
196
- location ??= visitorPath.formatPath();
197
-
198
- const hop = { uri, type, refFieldName, refFieldValue, location, codeFrame };
199
-
200
- // enrich existing error from nested visitor or create new one
201
- let unresolvedError: UnresolvableReferenceError;
202
- if (error instanceof UnresolvableReferenceError) {
203
- // prefix relative locations for entries belonging to the referenced document
204
- const refBaseURI = this.toBaseURI(refFieldValue);
205
- const fragment = URIFragmentIdentifier.fromURIReference(refFieldValue);
206
- if (fragment) {
207
- if (refBaseURI === (error as any).uri && (error as any).location) {
208
- (error as any).location = fragment + (error as any).location;
209
- }
210
- for (const h of (error as any).trace) {
211
- if (h.uri === refBaseURI && h.location) h.location = fragment + h.location;
212
- }
213
- }
214
- // @ts-ignore
215
- error.trace = [hop, ...error.trace];
216
- unresolvedError = error;
217
- } else {
218
- unresolvedError = new UnresolvableReferenceError(message, {
219
- cause: error,
220
- type,
221
- uri,
222
- location,
223
- codeFrame,
224
- refFieldName,
225
- refFieldValue,
226
- trace: [],
227
- });
228
- }
229
-
230
- if (!isEntryDocument || continueOnError === false) throw unresolvedError;
231
- if (typeof continueOnError === 'function') continueOnError(unresolvedError);
232
- }
233
-
234
- public async ReferenceElement(path: Path<Element>) {
235
- const referencingElement = path.node as ReferenceElement;
236
-
237
- // skip current referencing element as it's already been access
238
- if (this.indirections.includes(referencingElement)) {
239
- path.skip();
240
- return;
241
- }
242
-
243
- const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref) as string);
244
- const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
245
- const isExternalReference = !isInternalReference;
246
-
247
- // ignore resolving internal Reference Objects
248
- if (!this.options.resolve.internal && isInternalReference) {
249
- // skip traversing this reference element and all it's child elements
250
- path.skip();
251
- return;
252
- }
253
- // ignore resolving external Reference Objects
254
- if (!this.options.resolve.external && isExternalReference) {
255
- // skip traversing this reference element and all it's child elements
256
- path.skip();
257
- return;
258
- }
259
-
260
- const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref) as string);
261
- const indirectionsSize = this.indirections.length;
262
-
263
- try {
264
- const reference = await this.toReference(toValue(referencingElement.$ref) as string);
265
-
266
- this.indirections.push(referencingElement);
267
-
268
- const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
269
-
270
- // possibly non-semantic fragment
271
- let referencedElement = evaluate<Element>(
272
- (reference.value as ParseResultElement).result as Element,
273
- jsonPointer,
274
- );
275
-
276
- // applying semantics to a fragment
277
- const referencedElementType = referencingElement.meta.get('referenced-element') as string;
278
- if (
279
- referencedElement.element !== referencedElementType &&
280
- !isReferenceElement(referencedElement)
281
- ) {
282
- if (this.refractCache.has(referencedElement)) {
283
- referencedElement = this.refractCache.get(referencedElement)!;
284
- } else if (isReferenceLikeElement(referencedElement)) {
285
- // handling generic indirect references
286
- const sourceElement = referencedElement;
287
- referencedElement = refractReference(referencedElement);
288
- referencedElement.meta.set('referenced-element', referencedElementType);
289
- this.refractCache.set(sourceElement, referencedElement);
290
- } else {
291
- // handling direct references
292
- const sourceElement = referencedElement;
293
- referencedElement = refract(referencedElement, { element: referencedElementType });
294
- this.refractCache.set(sourceElement, referencedElement);
295
- }
296
- }
297
-
298
- // detect direct or indirect reference
299
- if (referencingElement === referencedElement) {
300
- throw new ApiDOMStructuredError('Recursive Reference Object detected', {
301
- $ref: toValue(referencingElement.$ref),
302
- });
303
- }
304
-
305
- // detect maximum depth of dereferencing
306
- if (this.indirections.length > this.options.dereference.maxDepth) {
307
- throw new MaximumDereferenceDepthError(
308
- `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`,
309
- { maxDepth: this.options.dereference.maxDepth, uri: this.reference.uri },
310
- );
311
- }
312
-
313
- // detect cross-boundary cycle
314
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
315
- if (ancestorsLineage.includes(referencedElement)) {
316
- reference.refSet!.circular = true;
317
-
318
- if (this.options.dereference.circular === 'error') {
319
- throw new ApiDOMStructuredError('Circular reference detected', {
320
- $ref: toValue(referencingElement.$ref),
321
- });
322
- } else if (this.options.dereference.circular === 'replace') {
323
- const refElement = new RefElement($refBaseURI, {
324
- type: referencingElement.element,
325
- uri: reference.uri,
326
- $ref: toValue(referencingElement.$ref),
327
- });
328
- const replacer =
329
- this.options.dereference.strategyOpts['openapi-2']?.circularReplacer ??
330
- this.options.dereference.circularReplacer;
331
- const replacement = replacer(refElement);
332
-
333
- path.replaceWith(replacement);
334
- return;
335
- }
336
- }
337
-
338
- /**
339
- * Dive deep into the fragment.
340
- *
341
- * Cases to consider:
342
- * 1. We're crossing document boundary
343
- * 2. Fragment is from non-entry document
344
- * 3. Fragment is a Reference Object. We need to follow it to get the eventual value
345
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
346
- */
347
- const isNonEntryDocument = url.stripHash(reference.refSet!.rootRef!.uri) !== reference.uri;
348
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
349
- if (
350
- (isExternalReference ||
351
- isNonEntryDocument ||
352
- isReferenceElement(referencedElement) ||
353
- shouldDetectCircular) &&
354
- !ancestorsLineage.includesCycle(referencedElement)
355
- ) {
356
- // append referencing reference to ancestors lineage
357
- directAncestors.add(referencingElement);
358
-
359
- const visitor = new OpenAPI2DereferenceVisitor({
360
- reference,
361
- indirections: [...this.indirections],
362
- options: this.options,
363
- refractCache: this.refractCache,
364
- ancestors: ancestorsLineage,
365
- });
366
- referencedElement = await traverseAsync(referencedElement, visitor, { mutable: true });
367
-
368
- directAncestors.delete(referencingElement);
369
- }
370
-
371
- /**
372
- * Creating a new version of referenced element to avoid modifying the original one.
373
- */
374
- const mergedElement = cloneShallow(referencedElement);
375
- // annotate referenced element with info about original referencing element
376
- mergedElement.meta.set('ref-fields', {
377
- $ref: toValue(referencingElement.$ref),
378
- });
379
- // annotate fragment with info about origin
380
- mergedElement.meta.set('ref-origin', reference.uri);
381
- mergedElement.meta.set('ref-type', referencingElement.element);
382
-
383
- /**
384
- * Transclude referencing element with merged referenced element.
385
- */
386
- path.replaceWith(mergedElement);
387
- } catch (error: unknown) {
388
- const $ref = toValue(referencingElement.$ref) as string;
389
- this.handleError(
390
- `Error while dereferencing Reference Object. Cannot resolve $ref "${$ref}": ${(error as Error).message}`,
391
- error as Error,
392
- referencingElement,
393
- '$ref',
394
- $ref,
395
- path,
396
- );
397
- } finally {
398
- if (this.indirections.length > indirectionsSize) this.indirections.pop();
399
- }
400
- }
401
-
402
- public async PathItemElement(path: Path<Element>) {
403
- const referencingElement = path.node as PathItemElement;
404
-
405
- // ignore PathItemElement without $ref field
406
- if (!isStringElement(referencingElement.$ref)) {
407
- return;
408
- }
409
-
410
- // skip current referencing element as it's already been access
411
- if (this.indirections.includes(referencingElement)) {
412
- path.skip();
413
- return;
414
- }
415
-
416
- const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref) as string);
417
- const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
418
- const isExternalReference = !isInternalReference;
419
-
420
- // ignore resolving internal Path Item Objects
421
- if (!this.options.resolve.internal && isInternalReference) {
422
- // skip traversing this Path Item element but traverse all it's child elements
423
- return;
424
- }
425
- // ignore resolving external Path Item Objects
426
- if (!this.options.resolve.external && isExternalReference) {
427
- // skip traversing this Path Item element but traverse all it's child elements
428
- return;
429
- }
430
-
431
- const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref) as string);
432
- const indirectionsSize = this.indirections.length;
433
-
434
- try {
435
- const reference = await this.toReference(toValue(referencingElement.$ref) as string);
436
-
437
- this.indirections.push(referencingElement);
438
-
439
- const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
440
-
441
- // possibly non-semantic referenced element
442
- let referencedElement = evaluate<Element>(
443
- (reference.value as ParseResultElement).result as Element,
444
- jsonPointer,
445
- );
446
-
447
- // applying semantics to a referenced element
448
- if (!isPathItemElement(referencedElement)) {
449
- if (this.refractCache.has(referencedElement)) {
450
- referencedElement = this.refractCache.get(referencedElement)!;
451
- } else {
452
- const sourceElement = referencedElement;
453
- referencedElement = refractPathItem(referencedElement);
454
- this.refractCache.set(sourceElement, referencedElement);
455
- }
456
- }
457
-
458
- // detect direct or indirect reference
459
- if (referencingElement === referencedElement) {
460
- throw new ApiDOMStructuredError('Recursive Path Item Object reference detected', {
461
- $ref: toValue(referencingElement.$ref),
462
- });
463
- }
464
-
465
- // detect maximum depth of dereferencing
466
- if (this.indirections.length > this.options.dereference.maxDepth) {
467
- throw new MaximumDereferenceDepthError(
468
- `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`,
469
- { maxDepth: this.options.dereference.maxDepth, uri: this.reference.uri },
470
- );
471
- }
472
-
473
- // detect cross-boundary cycle
474
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
475
- if (ancestorsLineage.includes(referencedElement)) {
476
- reference.refSet!.circular = true;
477
-
478
- if (this.options.dereference.circular === 'error') {
479
- throw new ApiDOMStructuredError('Circular reference detected', {
480
- $ref: toValue(referencingElement.$ref),
481
- });
482
- } else if (this.options.dereference.circular === 'replace') {
483
- const refElement = new RefElement($refBaseURI, {
484
- type: referencingElement.element,
485
- uri: reference.uri,
486
- $ref: toValue(referencingElement.$ref),
487
- });
488
- const replacer =
489
- this.options.dereference.strategyOpts['openapi-2']?.circularReplacer ??
490
- this.options.dereference.circularReplacer;
491
- const replacement = replacer(refElement);
492
-
493
- path.replaceWith(replacement);
494
- return;
495
- }
496
- }
497
-
498
- /**
499
- * Dive deep into the fragment.
500
- *
501
- * Cases to consider:
502
- * 1. We're crossing document boundary
503
- * 2. Fragment is from non-entry document
504
- * 3. Fragment is a Path Item Object with $ref field. We need to follow it to get the eventual value
505
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
506
- */
507
- const isNonEntryDocument = url.stripHash(reference.refSet!.rootRef!.uri) !== reference.uri;
508
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
509
- if (
510
- (isExternalReference ||
511
- isNonEntryDocument ||
512
- (isPathItemElement(referencedElement) && isStringElement(referencedElement.$ref)) ||
513
- shouldDetectCircular) &&
514
- !ancestorsLineage.includesCycle(referencedElement)
515
- ) {
516
- // append referencing reference to ancestors lineage
517
- directAncestors.add(referencingElement);
518
-
519
- const visitor = new OpenAPI2DereferenceVisitor({
520
- reference,
521
- indirections: [...this.indirections],
522
- options: this.options,
523
- refractCache: this.refractCache,
524
- ancestors: ancestorsLineage,
525
- });
526
- referencedElement = await traverseAsync(referencedElement, visitor, { mutable: true });
527
-
528
- // remove referencing reference from ancestors lineage
529
- directAncestors.delete(referencingElement);
530
- }
531
-
532
- /**
533
- * Creating a new version of Path Item by merging fields from referenced Path Item with referencing one.
534
- */
535
- if (isPathItemElement(referencedElement)) {
536
- const mergedElement = cloneShallow<PathItemElement>(referencedElement);
537
- // existing keywords from referencing PathItemElement overrides ones from referenced element
538
- referencingElement.forEach((value: Element, keyElement: Element, item: Element) => {
539
- mergedElement.remove(toValue(keyElement) as string);
540
- (mergedElement.content as Element[]).push(item);
541
- });
542
- mergedElement.remove('$ref');
543
-
544
- // annotate referenced element with info about original referencing element
545
- mergedElement.meta.set('ref-fields', {
546
- $ref: toValue(referencingElement.$ref),
547
- });
548
- // annotate referenced element with info about origin and type
549
- mergedElement.meta.set('ref-origin', reference.uri);
550
- mergedElement.meta.set('ref-type', referencingElement.element);
551
-
552
- referencedElement = mergedElement;
553
- }
554
-
555
- /**
556
- * Transclude referencing element with merged referenced element.
557
- */
558
- path.replaceWith(referencedElement);
559
- return;
560
- } catch (error: unknown) {
561
- const $ref = toValue(referencingElement.$ref) as string;
562
- this.handleError(
563
- `Error while dereferencing Path Item Object. Cannot resolve $ref "${$ref}": ${(error as Error).message}`,
564
- error as Error,
565
- referencingElement,
566
- '$ref',
567
- $ref,
568
- path,
569
- );
570
- } finally {
571
- if (this.indirections.length > indirectionsSize) this.indirections.pop();
572
- }
573
- }
574
-
575
- public async JSONReferenceElement(path: Path<Element>) {
576
- const referencingElement = path.node as JSONReferenceElement;
577
-
578
- // skip current referencing element as it's already been access
579
- if (this.indirections.includes(referencingElement)) {
580
- path.skip();
581
- return;
582
- }
583
-
584
- const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref) as string);
585
- const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
586
- const isExternalReference = !isInternalReference;
587
-
588
- // ignore resolving internal JSONReference Objects
589
- if (!this.options.resolve.internal && isInternalReference) {
590
- // skip traversing this JSONReference element and all it's child elements
591
- path.skip();
592
- return;
593
- }
594
- // ignore resolving external JSONReference Objects
595
- if (!this.options.resolve.external && isExternalReference) {
596
- // skip traversing this JSONReference element and all it's child elements
597
- path.skip();
598
- return;
599
- }
600
-
601
- const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref) as string);
602
- const indirectionsSize = this.indirections.length;
603
-
604
- try {
605
- const reference = await this.toReference(toValue(referencingElement.$ref) as string);
606
-
607
- this.indirections.push(referencingElement);
608
-
609
- const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
610
-
611
- // possibly non-semantic fragment
612
- let referencedElement = evaluate<Element>(
613
- (reference.value as ParseResultElement).result as Element,
614
- jsonPointer,
615
- );
616
-
617
- // applying semantics to a fragment
618
- const referencedElementType = referencingElement.meta.get('referenced-element') as string;
619
- if (
620
- referencedElement.element !== referencedElementType &&
621
- !isJSONReferenceElement(referencedElement)
622
- ) {
623
- if (this.refractCache.has(referencedElement)) {
624
- referencedElement = this.refractCache.get(referencedElement)!;
625
- } else if (isJSONReferenceLikeElement(referencedElement)) {
626
- // handling generic indirect references
627
- const sourceElement = referencedElement;
628
- referencedElement = refractJSONReference(referencedElement);
629
- referencedElement.meta.set('referenced-element', referencedElementType);
630
- this.refractCache.set(sourceElement, referencedElement);
631
- } else {
632
- // handling direct references
633
- const sourceElement = referencedElement;
634
- referencedElement = refract(referencedElement, { element: referencedElementType });
635
- this.refractCache.set(sourceElement, referencedElement);
636
- }
637
- }
638
-
639
- // detect direct or indirect reference
640
- if (referencingElement === referencedElement) {
641
- throw new ApiDOMStructuredError('Recursive JSON Reference Object detected', {
642
- $ref: toValue(referencingElement.$ref),
643
- });
644
- }
645
-
646
- // detect maximum depth of dereferencing
647
- if (this.indirections.length > this.options.dereference.maxDepth) {
648
- throw new MaximumDereferenceDepthError(
649
- `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`,
650
- { maxDepth: this.options.dereference.maxDepth, uri: this.reference.uri },
651
- );
652
- }
653
-
654
- // detect cross-boundary cycle
655
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
656
- if (ancestorsLineage.includes(referencedElement)) {
657
- reference.refSet!.circular = true;
658
-
659
- if (this.options.dereference.circular === 'error') {
660
- throw new ApiDOMStructuredError('Circular reference detected', {
661
- $ref: toValue(referencingElement.$ref),
662
- });
663
- } else if (this.options.dereference.circular === 'replace') {
664
- const refElement = new RefElement($refBaseURI, {
665
- type: referencingElement.element,
666
- uri: reference.uri,
667
- $ref: toValue(referencingElement.$ref),
668
- });
669
- const replacer =
670
- this.options.dereference.strategyOpts['openapi-2']?.circularReplacer ??
671
- this.options.dereference.circularReplacer;
672
- const replacement = replacer(refElement);
673
-
674
- path.replaceWith(replacement);
675
- return;
676
- }
677
- }
678
-
679
- /**
680
- * Dive deep into the fragment.
681
- *
682
- * Cases to consider:
683
- * 1. We're crossing document boundary
684
- 2. Fragment is from non-entry document
685
- * 3. Fragment is a JSON Reference Object. We need to follow it to get the eventual value
686
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
687
- */
688
- const isNonEntryDocument = url.stripHash(reference.refSet!.rootRef!.uri) !== reference.uri;
689
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
690
- if (
691
- (isExternalReference ||
692
- isNonEntryDocument ||
693
- isJSONReferenceElement(referencedElement) ||
694
- shouldDetectCircular) &&
695
- !ancestorsLineage.includesCycle(referencedElement)
696
- ) {
697
- // append referencing reference to ancestors lineage
698
- directAncestors.add(referencingElement);
699
-
700
- const visitor = new OpenAPI2DereferenceVisitor({
701
- reference,
702
- indirections: [...this.indirections],
703
- options: this.options,
704
- refractCache: this.refractCache,
705
- ancestors: ancestorsLineage,
706
- });
707
- referencedElement = await traverseAsync(referencedElement, visitor, { mutable: true });
708
-
709
- // remove referencing reference from ancestors lineage
710
- directAncestors.delete(referencingElement);
711
- }
712
-
713
- /**
714
- * Creating a new version of referenced element to avoid modifying the original one.
715
- */
716
- const mergedElement = cloneShallow(referencedElement);
717
- // annotate referenced element with info about original referencing element
718
- mergedElement.meta.set('ref-fields', {
719
- $ref: toValue(referencingElement.$ref),
720
- });
721
- // annotate fragment with info about origin
722
- mergedElement.meta.set('ref-origin', reference.uri);
723
- mergedElement.meta.set('ref-type', referencingElement.element);
724
-
725
- /**
726
- * Transclude referencing element with merged referenced element.
727
- */
728
- path.replaceWith(mergedElement);
729
- } catch (error: unknown) {
730
- const $ref = toValue(referencingElement.$ref) as string;
731
- this.handleError(
732
- `Error while dereferencing JSON Reference Object. Cannot resolve $ref "${$ref}": ${(error as Error).message}`,
733
- error as Error,
734
- referencingElement,
735
- '$ref',
736
- $ref,
737
- path,
738
- );
739
- } finally {
740
- if (this.indirections.length > indirectionsSize) this.indirections.pop();
741
- }
742
- }
743
- }
744
-
745
- export default OpenAPI2DereferenceVisitor;