@speclynx/apidom-reference 6.2.0 → 7.0.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 (91) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +45 -0
  3. package/dist/apidom-reference.browser.js +4701 -3478
  4. package/dist/apidom-reference.browser.min.js +1 -1
  5. package/package.json +29 -28
  6. package/src/File.cjs +5 -2
  7. package/src/File.mjs +5 -2
  8. package/src/bundle/strategies/arazzo-1/index.cjs +3 -2
  9. package/src/bundle/strategies/arazzo-1/index.mjs +3 -2
  10. package/src/bundle/strategies/asyncapi-2/index.cjs +3 -2
  11. package/src/bundle/strategies/asyncapi-2/index.mjs +3 -2
  12. package/src/bundle/strategies/openapi-2/index.cjs +3 -2
  13. package/src/bundle/strategies/openapi-2/index.mjs +3 -2
  14. package/src/bundle/strategies/openapi-3-0/index.cjs +3 -2
  15. package/src/bundle/strategies/openapi-3-0/index.mjs +3 -2
  16. package/src/bundle/strategies/openapi-3-1/index.cjs +3 -2
  17. package/src/bundle/strategies/openapi-3-1/index.mjs +3 -2
  18. package/src/dereference/strategies/DereferenceVisitor.cjs +588 -0
  19. package/src/dereference/strategies/DereferenceVisitor.mjs +580 -0
  20. package/src/dereference/strategies/arazzo-1/index.cjs +14 -39
  21. package/src/dereference/strategies/arazzo-1/index.mjs +15 -40
  22. package/src/dereference/strategies/arazzo-1/visitor.cjs +207 -331
  23. package/src/dereference/strategies/arazzo-1/visitor.mjs +215 -338
  24. package/src/dereference/strategies/asyncapi-2/index.cjs +11 -39
  25. package/src/dereference/strategies/asyncapi-2/index.mjs +11 -39
  26. package/src/dereference/strategies/asyncapi-2/visitor.cjs +133 -425
  27. package/src/dereference/strategies/asyncapi-2/visitor.mjs +137 -429
  28. package/src/dereference/strategies/openapi-2/index.cjs +12 -42
  29. package/src/dereference/strategies/openapi-2/index.mjs +12 -42
  30. package/src/dereference/strategies/openapi-2/visitor.cjs +168 -532
  31. package/src/dereference/strategies/openapi-2/visitor.mjs +172 -536
  32. package/src/dereference/strategies/openapi-3-0/index.cjs +12 -42
  33. package/src/dereference/strategies/openapi-3-0/index.mjs +12 -42
  34. package/src/dereference/strategies/openapi-3-0/visitor.cjs +194 -468
  35. package/src/dereference/strategies/openapi-3-0/visitor.mjs +199 -473
  36. package/src/dereference/strategies/openapi-3-1/index.cjs +16 -42
  37. package/src/dereference/strategies/openapi-3-1/index.mjs +16 -42
  38. package/src/dereference/strategies/openapi-3-1/visitor.cjs +350 -602
  39. package/src/dereference/strategies/openapi-3-1/visitor.mjs +357 -608
  40. package/src/dereference/strategies/overlay-1/index.cjs +3 -2
  41. package/src/dereference/strategies/overlay-1/index.mjs +3 -2
  42. package/src/index.cjs +0 -7
  43. package/src/index.mjs +0 -1
  44. package/src/parse/index.cjs +9 -1
  45. package/src/parse/index.mjs +9 -1
  46. package/src/parse/parsers/apidom-json/index.cjs +2 -1
  47. package/src/parse/parsers/apidom-json/index.mjs +2 -1
  48. package/src/parse/parsers/arazzo-json-1/index.cjs +2 -1
  49. package/src/parse/parsers/arazzo-json-1/index.mjs +2 -1
  50. package/src/parse/parsers/arazzo-yaml-1/index.cjs +2 -1
  51. package/src/parse/parsers/arazzo-yaml-1/index.mjs +2 -1
  52. package/src/parse/parsers/asyncapi-json-2/index.cjs +2 -1
  53. package/src/parse/parsers/asyncapi-json-2/index.mjs +2 -1
  54. package/src/parse/parsers/asyncapi-yaml-2/index.cjs +2 -1
  55. package/src/parse/parsers/asyncapi-yaml-2/index.mjs +2 -1
  56. package/src/parse/parsers/json/index.cjs +2 -1
  57. package/src/parse/parsers/json/index.mjs +2 -1
  58. package/src/parse/parsers/openapi-json-2/index.cjs +2 -1
  59. package/src/parse/parsers/openapi-json-2/index.mjs +2 -1
  60. package/src/parse/parsers/openapi-json-3-0/index.cjs +2 -1
  61. package/src/parse/parsers/openapi-json-3-0/index.mjs +2 -1
  62. package/src/parse/parsers/openapi-json-3-1/index.cjs +2 -1
  63. package/src/parse/parsers/openapi-json-3-1/index.mjs +2 -1
  64. package/src/parse/parsers/openapi-yaml-2/index.cjs +2 -1
  65. package/src/parse/parsers/openapi-yaml-2/index.mjs +2 -1
  66. package/src/parse/parsers/openapi-yaml-3-0/index.cjs +2 -1
  67. package/src/parse/parsers/openapi-yaml-3-0/index.mjs +2 -1
  68. package/src/parse/parsers/openapi-yaml-3-1/index.cjs +2 -1
  69. package/src/parse/parsers/openapi-yaml-3-1/index.mjs +2 -1
  70. package/src/parse/parsers/overlay-json-1/index.cjs +2 -1
  71. package/src/parse/parsers/overlay-json-1/index.mjs +2 -1
  72. package/src/parse/parsers/overlay-yaml-1/index.cjs +2 -1
  73. package/src/parse/parsers/overlay-yaml-1/index.mjs +2 -1
  74. package/src/parse/parsers/yaml-1-2/index.cjs +2 -1
  75. package/src/parse/parsers/yaml-1-2/index.mjs +2 -1
  76. package/types/apidom-reference.d.ts +0 -9
  77. package/types/dereference/strategies/DereferenceVisitor.d.ts +224 -0
  78. package/types/dereference/strategies/arazzo-1/index.d.ts +0 -1
  79. package/types/dereference/strategies/arazzo-1/visitor.d.ts +38 -38
  80. package/types/dereference/strategies/asyncapi-2/index.d.ts +0 -1
  81. package/types/dereference/strategies/asyncapi-2/visitor.d.ts +13 -29
  82. package/types/dereference/strategies/openapi-2/index.d.ts +0 -1
  83. package/types/dereference/strategies/openapi-2/visitor.d.ts +14 -29
  84. package/types/dereference/strategies/openapi-3-0/index.d.ts +0 -1
  85. package/types/dereference/strategies/openapi-3-0/visitor.d.ts +13 -28
  86. package/types/dereference/strategies/openapi-3-1/index.d.ts +0 -1
  87. package/types/dereference/strategies/openapi-3-1/visitor.d.ts +37 -33
  88. package/types/index.d.ts +0 -1
  89. package/src/dereference/util.cjs +0 -33
  90. package/src/dereference/util.mjs +0 -27
  91. package/types/dereference/util.d.ts +0 -9
@@ -1,406 +1,255 @@
1
- import { propEq, none } from 'ramda';
1
+ import { none } from 'ramda';
2
2
  import { isUndefined } from 'ramda-adjunct';
3
- import { isElement, isStringElement, isObjectElement, isParseResultElement, RefElement, cloneShallow, cloneDeep } from '@speclynx/apidom-datamodel';
4
- import { toValue, fixedFields, toYAML } from '@speclynx/apidom-core';
3
+ import { isElement, isStringElement, isObjectElement, isParseResultElement, isPrimitiveElement, cloneShallow, cloneDeep } from '@speclynx/apidom-datamodel';
4
+ import { toValue, fixedFields } from '@speclynx/apidom-core';
5
5
  import { ApiDOMStructuredError } from '@speclynx/apidom-error';
6
- import { traverse, traverseAsync, find } from '@speclynx/apidom-traverse';
6
+ import { traverse, find } from '@speclynx/apidom-traverse';
7
7
  import { evaluate as jsonPointerEvaluate, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
8
- import { isReferenceLikeElement, isPathItemElement, isReferenceElement, isSchemaElement, isOperationElement, isBooleanJSONSchemaElement, refract, refractReference, refractPathItem, refractOperation } from '@speclynx/apidom-ns-openapi-3-1';
8
+ import { isReferenceLikeElement, isPathItemElement, isReferenceElement, isSchemaElement, isOperationElement, isBooleanJSONSchemaElement, refract, refractReference, refractSchema } from '@speclynx/apidom-ns-openapi-3-1';
9
9
  import { isAnchor, uriToAnchor, locate as $anchorLocate } from "./selectors/$anchor.mjs";
10
10
  import { locate as uriLocate } from "./selectors/uri.mjs";
11
11
  import { locate as jsonPointerLocate } from "./selectors/json-pointer.mjs";
12
- import MaximumDereferenceDepthError from "../../../errors/MaximumDereferenceDepthError.mjs";
13
- import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
14
- import UnresolvableReferenceError from "../../../errors/UnresolvableReferenceError.mjs";
15
12
  import EvaluationJsonSchemaUriError from "../../../errors/EvaluationJsonSchemaUriError.mjs";
13
+ import DereferenceVisitor from "../DereferenceVisitor.mjs";
16
14
  import * as url from "../../../util/url.mjs";
17
- import parse from "../../../parse/index.mjs";
18
- import Reference from "../../../Reference.mjs";
19
15
  import File from "../../../File.mjs";
20
- import { schema$idsOf, resolveSchema$ids, resolveSchemaBaseURI, resolveSchema$refField, maybeRefractToSchemaElement } from "./util.mjs";
21
- import { AncestorLineage } from "../../util.mjs";
16
+ import { isJSONSchemaElementWith$id, collectSchema$ids, resolveSchema$ids, resolveSchemaBaseURI, resolveSchema$refField } from "./util.mjs";
22
17
  /**
23
- * @public
18
+ * Type a Schema Object `$ref` expects its target to have.
19
+ */
20
+ const SCHEMA_TYPE = 'schema';
21
+
22
+ /**
23
+ * Context of a source being dereferenced: the `$id`s of the Schema Objects enclosing it.
24
24
  */
25
+
25
26
  /**
26
27
  * @public
27
28
  */
28
- class OpenAPI3_1DereferenceVisitor {
29
- indirections;
30
- reference;
31
- options;
32
- refractCache;
33
29
 
30
+ /**
31
+ * Dereferences an OpenAPI 3.1 document. Reference Objects expect the type of their
32
+ * `referenced-element` meta, Path Item `$ref`s a Path Item, Schema Object `$ref`s a Schema Object.
33
+ * Link Objects get the Operation Object they point at attached, Example Objects
34
+ * the external value.
35
+ *
36
+ * @public
37
+ */
38
+ class OpenAPI3_1DereferenceVisitor extends DereferenceVisitor {
34
39
  /**
35
- * Index of `$id`-bearing schemas per document, shared across nested visitors
36
- * of a single dereference run so it never outlives mutations of the documents.
40
+ * Index of `$id`-bearing schemas per document, shared across the visitors
41
+ * of a single dereference run.
37
42
  */
38
43
  schema$idIndex;
39
- visited;
40
44
 
41
45
  /**
42
46
  * `$id`s of the JSON Schema elements enclosing the traversal root within its
43
47
  * document (outermost first). Empty when the root is the document itself; a
44
- * nested visitor traversing a fragment is seeded with the `$id`s of the
45
- * schemas the fragment sits under, so the `$ref`s and `$id`s within resolve
46
- * against the same base as they would from the document root.
48
+ * visitor traversing a fragment is seeded with the `$id`s of the schemas the
49
+ * fragment sits under, so the `$ref`s and `$id`s within resolve against the
50
+ * same base as they would from the document root.
47
51
  */
48
52
  ancestorSchema$ids;
49
-
50
- /**
51
- * Tracks element ancestors across dive-deep traversal boundaries.
52
- * Used for cycle detection: if a referenced element is found in
53
- * the ancestor lineage, a circular reference is detected.
54
- */
55
- ancestors;
56
53
  constructor({
57
54
  reference,
58
55
  options,
59
- indirections = [],
60
- refractCache = new WeakMap(),
61
56
  schema$idIndex = new WeakMap(),
62
- ancestors = new AncestorLineage(),
63
- visited = new WeakSet(),
64
57
  ancestorSchema$ids = []
65
58
  }) {
66
- this.indirections = indirections;
67
- this.reference = reference;
68
- this.options = options;
69
- this.refractCache = refractCache;
59
+ super({
60
+ reference,
61
+ options
62
+ });
70
63
  this.schema$idIndex = schema$idIndex;
71
- this.ancestors = new AncestorLineage(...ancestors);
72
- this.visited = visited;
73
64
  this.ancestorSchema$ids = ancestorSchema$ids;
74
65
  }
75
- toAncestorLineage(path) {
76
- const ancestorNodes = path.getAncestorNodes();
77
- const directAncestors = new Set(ancestorNodes.filter(isElement));
78
- const ancestorsLineage = new AncestorLineage(...this.ancestors, directAncestors);
79
- return [ancestorsLineage, directAncestors];
66
+ get strategyName() {
67
+ return 'openapi-3-1';
80
68
  }
81
- toBaseURI(uri) {
82
- return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
69
+ createVisitor(reference, {
70
+ ancestorSchema$ids
71
+ }) {
72
+ return new OpenAPI3_1DereferenceVisitor({
73
+ reference,
74
+ options: this.options,
75
+ schema$idIndex: this.schema$idIndex,
76
+ ancestorSchema$ids
77
+ });
83
78
  }
84
- async toReference(uri) {
85
- // detect maximum depth of resolution
86
- if (this.reference.depth >= this.options.resolve.maxDepth) {
87
- throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`, {
88
- maxDepth: this.options.resolve.maxDepth,
89
- uri: this.reference.uri
90
- });
91
- }
92
- const baseURI = this.toBaseURI(uri);
93
- const {
94
- refSet
95
- } = this.reference;
96
-
97
- // we've already processed this Reference in past
98
- if (refSet.has(baseURI)) {
99
- return refSet.find(propEq(baseURI, 'uri'));
79
+
80
+ /**
81
+ * A Reference Object stands for the type its reference expects: it's dereferenced as that type.
82
+ */
83
+ isOfType(element, type) {
84
+ if (isReferenceElement(element)) return true;
85
+ return type === SCHEMA_TYPE ? isSchemaElement(element) || isBooleanJSONSchemaElement(element) : element.element === type;
86
+ }
87
+
88
+ /**
89
+ * A generic structure holding a `$ref` is a Reference Object to the expected type,
90
+ * unless the type has its own `$ref` field (Path Item Object, Schema Object).
91
+ */
92
+ refractTo(element, type) {
93
+ if (type !== SCHEMA_TYPE && type !== 'pathItem' && isReferenceLikeElement(element)) {
94
+ const referenceElement = refractReference(element);
95
+ referenceElement.meta.set('referenced-element', type);
96
+ return referenceElement;
100
97
  }
101
- const parseResult = await parse(url.unsanitize(baseURI), {
102
- ...this.options,
103
- parse: {
104
- ...this.options.parse,
105
- mediaType: 'text/plain'
106
- }
98
+ return refract(element, {
99
+ element: type
107
100
  });
101
+ }
108
102
 
109
- // register new mutable reference with a refSet
110
- //
111
- // NOTE(known limitation): the mutable reference is mutated in place during traversal
112
- // (via `{ mutable: true }`). When an external document evaluates a JSON pointer back
113
- // into this document, it may receive an already-resolved element instead of the original
114
- // $ref. That resolved element was produced using the entry document's resolution context
115
- // (ancestors, indirections), which may differ from the external document's context.
116
- // This can affect cycle detection in rare cross-document circular reference patterns.
117
- //
118
- // Remediation: evaluate JSON pointers against the immutable (original) parse tree
119
- // instead of the mutable working copy. The `immutable://` reference below preserves
120
- // the original tree and could be used for pointer evaluation, ensuring every resolution
121
- // context always sees raw, unresolved elements and processes them with its own
122
- // ancestors/indirections. The trade-off is that elements referenced by multiple
123
- // documents would be resolved once per context instead of being reused.
124
- const mutableReference = new Reference({
125
- uri: baseURI,
126
- value: this.options.dereference.immutable ? cloneDeep(parseResult) : parseResult,
127
- depth: this.reference.depth + 1
128
- });
129
- refSet.add(mutableReference);
130
- if (this.options.dereference.immutable) {
131
- // register new immutable reference with original parseResult
132
- const immutableReference = new Reference({
133
- uri: `immutable://${baseURI}`,
134
- value: parseResult,
135
- depth: this.reference.depth + 1
136
- });
137
- refSet.add(immutableReference);
103
+ /**
104
+ * A document as Schema Object for locating schemas within it: a generic document is
105
+ * refracted to Schema Object once per dereference run, a semantic one is used as it is.
106
+ */
107
+ toSchemaDocument(document) {
108
+ if (!isPrimitiveElement(document)) return document;
109
+ let byType = this.run.refracted.get(document);
110
+ if (byType === undefined) {
111
+ byType = new Map();
112
+ this.run.refracted.set(document, byType);
138
113
  }
139
- return mutableReference;
114
+ let refracted = byType.get(SCHEMA_TYPE);
115
+ if (refracted === undefined) {
116
+ refracted = refractSchema(document);
117
+ byType.set(SCHEMA_TYPE, refracted);
118
+ }
119
+ return refracted;
140
120
  }
141
121
 
142
122
  /**
143
- * Handles an error according to the continueOnError option.
144
- *
145
- * For new errors: wraps in UnresolvableReferenceError with structured context
146
- * (type, uri, location, codeFrame, refFieldName, refFieldValue, trace).
147
- * For errors already wrapped by a nested visitor: prepends the current hop to the trace.
148
- *
149
- * Inner/intermediate visitors always throw to let the trace accumulate.
150
- * Only the entry document visitor respects continueOnError (callback/swallow/throw).
123
+ * Indexes the `$id`-bearing schemas of an element for the run. Transcluded elements aren't
124
+ * indexed: they're copies of structures indexed where they're defined,
125
+ * and in a document dereferenced in place their shared structure would be walked once per path.
151
126
  */
152
- handleError(message, error, referencingElement, refFieldName, refFieldValue, visitorPath) {
153
- const {
154
- continueOnError
155
- } = this.options.dereference;
156
- const isEntryDocument = url.stripHash(this.reference.refSet?.rootRef?.uri ?? '') === this.reference.uri;
157
- const uri = this.reference.uri;
158
- const type = referencingElement.element;
159
- const codeFrame = toYAML(referencingElement);
160
-
161
- // find element location by identity in the document tree.
162
- // guarded: this.reference.value may not be a ParseResultElement or may lack a result.
163
- // falls back to visitorPath which may produce an incomplete path when
164
- // dereferenceApiDOM is called with a fragment (cloneShallow creates a new root identity).
165
- let location;
166
- const root = this.reference.value.result;
167
- if (isElement(root)) {
168
- traverse(root, {
169
- enter: p => {
170
- if (p.node === referencingElement || this.refractCache.get(p.node) === referencingElement) {
171
- location = p.formatPath();
172
- p.stop();
173
- }
174
- }
175
- });
176
- }
177
- location ??= visitorPath.formatPath();
178
- const hop = {
179
- uri,
180
- type,
181
- refFieldName,
182
- refFieldValue,
183
- location,
184
- codeFrame
185
- };
186
-
187
- // enrich existing error from nested visitor or create new one
188
- let unresolvedError;
189
- if (error instanceof UnresolvableReferenceError) {
190
- // prefix relative locations for entries belonging to the referenced document
191
- const refBaseURI = this.toBaseURI(refFieldValue);
192
- const fragment = URIFragmentIdentifier.fromURIReference(refFieldValue);
193
- if (fragment) {
194
- if (refBaseURI === error.uri && error.location) {
195
- error.location = fragment + error.location;
196
- }
197
- for (const h of error.trace) {
198
- if (h.uri === refBaseURI && h.location) h.location = fragment + h.location;
127
+ index$ids(element) {
128
+ if (this.schema$idIndex.has(element)) return;
129
+ const entries = [];
130
+ traverse(element, {
131
+ enter: path => {
132
+ if (this.isTranscluded(path.node)) {
133
+ path.skip();
134
+ } else if (isJSONSchemaElementWith$id(path.node)) {
135
+ entries.push({
136
+ schema: path.node,
137
+ $ids: collectSchema$ids(path)
138
+ });
199
139
  }
200
140
  }
201
- // @ts-ignore
202
- error.trace = [hop, ...error.trace];
203
- unresolvedError = error;
204
- } else {
205
- unresolvedError = new UnresolvableReferenceError(message, {
206
- cause: error,
207
- type,
208
- uri,
209
- location,
210
- codeFrame,
211
- refFieldName,
212
- refFieldValue,
213
- trace: []
214
- });
215
- }
216
- if (!isEntryDocument || continueOnError === false) throw unresolvedError;
217
- if (typeof continueOnError === 'function') continueOnError(unresolvedError);
141
+ });
142
+ this.schema$idIndex.set(element, entries);
218
143
  }
219
144
  async ReferenceElement(path) {
220
145
  const referencingElement = path.node;
221
146
 
222
- // ignore Reference Object without a string $ref, e.g. an empty YAML value
223
- if (!isStringElement(referencingElement.$ref)) {
224
- return;
225
- }
147
+ // the fields of a Reference Object hold no references
148
+ path.skip();
226
149
 
227
- // skip current referencing element as it's already been accessed
228
- if (this.indirections.includes(referencingElement)) {
229
- path.skip();
150
+ // ignore Reference Object without a string $ref, e.g. an empty YAML value
151
+ if (this.isTranscluded(referencingElement) || !isStringElement(referencingElement.$ref)) {
230
152
  return;
231
153
  }
232
- const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref));
154
+ const $ref = toValue(referencingElement.$ref);
155
+ const retrievalURI = this.toBaseURI($ref);
233
156
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
234
157
  const isExternalReference = !isInternalReference;
235
158
 
236
159
  // ignore resolving internal Reference Objects
237
160
  if (!this.options.resolve.internal && isInternalReference) {
238
- // skip traversing this reference element and all it's child elements
239
- path.skip();
240
161
  return;
241
162
  }
242
163
  // ignore resolving external Reference Objects
243
164
  if (!this.options.resolve.external && isExternalReference) {
244
- // skip traversing this reference element and all it's child elements
245
- path.skip();
246
165
  return;
247
166
  }
248
- const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref));
249
- const indirectionsSize = this.indirections.length;
167
+ const $refBaseURI = url.resolve(retrievalURI, $ref);
250
168
  try {
251
- const reference = await this.toReference(toValue(referencingElement.$ref));
252
- this.indirections.push(referencingElement);
169
+ const reference = await this.toReference($ref);
253
170
  const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
254
-
255
171
  // possibly non-semantic fragment
256
- let referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
257
-
258
- // applying semantics to a fragment
259
- const referencedElementType = referencingElement.meta.get('referenced-element');
260
- if (referencedElement.element !== referencedElementType && !isReferenceElement(referencedElement)) {
261
- if (this.refractCache.has(referencedElement)) {
262
- referencedElement = this.refractCache.get(referencedElement);
263
- } else if (isReferenceLikeElement(referencedElement)) {
264
- // handling generic indirect references
265
- const sourceElement = referencedElement;
266
- referencedElement = refractReference(referencedElement);
267
- referencedElement.meta.set('referenced-element', referencedElementType);
268
- this.refractCache.set(sourceElement, referencedElement);
269
- } else {
270
- // handling direct references
271
- const sourceElement = referencedElement;
272
- referencedElement = refract(referencedElement, {
273
- element: referencedElementType
274
- });
275
- this.refractCache.set(sourceElement, referencedElement);
276
- }
277
- }
172
+ const referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
278
173
 
279
- // detect direct or indirect reference
174
+ // detect direct reference
280
175
  if (referencingElement === referencedElement) {
281
176
  throw new ApiDOMStructuredError('Recursive Reference Object detected', {
282
- $ref: toValue(referencingElement.$ref)
283
- });
284
- }
285
-
286
- // detect maximum depth of dereferencing
287
- if (this.indirections.length > this.options.dereference.maxDepth) {
288
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
289
- maxDepth: this.options.dereference.maxDepth,
290
- uri: this.reference.uri
177
+ $ref
291
178
  });
292
179
  }
293
180
 
294
- // detect cross-boundary cycle
295
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
296
- if (ancestorsLineage.includes(referencedElement)) {
297
- reference.refSet.circular = true;
298
- if (this.options.dereference.circular === 'error') {
299
- throw new ApiDOMStructuredError('Circular reference detected', {
300
- $ref: toValue(referencingElement.$ref)
181
+ // a reference without an expected type reads its target as its own type
182
+ const type = toValue(referencingElement.meta.get('referenced-element')) ?? referencedElement.element;
183
+ await this.addTransclusion(path, {
184
+ source: referencedElement,
185
+ type,
186
+ reference,
187
+ context: {
188
+ ancestorSchema$ids: []
189
+ }
190
+ }, {
191
+ toTranscludedElement: (element, target) => {
192
+ // a chain of Reference Objects leading back to itself never reaches a value
193
+ if (isReferenceElement(target)) return element;
194
+ const reusingElement = element;
195
+ /**
196
+ * Creating a new version of referenced element to avoid modifying the original one.
197
+ */
198
+ const mergedElement = cloneShallow(target);
199
+ // annotate fragment with info about original Reference element
200
+ mergedElement.meta.set('ref-fields', {
201
+ $ref,
202
+ description: toValue(reusingElement.get('description')),
203
+ summary: toValue(reusingElement.get('summary'))
301
204
  });
302
- } else if (this.options.dereference.circular === 'replace') {
303
- const refElement = new RefElement($refBaseURI, {
304
- type: referencingElement.element,
205
+ // annotate fragment with info about origin and type
206
+ mergedElement.meta.set('ref-origin', reference.uri);
207
+ mergedElement.meta.set('ref-type', element.element);
208
+
209
+ // override description and summary (outer has higher priority then inner)
210
+ if (isObjectElement(target) && isObjectElement(mergedElement)) {
211
+ const fields = fixedFields(target, {
212
+ indexed: true
213
+ });
214
+ if (reusingElement.hasKey('description') && Object.hasOwn(fields, 'description')) {
215
+ mergedElement.remove('description');
216
+ mergedElement.set('description', reusingElement.get('description'));
217
+ }
218
+ if (reusingElement.hasKey('summary') && Object.hasOwn(fields, 'summary')) {
219
+ mergedElement.remove('summary');
220
+ mergedElement.set('summary', reusingElement.get('summary'));
221
+ }
222
+ }
223
+ return mergedElement;
224
+ },
225
+ circular: {
226
+ uri: $refBaseURI,
227
+ meta: {
305
228
  uri: reference.uri,
306
- $ref: toValue(referencingElement.$ref)
307
- });
308
- const replacer = this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
309
- const replacement = replacer(refElement);
310
- path.replaceWith(replacement);
311
- path.skip();
312
- return;
313
- }
314
- }
315
-
316
- /**
317
- * Dive deep into the fragment.
318
- *
319
- * Cases to consider:
320
- * 1. We're crossing document boundary
321
- * 2. Fragment is from non-entry document
322
- * 3. Fragment is a Reference Object. We need to follow it to get the eventual value
323
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
324
- */
325
- const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
326
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
327
- if ((isExternalReference || isNonEntryDocument || isReferenceElement(referencedElement) || shouldDetectCircular && !this.visited.has(referencedElement)) && !ancestorsLineage.includesCycle(referencedElement)) {
328
- if (shouldDetectCircular) {
329
- this.visited.add(referencedElement);
229
+ $ref
230
+ },
231
+ details: {
232
+ $ref
233
+ }
330
234
  }
331
- directAncestors.add(referencingElement);
332
- const visitor = new OpenAPI3_1DereferenceVisitor({
333
- reference,
334
- indirections: [...this.indirections],
335
- options: this.options,
336
- refractCache: this.refractCache,
337
- schema$idIndex: this.schema$idIndex,
338
- visited: this.visited,
339
- ancestors: ancestorsLineage
340
- });
341
- referencedElement = await traverseAsync(referencedElement, visitor, {
342
- mutable: true,
343
- skipVisited: 'skip'
344
- });
345
- directAncestors.delete(referencingElement);
346
- }
347
-
348
- /**
349
- * Creating a new version of referenced element to avoid modifying the original one.
350
- */
351
- const mergedElement = cloneShallow(referencedElement);
352
- // annotate fragment with info about original Reference element
353
- mergedElement.meta.set('ref-fields', {
354
- $ref: toValue(referencingElement.$ref),
355
- // @ts-ignore
356
- description: toValue(referencingElement.description),
357
- // @ts-ignore
358
- summary: toValue(referencingElement.summary)
359
235
  });
360
- // annotate fragment with info about origin and type
361
- mergedElement.meta.set('ref-origin', reference.uri);
362
- mergedElement.meta.set('ref-type', referencingElement.element);
363
-
364
- // override description and summary (outer has higher priority then inner)
365
- if (isObjectElement(referencedElement) && isObjectElement(mergedElement)) {
366
- const fields = fixedFields(referencedElement, {
367
- indexed: true
368
- });
369
- if (referencingElement.hasKey('description') && Object.hasOwn(fields, 'description')) {
370
- mergedElement.remove('description');
371
- mergedElement.set('description', referencingElement.get('description'));
372
- }
373
- if (referencingElement.hasKey('summary') && Object.hasOwn(fields, 'summary')) {
374
- mergedElement.remove('summary');
375
- mergedElement.set('summary', referencingElement.get('summary'));
376
- }
377
- }
378
-
379
- /**
380
- * Transclude referencing element with merged referenced element.
381
- */
382
- path.replaceWith(mergedElement);
383
236
  } catch (error) {
384
- const $ref = toValue(referencingElement.$ref);
385
237
  this.handleError(`Error while dereferencing Reference Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
386
- } finally {
387
- if (this.indirections.length > indirectionsSize) this.indirections.pop();
388
238
  }
389
239
  }
390
240
  async PathItemElement(path) {
391
241
  const referencingElement = path.node;
392
-
393
- // ignore PathItemElement without $ref field
394
- if (!isStringElement(referencingElement.$ref)) {
242
+ if (this.isTranscluded(referencingElement)) {
243
+ path.skip();
395
244
  return;
396
245
  }
397
246
 
398
- // skip current referencing element as it's already been accessed
399
- if (this.indirections.includes(referencingElement)) {
400
- path.skip();
247
+ // ignore PathItemElement without $ref field
248
+ if (!isStringElement(referencingElement.$ref)) {
401
249
  return;
402
250
  }
403
- const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref));
251
+ const $ref = toValue(referencingElement.$ref);
252
+ const retrievalURI = this.toBaseURI($ref);
404
253
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
405
254
  const isExternalReference = !isInternalReference;
406
255
 
@@ -414,131 +263,72 @@ class OpenAPI3_1DereferenceVisitor {
414
263
  // skip traversing this Path Item element but traverse all it's child elements
415
264
  return;
416
265
  }
417
- const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref));
418
- const indirectionsSize = this.indirections.length;
266
+ const $refBaseURI = url.resolve(retrievalURI, $ref);
419
267
  try {
420
- const reference = await this.toReference(toValue(referencingElement.$ref));
421
- this.indirections.push(referencingElement);
268
+ const reference = await this.toReference($ref);
422
269
  const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
423
-
424
270
  // possibly non-semantic referenced element
425
- let referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
271
+ const referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
426
272
 
427
- // applying semantics to a referenced element
428
- if (!isPathItemElement(referencedElement)) {
429
- if (this.refractCache.has(referencedElement)) {
430
- referencedElement = this.refractCache.get(referencedElement);
431
- } else {
432
- const sourceElement = referencedElement;
433
- referencedElement = refractPathItem(referencedElement);
434
- this.refractCache.set(sourceElement, referencedElement);
435
- }
436
- }
437
-
438
- // detect direct or indirect reference
273
+ // detect direct reference
439
274
  if (referencingElement === referencedElement) {
440
275
  throw new ApiDOMStructuredError('Recursive Path Item Object reference detected', {
441
- $ref: toValue(referencingElement.$ref)
442
- });
443
- }
444
-
445
- // detect maximum depth of dereferencing
446
- if (this.indirections.length > this.options.dereference.maxDepth) {
447
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
448
- maxDepth: this.options.dereference.maxDepth,
449
- uri: this.reference.uri
276
+ $ref
450
277
  });
451
278
  }
279
+ await this.addTransclusion(path, {
280
+ source: referencedElement,
281
+ type: 'pathItem',
282
+ reference,
283
+ context: {
284
+ ancestorSchema$ids: []
285
+ }
286
+ }, {
287
+ toTranscludedElement: (element, target) => {
288
+ if (!isPathItemElement(target)) return target;
289
+
290
+ /**
291
+ * Creating a new version of Path Item by merging fields from referenced Path Item with referencing one.
292
+ */
293
+ const mergedElement = cloneShallow(target);
294
+ // existing keywords from referencing PathItemElement overrides ones from referenced element
295
+ element.forEach((value, keyElement, item) => {
296
+ mergedElement.remove(toValue(keyElement));
297
+ mergedElement.content.push(item);
298
+ });
299
+ mergedElement.remove('$ref');
452
300
 
453
- // detect cross-boundary cycle
454
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
455
- if (ancestorsLineage.includes(referencedElement)) {
456
- reference.refSet.circular = true;
457
- if (this.options.dereference.circular === 'error') {
458
- throw new ApiDOMStructuredError('Circular reference detected', {
459
- $ref: toValue(referencingElement.$ref)
301
+ // annotate referenced element with info about original referencing element
302
+ mergedElement.meta.set('ref-fields', {
303
+ $ref
460
304
  });
461
- } else if (this.options.dereference.circular === 'replace') {
462
- const refElement = new RefElement($refBaseURI, {
463
- type: referencingElement.element,
305
+ // annotate referenced element with info about origin and type
306
+ mergedElement.meta.set('ref-origin', reference.uri);
307
+ mergedElement.meta.set('ref-type', element.element);
308
+ return mergedElement;
309
+ },
310
+ aliasField: '$ref',
311
+ circular: {
312
+ uri: $refBaseURI,
313
+ meta: {
464
314
  uri: reference.uri,
465
- $ref: toValue(referencingElement.$ref)
466
- });
467
- const replacer = this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
468
- const replacement = replacer(refElement);
469
- path.replaceWith(replacement);
470
- path.skip();
471
- return;
472
- }
473
- }
474
-
475
- /**
476
- * Dive deep into the fragment.
477
- *
478
- * Cases to consider:
479
- * 1. We're crossing document boundary
480
- * 2. Fragment is from non-entry document
481
- * 3. Fragment is a Path Item Object with $ref field. We need to follow it to get the eventual value
482
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
483
- */
484
- const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
485
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
486
- if ((isExternalReference || isNonEntryDocument || isPathItemElement(referencedElement) && isStringElement(referencedElement.$ref) || shouldDetectCircular && !this.visited.has(referencedElement)) && !ancestorsLineage.includesCycle(referencedElement)) {
487
- if (shouldDetectCircular) {
488
- this.visited.add(referencedElement);
315
+ $ref
316
+ },
317
+ details: {
318
+ $ref
319
+ }
489
320
  }
490
- directAncestors.add(referencingElement);
491
- const visitor = new OpenAPI3_1DereferenceVisitor({
492
- reference,
493
- indirections: [...this.indirections],
494
- options: this.options,
495
- refractCache: this.refractCache,
496
- schema$idIndex: this.schema$idIndex,
497
- visited: this.visited,
498
- ancestors: ancestorsLineage
499
- });
500
- referencedElement = await traverseAsync(referencedElement, visitor, {
501
- mutable: true,
502
- skipVisited: 'skip'
503
- });
504
- directAncestors.delete(referencingElement);
505
- }
506
-
507
- /**
508
- * Creating a new version of Path Item by merging fields from referenced Path Item with referencing one.
509
- */
510
- if (isPathItemElement(referencedElement)) {
511
- const mergedElement = cloneShallow(referencedElement);
512
- // existing keywords from referencing PathItemElement overrides ones from referenced element
513
- referencingElement.forEach((value, keyElement, item) => {
514
- mergedElement.remove(toValue(keyElement));
515
- mergedElement.content.push(item);
516
- });
517
- mergedElement.remove('$ref');
518
-
519
- // annotate referenced element with info about original referencing element
520
- mergedElement.meta.set('ref-fields', {
521
- $ref: toValue(referencingElement.$ref)
522
- });
523
- // annotate referenced element with info about origin and type
524
- mergedElement.meta.set('ref-origin', reference.uri);
525
- mergedElement.meta.set('ref-type', referencingElement.element);
526
- referencedElement = mergedElement;
527
- }
528
-
529
- /**
530
- * Transclude referencing element with merged referenced element.
531
- */
532
- path.replaceWith(referencedElement);
321
+ });
533
322
  } catch (error) {
534
- const $ref = toValue(referencingElement.$ref);
535
323
  this.handleError(`Error while dereferencing Path Item Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
536
- } finally {
537
- if (this.indirections.length > indirectionsSize) this.indirections.pop();
538
324
  }
539
325
  }
540
326
  async LinkElement(path) {
541
327
  const linkElement = path.node;
328
+ if (this.isTranscluded(linkElement)) {
329
+ path.skip();
330
+ return;
331
+ }
542
332
 
543
333
  // ignore LinkElement without operationRef or operationId field
544
334
  if (!isStringElement(linkElement.operationRef) && !isStringElement(linkElement.operationId)) {
@@ -553,11 +343,11 @@ class OpenAPI3_1DereferenceVisitor {
553
343
  });
554
344
  }
555
345
  try {
556
- let operationElement;
557
346
  if (isStringElement(linkElement.operationRef)) {
347
+ const operationRef = toValue(linkElement.operationRef);
558
348
  // possibly non-semantic referenced element
559
- const jsonPointer = URIFragmentIdentifier.fromURIReference(toValue(linkElement.operationRef));
560
- const retrievalURI = this.toBaseURI(toValue(linkElement.operationRef));
349
+ const jsonPointer = URIFragmentIdentifier.fromURIReference(operationRef);
350
+ const retrievalURI = this.toBaseURI(operationRef);
561
351
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
562
352
  const isExternalReference = !isInternalReference;
563
353
 
@@ -571,51 +361,56 @@ class OpenAPI3_1DereferenceVisitor {
571
361
  // skip traversing this Link element but traverse all it's child elements
572
362
  return;
573
363
  }
574
- const reference = await this.toReference(toValue(linkElement.operationRef));
575
- operationElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
576
- // applying semantics to a referenced element
577
- if (!isOperationElement(operationElement)) {
578
- if (this.refractCache.has(operationElement)) {
579
- operationElement = this.refractCache.get(operationElement);
580
- } else {
581
- const sourceElement = operationElement;
582
- operationElement = refractOperation(operationElement);
583
- this.refractCache.set(sourceElement, operationElement);
364
+ const reference = await this.toReference(operationRef);
365
+ const operationElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
366
+ await this.addAttachment(path, {
367
+ source: operationElement,
368
+ type: 'operation',
369
+ reference,
370
+ context: {
371
+ ancestorSchema$ids: []
584
372
  }
585
- }
586
- // create shallow clone to be able to annotate with metadata
587
- operationElement = cloneShallow(operationElement);
588
- // annotate operation element with info about origin and type
589
- operationElement.meta.set('ref-origin', reference.uri);
590
- operationElement.meta.set('ref-type', linkElement.element);
591
- const linkElementCopy = cloneShallow(linkElement);
592
- linkElementCopy.operationRef?.meta.set('operation', operationElement);
593
-
594
- /**
595
- * Transclude Link Object containing Operation Object in its meta.
596
- */
597
- path.replaceWith(linkElementCopy);
373
+ }, (element, target) => {
374
+ // create shallow clone to be able to annotate with metadata
375
+ const operationCopy = cloneShallow(target);
376
+ // annotate operation element with info about origin and type
377
+ operationCopy.meta.set('ref-origin', reference.uri);
378
+ operationCopy.meta.set('ref-type', element.element);
379
+
380
+ /**
381
+ * Transclude Link Object containing Operation Object in its meta.
382
+ */
383
+ const linkElementCopy = cloneShallow(element);
384
+ linkElementCopy.operationRef?.meta.set('operation', operationCopy);
385
+ return linkElementCopy;
386
+ });
598
387
  return;
599
388
  }
600
- if (isStringElement(linkElement.operationId)) {
601
- const operationId = toValue(linkElement.operationId);
602
- const reference = await this.toReference(url.unsanitize(this.reference.uri));
603
- const operationPath = find(reference.value.result, path => isOperationElement(path.node) && isElement(path.node.operationId) && path.node.operationId.equals(operationId));
604
- operationElement = operationPath?.node;
605
- // OperationElement not found by its operationId
606
- if (isUndefined(operationElement)) {
607
- throw new ApiDOMStructuredError(`OperationElement(operationId=${operationId}) not found`, {
608
- operationId
609
- });
389
+ const operationId = toValue(linkElement.operationId);
390
+ const reference = await this.toReference(url.unsanitize(this.reference.uri));
391
+ const operationPath = find(reference.value.result, p => isOperationElement(p.node) && isElement(p.node.operationId) && p.node.operationId.equals(operationId));
392
+ const operationElement = operationPath?.node;
393
+ // OperationElement not found by its operationId
394
+ if (isUndefined(operationElement)) {
395
+ throw new ApiDOMStructuredError(`OperationElement(operationId=${operationId}) not found`, {
396
+ operationId
397
+ });
398
+ }
399
+ await this.addAttachment(path, {
400
+ source: operationElement,
401
+ type: 'operation',
402
+ reference,
403
+ context: {
404
+ ancestorSchema$ids: []
610
405
  }
611
- const linkElementCopy = cloneShallow(linkElement);
612
- linkElementCopy.operationId?.meta.set('operation', operationElement);
613
-
406
+ }, (element, target) => {
614
407
  /**
615
408
  * Transclude Link Object containing Operation Object in its meta.
616
409
  */
617
- path.replaceWith(linkElementCopy);
618
- }
410
+ const linkElementCopy = cloneShallow(element);
411
+ linkElementCopy.operationId?.meta.set('operation', target);
412
+ return linkElementCopy;
413
+ });
619
414
  } catch (error) {
620
415
  const refFieldName = isStringElement(linkElement.operationRef) ? 'operationRef' : 'operationId';
621
416
  const refFieldValue = isStringElement(linkElement.operationRef) ? toValue(linkElement.operationRef) : toValue(linkElement.operationId);
@@ -624,6 +419,10 @@ class OpenAPI3_1DereferenceVisitor {
624
419
  }
625
420
  async ExampleElement(path) {
626
421
  const exampleElement = path.node;
422
+ if (this.isTranscluded(exampleElement)) {
423
+ path.skip();
424
+ return;
425
+ }
627
426
 
628
427
  // ignore ExampleElement without externalValue field
629
428
  if (!isStringElement(exampleElement.externalValue)) {
@@ -637,7 +436,8 @@ class OpenAPI3_1DereferenceVisitor {
637
436
  externalValue: toValue(exampleElement.externalValue)
638
437
  });
639
438
  }
640
- const retrievalURI = this.toBaseURI(toValue(exampleElement.externalValue));
439
+ const externalValue = toValue(exampleElement.externalValue);
440
+ const retrievalURI = this.toBaseURI(externalValue);
641
441
  const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
642
442
  const isExternalReference = !isInternalReference;
643
443
 
@@ -652,39 +452,44 @@ class OpenAPI3_1DereferenceVisitor {
652
452
  return;
653
453
  }
654
454
  try {
655
- const reference = await this.toReference(toValue(exampleElement.externalValue));
656
-
657
- // shallow clone of the referenced element
658
- const valueElement = cloneShallow(reference.value.result);
659
- // annotate element with info about origin and type
660
- valueElement.meta.set('ref-origin', reference.uri);
661
- valueElement.meta.set('ref-type', exampleElement.element);
662
- const exampleElementCopy = cloneShallow(exampleElement);
663
- exampleElementCopy.value = valueElement;
664
-
665
- /**
666
- * Transclude Example Object containing external value.
667
- */
668
- path.replaceWith(exampleElementCopy);
455
+ const reference = await this.toReference(externalValue);
456
+ const valueSource = reference.value.result;
457
+ await this.addAttachment(path, {
458
+ source: valueSource,
459
+ type: valueSource.element,
460
+ reference,
461
+ context: {
462
+ ancestorSchema$ids: []
463
+ }
464
+ }, (element, target) => {
465
+ // shallow clone of the referenced element
466
+ const valueElement = cloneShallow(target);
467
+ // annotate element with info about origin and type
468
+ valueElement.meta.set('ref-origin', reference.uri);
469
+ valueElement.meta.set('ref-type', element.element);
470
+
471
+ /**
472
+ * Transclude Example Object containing external value.
473
+ */
474
+ const exampleElementCopy = cloneShallow(element);
475
+ exampleElementCopy.value = valueElement;
476
+ return exampleElementCopy;
477
+ });
669
478
  } catch (error) {
670
- const externalValue = toValue(exampleElement.externalValue);
671
479
  this.handleError(`Error while dereferencing Example Object. Cannot resolve externalValue "${externalValue}": ${error.message}`, error, exampleElement, 'externalValue', externalValue, path);
672
480
  }
673
481
  }
674
482
  async SchemaElement(path) {
675
483
  const referencingElement = path.node;
676
-
677
- // skip current referencing schema as $ref keyword was not defined
678
- if (!isStringElement(referencingElement.$ref)) {
484
+ if (this.isTranscluded(referencingElement)) {
485
+ path.skip();
679
486
  return;
680
487
  }
681
488
 
682
- // skip current referencing element as it's already been accessed
683
- if (this.indirections.includes(referencingElement)) {
684
- path.skip();
489
+ // skip current referencing schema as $ref keyword was not defined
490
+ if (!isStringElement(referencingElement.$ref)) {
685
491
  return;
686
492
  }
687
- const indirectionsSize = this.indirections.length;
688
493
  try {
689
494
  // compute baseURI using rules around $id and $ref keywords
690
495
  let reference = await this.toReference(url.unsanitize(this.reference.uri));
@@ -727,10 +532,12 @@ class OpenAPI3_1DereferenceVisitor {
727
532
  try {
728
533
  // the fragment's $ids resolve against the base its enclosing chain
729
534
  // leaves in effect, which then also prefixes the located schema's chain
535
+ const fragmentAsSchema = this.toSchemaDocument(fragmentElement);
536
+ this.index$ids(fragmentAsSchema);
730
537
  ({
731
538
  element: referencedElement,
732
539
  ancestorSchema$ids
733
- } = uriLocate(selector, maybeRefractToSchemaElement(fragmentElement), {
540
+ } = uriLocate(selector, fragmentAsSchema, {
734
541
  baseURI: rootBaseURI,
735
542
  index: this.schema$idIndex
736
543
  }));
@@ -741,15 +548,16 @@ class OpenAPI3_1DereferenceVisitor {
741
548
  }
742
549
  // the document index is rooted at the document, so the located chain
743
550
  // is already document-relative and takes no prefix
551
+ const documentAsSchema = this.toSchemaDocument(documentElement);
552
+ this.index$ids(documentAsSchema);
744
553
  ({
745
554
  element: referencedElement,
746
555
  ancestorSchema$ids
747
- } = uriLocate(selector, maybeRefractToSchemaElement(documentElement), {
556
+ } = uriLocate(selector, documentAsSchema, {
748
557
  baseURI: reference.uri,
749
558
  index: this.schema$idIndex
750
559
  }));
751
560
  }
752
- referencedElement = maybeRefractToSchemaElement(referencedElement);
753
561
 
754
562
  // ignore resolving internal Schema Objects
755
563
  if (!this.options.resolve.internal && isInternalReference) {
@@ -779,12 +587,10 @@ class OpenAPI3_1DereferenceVisitor {
779
587
  }
780
588
  reference = await this.toReference(url.unsanitize($refBaseURI));
781
589
  const selector = URIFragmentIdentifier.fromURIReference($refBaseURI);
782
- const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result);
783
590
  ({
784
591
  element: referencedElement,
785
592
  ancestorSchema$ids
786
- } = jsonPointerLocate(referenceAsSchema, selector));
787
- referencedElement = maybeRefractToSchemaElement(referencedElement);
593
+ } = jsonPointerLocate(this.toSchemaDocument(reference.value.result), selector));
788
594
  }
789
595
  } catch (error) {
790
596
  /**
@@ -809,12 +615,10 @@ class OpenAPI3_1DereferenceVisitor {
809
615
  }
810
616
  reference = await this.toReference(url.unsanitize($refBaseURI));
811
617
  const selector = uriToAnchor($refBaseURI);
812
- const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result);
813
618
  ({
814
619
  element: referencedElement,
815
620
  ancestorSchema$ids
816
- } = $anchorLocate(selector, referenceAsSchema));
817
- referencedElement = maybeRefractToSchemaElement(referencedElement);
621
+ } = $anchorLocate(selector, this.toSchemaDocument(reference.value.result)));
818
622
  } else {
819
623
  // we're assuming here that we're dealing with JSON Pointer here
820
624
  retrievalURI = this.toBaseURI($refBaseURI);
@@ -833,137 +637,82 @@ class OpenAPI3_1DereferenceVisitor {
833
637
  }
834
638
  reference = await this.toReference(url.unsanitize($refBaseURI));
835
639
  const selector = URIFragmentIdentifier.fromURIReference($refBaseURI);
836
- const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result);
837
640
  ({
838
641
  element: referencedElement,
839
642
  ancestorSchema$ids
840
- } = jsonPointerLocate(referenceAsSchema, selector));
841
- referencedElement = maybeRefractToSchemaElement(referencedElement);
643
+ } = jsonPointerLocate(this.toSchemaDocument(reference.value.result), selector));
842
644
  }
843
645
  } else {
844
646
  throw error;
845
647
  }
846
648
  }
847
- this.indirections.push(referencingElement);
649
+ const $ref = toValue(referencingElement.$ref);
848
650
 
849
- // detect direct or indirect reference
651
+ // detect direct reference
850
652
  if (referencingElement === referencedElement) {
851
653
  throw new ApiDOMStructuredError('Recursive Schema Object reference detected', {
852
- $ref: toValue(referencingElement.$ref)
654
+ $ref
853
655
  });
854
656
  }
855
-
856
- // detect maximum depth of dereferencing
857
- if (this.indirections.length > this.options.dereference.maxDepth) {
858
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
859
- maxDepth: this.options.dereference.maxDepth,
860
- uri: this.reference.uri
861
- });
862
- }
863
-
864
- // detect cross-boundary cycle
865
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
866
- if (ancestorsLineage.includes(referencedElement)) {
867
- reference.refSet.circular = true;
868
- if (this.options.dereference.circular === 'error') {
869
- throw new ApiDOMStructuredError('Circular reference detected', {
870
- $ref: toValue(referencingElement.$ref)
657
+ const origin = reference.uri;
658
+ // a Schema Object $ref reads whatever it points at as a Schema Object
659
+ await this.addTransclusion(path, {
660
+ source: referencedElement,
661
+ type: SCHEMA_TYPE,
662
+ reference,
663
+ context: {
664
+ ancestorSchema$ids
665
+ }
666
+ }, {
667
+ toTranscludedElement: (element, target) => {
668
+ // Boolean JSON Schemas
669
+ if (isBooleanJSONSchemaElement(target)) {
670
+ const booleanJsonSchemaElement = cloneDeep(target);
671
+ // annotate referenced element with info about original referencing element
672
+ booleanJsonSchemaElement.meta.set('ref-fields', {
673
+ $ref
674
+ });
675
+ // annotate referenced element with info about origin and type
676
+ booleanJsonSchemaElement.meta.set('ref-origin', origin);
677
+ booleanJsonSchemaElement.meta.set('ref-type', element.element);
678
+ return booleanJsonSchemaElement;
679
+ }
680
+ if (!isSchemaElement(target)) return target;
681
+
682
+ /**
683
+ * Creating a new version of Schema Object by merging fields from referenced Schema Object with referencing one.
684
+ */
685
+ const mergedElement = cloneShallow(target);
686
+ // existing keywords from referencing schema overrides ones from referenced schema
687
+ element.forEach((value, keyElement, item) => {
688
+ mergedElement.remove(toValue(keyElement));
689
+ mergedElement.content.push(item);
871
690
  });
872
- } else if (this.options.dereference.circular === 'replace') {
873
- const refElement = new RefElement($refBaseURI, {
874
- type: referencingElement.element,
875
- uri: reference.uri,
876
- $ref: toValue(referencingElement.$ref)
691
+ mergedElement.remove('$ref');
692
+ // annotate referenced element with info about original referencing element
693
+ mergedElement.meta.set('ref-fields', {
694
+ $ref
877
695
  });
878
- const replacer = this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
879
- const replacement = replacer(refElement);
880
- path.replaceWith(replacement);
881
- path.skip();
882
- return;
883
- }
884
- }
885
-
886
- /**
887
- * Dive deep into the fragment.
888
- *
889
- * Cases to consider:
890
- * 1. We're crossing document boundary
891
- * 2. Fragment is from non-entry document
892
- * 3. Fragment is a Schema Object with $ref field. We need to follow it to get the eventual value
893
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
894
- */
895
- const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
896
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
897
- // content transplanted into the referencing site is traversed under that
898
- // site's $id chain; a fragment enclosed by a different chain has its own
899
- // $refs resolved in place first, by a nested visitor seeded with that chain
900
- const hasDifferentEnclosingBase = resolveSchema$ids(reference.uri, ancestorSchema$ids) !== resolveSchema$ids(rootBaseURI, schema$idsOf(path.getAncestorNodes().reverse()));
901
- if ((isExternalReference || isNonEntryDocument || hasDifferentEnclosingBase || isSchemaElement(referencedElement) && isStringElement(referencedElement.$ref) || shouldDetectCircular && !this.visited.has(referencedElement)) && !ancestorsLineage.includesCycle(referencedElement)) {
902
- if (shouldDetectCircular) {
903
- this.visited.add(referencedElement);
696
+ // annotate fragment with info about origin and type
697
+ mergedElement.meta.set('ref-origin', origin);
698
+ mergedElement.meta.set('ref-type', element.element);
699
+ return mergedElement;
700
+ },
701
+ aliasField: '$ref',
702
+ circular: {
703
+ uri: $refBaseURI,
704
+ meta: {
705
+ uri: origin,
706
+ $ref
707
+ },
708
+ details: {
709
+ $ref
710
+ }
904
711
  }
905
- directAncestors.add(referencingElement);
906
- const visitor = new OpenAPI3_1DereferenceVisitor({
907
- reference,
908
- indirections: [...this.indirections],
909
- options: this.options,
910
- refractCache: this.refractCache,
911
- schema$idIndex: this.schema$idIndex,
912
- visited: this.visited,
913
- ancestors: ancestorsLineage,
914
- ancestorSchema$ids
915
- });
916
- referencedElement = await traverseAsync(referencedElement, visitor, {
917
- mutable: true,
918
- skipVisited: 'skip'
919
- });
920
- directAncestors.delete(referencingElement);
921
- }
922
-
923
- // Boolean JSON Schemas
924
- if (isBooleanJSONSchemaElement(referencedElement)) {
925
- const booleanJsonSchemaElement = cloneDeep(referencedElement);
926
- // annotate referenced element with info about original referencing element
927
- booleanJsonSchemaElement.meta.set('ref-fields', {
928
- $ref: toValue(referencingElement.$ref)
929
- });
930
- // annotate referenced element with info about origin and type
931
- booleanJsonSchemaElement.meta.set('ref-origin', reference.uri);
932
- booleanJsonSchemaElement.meta.set('ref-type', referencingElement.element);
933
- path.replaceWith(booleanJsonSchemaElement);
934
- path.skip();
935
- return;
936
- }
937
-
938
- /**
939
- * Creating a new version of Schema Object by merging fields from referenced Schema Object with referencing one.
940
- */
941
- if (isSchemaElement(referencedElement)) {
942
- const mergedElement = cloneShallow(referencedElement);
943
- // existing keywords from referencing schema overrides ones from referenced schema
944
- referencingElement.forEach((value, keyElement, item) => {
945
- mergedElement.remove(toValue(keyElement));
946
- mergedElement.content.push(item);
947
- });
948
- mergedElement.remove('$ref');
949
- // annotate referenced element with info about original referencing element
950
- mergedElement.meta.set('ref-fields', {
951
- $ref: toValue(referencingElement.$ref)
952
- });
953
- // annotate fragment with info about origin and type
954
- mergedElement.meta.set('ref-origin', reference.uri);
955
- mergedElement.meta.set('ref-type', referencingElement.element);
956
- referencedElement = mergedElement;
957
- }
958
- /**
959
- * Transclude referencing element with merged referenced element.
960
- */
961
- path.replaceWith(referencedElement);
712
+ });
962
713
  } catch (error) {
963
714
  const $ref = toValue(referencingElement.$ref);
964
715
  this.handleError(`Error while dereferencing Schema Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
965
- } finally {
966
- if (this.indirections.length > indirectionsSize) this.indirections.pop();
967
716
  }
968
717
  }
969
718
  }