@speclynx/apidom-reference 6.1.2 → 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 (106) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +57 -0
  3. package/dist/apidom-reference.browser.js +5783 -3741
  4. package/dist/apidom-reference.browser.min.js +1 -1
  5. package/package.json +31 -30
  6. package/src/File.cjs +5 -2
  7. package/src/File.mjs +5 -2
  8. package/src/bundle/index.cjs +6 -4
  9. package/src/bundle/index.mjs +6 -4
  10. package/src/bundle/strategies/arazzo-1/index.cjs +3 -2
  11. package/src/bundle/strategies/arazzo-1/index.mjs +3 -2
  12. package/src/bundle/strategies/asyncapi-2/index.cjs +3 -2
  13. package/src/bundle/strategies/asyncapi-2/index.mjs +3 -2
  14. package/src/bundle/strategies/openapi-2/index.cjs +3 -2
  15. package/src/bundle/strategies/openapi-2/index.mjs +3 -2
  16. package/src/bundle/strategies/openapi-3-0/index.cjs +3 -2
  17. package/src/bundle/strategies/openapi-3-0/index.mjs +3 -2
  18. package/src/bundle/strategies/openapi-3-1/index.cjs +3 -2
  19. package/src/bundle/strategies/openapi-3-1/index.mjs +3 -2
  20. package/src/dereference/index.cjs +8 -5
  21. package/src/dereference/index.mjs +8 -5
  22. package/src/dereference/strategies/DereferenceVisitor.cjs +588 -0
  23. package/src/dereference/strategies/DereferenceVisitor.mjs +580 -0
  24. package/src/dereference/strategies/arazzo-1/index.cjs +14 -39
  25. package/src/dereference/strategies/arazzo-1/index.mjs +15 -40
  26. package/src/dereference/strategies/arazzo-1/visitor.cjs +207 -331
  27. package/src/dereference/strategies/arazzo-1/visitor.mjs +215 -338
  28. package/src/dereference/strategies/asyncapi-2/index.cjs +11 -39
  29. package/src/dereference/strategies/asyncapi-2/index.mjs +11 -39
  30. package/src/dereference/strategies/asyncapi-2/visitor.cjs +133 -425
  31. package/src/dereference/strategies/asyncapi-2/visitor.mjs +137 -429
  32. package/src/dereference/strategies/openapi-2/index.cjs +12 -42
  33. package/src/dereference/strategies/openapi-2/index.mjs +12 -42
  34. package/src/dereference/strategies/openapi-2/visitor.cjs +168 -532
  35. package/src/dereference/strategies/openapi-2/visitor.mjs +172 -536
  36. package/src/dereference/strategies/openapi-3-0/index.cjs +12 -42
  37. package/src/dereference/strategies/openapi-3-0/index.mjs +12 -42
  38. package/src/dereference/strategies/openapi-3-0/visitor.cjs +194 -468
  39. package/src/dereference/strategies/openapi-3-0/visitor.mjs +199 -473
  40. package/src/dereference/strategies/openapi-3-1/index.cjs +16 -42
  41. package/src/dereference/strategies/openapi-3-1/index.mjs +16 -42
  42. package/src/dereference/strategies/openapi-3-1/visitor.cjs +350 -602
  43. package/src/dereference/strategies/openapi-3-1/visitor.mjs +357 -608
  44. package/src/dereference/strategies/overlay-1/index.cjs +3 -2
  45. package/src/dereference/strategies/overlay-1/index.mjs +3 -2
  46. package/src/index.cjs +0 -7
  47. package/src/index.mjs +0 -1
  48. package/src/options/index.cjs +4 -0
  49. package/src/options/index.mjs +4 -0
  50. package/src/options/util.cjs +7 -1
  51. package/src/options/util.mjs +8 -2
  52. package/src/parse/index.cjs +9 -1
  53. package/src/parse/index.mjs +9 -1
  54. package/src/parse/parsers/apidom-json/index.cjs +2 -1
  55. package/src/parse/parsers/apidom-json/index.mjs +2 -1
  56. package/src/parse/parsers/arazzo-json-1/index.cjs +2 -1
  57. package/src/parse/parsers/arazzo-json-1/index.mjs +2 -1
  58. package/src/parse/parsers/arazzo-yaml-1/index.cjs +2 -1
  59. package/src/parse/parsers/arazzo-yaml-1/index.mjs +2 -1
  60. package/src/parse/parsers/asyncapi-json-2/index.cjs +2 -1
  61. package/src/parse/parsers/asyncapi-json-2/index.mjs +2 -1
  62. package/src/parse/parsers/asyncapi-yaml-2/index.cjs +2 -1
  63. package/src/parse/parsers/asyncapi-yaml-2/index.mjs +2 -1
  64. package/src/parse/parsers/json/index.cjs +2 -1
  65. package/src/parse/parsers/json/index.mjs +2 -1
  66. package/src/parse/parsers/openapi-json-2/index.cjs +2 -1
  67. package/src/parse/parsers/openapi-json-2/index.mjs +2 -1
  68. package/src/parse/parsers/openapi-json-3-0/index.cjs +2 -1
  69. package/src/parse/parsers/openapi-json-3-0/index.mjs +2 -1
  70. package/src/parse/parsers/openapi-json-3-1/index.cjs +2 -1
  71. package/src/parse/parsers/openapi-json-3-1/index.mjs +2 -1
  72. package/src/parse/parsers/openapi-yaml-2/index.cjs +2 -1
  73. package/src/parse/parsers/openapi-yaml-2/index.mjs +2 -1
  74. package/src/parse/parsers/openapi-yaml-3-0/index.cjs +2 -1
  75. package/src/parse/parsers/openapi-yaml-3-0/index.mjs +2 -1
  76. package/src/parse/parsers/openapi-yaml-3-1/index.cjs +2 -1
  77. package/src/parse/parsers/openapi-yaml-3-1/index.mjs +2 -1
  78. package/src/parse/parsers/overlay-json-1/index.cjs +2 -1
  79. package/src/parse/parsers/overlay-json-1/index.mjs +2 -1
  80. package/src/parse/parsers/overlay-yaml-1/index.cjs +2 -1
  81. package/src/parse/parsers/overlay-yaml-1/index.mjs +2 -1
  82. package/src/parse/parsers/yaml-1-2/index.cjs +2 -1
  83. package/src/parse/parsers/yaml-1-2/index.mjs +2 -1
  84. package/src/resolve/index.cjs +6 -3
  85. package/src/resolve/index.mjs +6 -3
  86. package/src/util/url.cjs +38 -8
  87. package/src/util/url.mjs +35 -6
  88. package/types/apidom-reference.d.ts +13 -9
  89. package/types/dereference/strategies/DereferenceVisitor.d.ts +224 -0
  90. package/types/dereference/strategies/arazzo-1/index.d.ts +0 -1
  91. package/types/dereference/strategies/arazzo-1/visitor.d.ts +38 -38
  92. package/types/dereference/strategies/asyncapi-2/index.d.ts +0 -1
  93. package/types/dereference/strategies/asyncapi-2/visitor.d.ts +13 -29
  94. package/types/dereference/strategies/openapi-2/index.d.ts +0 -1
  95. package/types/dereference/strategies/openapi-2/visitor.d.ts +14 -29
  96. package/types/dereference/strategies/openapi-3-0/index.d.ts +0 -1
  97. package/types/dereference/strategies/openapi-3-0/visitor.d.ts +13 -28
  98. package/types/dereference/strategies/openapi-3-1/index.d.ts +0 -1
  99. package/types/dereference/strategies/openapi-3-1/visitor.d.ts +37 -33
  100. package/types/index.d.ts +0 -1
  101. package/types/util/url.d.ts +11 -0
  102. package/LICENSES/AFL-3.0.txt +0 -182
  103. package/LICENSES/BSD-3-Clause.txt +0 -26
  104. package/src/dereference/util.cjs +0 -33
  105. package/src/dereference/util.mjs +0 -27
  106. package/types/dereference/util.d.ts +0 -9
@@ -1,249 +1,171 @@
1
- import { propEq, none } from 'ramda';
2
- import { isElement, isStringElement, isParseResultElement, RefElement, cloneShallow, cloneDeep } from '@speclynx/apidom-datamodel';
3
- import { toValue, toYAML } from '@speclynx/apidom-core';
1
+ import { none } from 'ramda';
2
+ import { isStringElement, isParseResultElement, isPrimitiveElement, includesClasses, cloneShallow, cloneDeep } from '@speclynx/apidom-datamodel';
3
+ import { toValue } from '@speclynx/apidom-core';
4
4
  import { ApiDOMStructuredError } from '@speclynx/apidom-error';
5
- import { traverse, traverseAsync } from '@speclynx/apidom-traverse';
5
+ import { traverse } from '@speclynx/apidom-traverse';
6
6
  import { evaluate as jsonPointerEvaluate, compile as jsonPointerCompile, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
7
- import { isParameterElement, isJSONSchemaElement, isBooleanJSONSchemaElement } from '@speclynx/apidom-ns-arazzo-1';
7
+ import { isParameterElement, isJSONSchemaElement, isBooleanJSONSchemaElement, refract, refractJSONSchema } from '@speclynx/apidom-ns-arazzo-1';
8
8
  import { parse as parseRuntimeExpression } from '@swaggerexpert/arazzo-runtime-expression';
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 "../openapi-3-1/selectors/json-pointer.mjs";
12
- import { schema$idsOf, resolveSchema$ids, resolveSchemaBaseURI, resolveSchema$refField } from "../openapi-3-1/util.mjs";
13
- import { maybeRefractToJSONSchemaElement, resolveArazzo$selfField, identifiedBy$self } from "./util.mjs";
14
- import UnresolvableReferenceError from "../../../errors/UnresolvableReferenceError.mjs";
15
- import MaximumDereferenceDepthError from "../../../errors/MaximumDereferenceDepthError.mjs";
16
- import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
12
+ import { isJSONSchemaElementWith$id, collectSchema$ids, resolveSchema$ids, resolveSchemaBaseURI, resolveSchema$refField } from "../openapi-3-1/util.mjs";
13
+ import { resolveArazzo$selfField, identifiedBy$self } from "./util.mjs";
14
+ import DereferenceVisitor from "../DereferenceVisitor.mjs";
17
15
  import * as url from "../../../util/url.mjs";
18
- import parse from "../../../parse/index.mjs";
19
- import Reference from "../../../Reference.mjs";
20
16
  import File from "../../../File.mjs";
21
- import { AncestorLineage } from "../../util.mjs";
22
17
  import EvaluationJsonSchemaUriError from "../../../errors/EvaluationJsonSchemaUriError.mjs";
23
18
  /**
24
- * @public
19
+ * Type a JSON Schema `$ref` expects its target to have.
25
20
  */
21
+ const JSON_SCHEMA_TYPE = 'JSONSchema';
22
+
26
23
  /**
27
- * @public
24
+ * Context of a source being dereferenced: the `$id`s of the JSON Schema elements enclosing it.
28
25
  */
29
- class Arazzo1DereferenceVisitor {
30
- indirections;
31
- reference;
32
26
 
33
- /**
34
- * Base URI of the current document for resolving relative references.
35
- * Honors the Arazzo `$self` field when present, otherwise falls back
36
- * to the retrieval URI (`this.reference.uri`).
37
- */
38
- baseURI;
39
- options;
27
+ /**
28
+ * @public
29
+ */
40
30
 
31
+ /**
32
+ * Dereferences an Arazzo document. Reusable Objects expect the type of their
33
+ * `referenced-element` meta, JSON Schema `$ref`s expect a JSON Schema.
34
+ *
35
+ * @public
36
+ */
37
+ class Arazzo1DereferenceVisitor extends DereferenceVisitor {
41
38
  /**
42
- * Index of `$id`-bearing schemas per document, shared across nested visitors
43
- * of a single dereference run so it never outlives mutations of the documents.
39
+ * Index of `$id`-bearing schemas per document, shared across the visitors
40
+ * of a single dereference run.
44
41
  */
45
42
  schema$idIndex;
46
- visited;
47
43
 
48
44
  /**
49
45
  * `$id`s of the JSON Schema elements enclosing the traversal root within its
50
46
  * document (outermost first). Empty when the root is the document itself; a
51
- * nested visitor traversing a fragment is seeded with the `$id`s of the
52
- * schemas the fragment sits under, so the `$ref`s and `$id`s within resolve
53
- * against the same base as they would from the document root.
47
+ * visitor traversing a fragment is seeded with the `$id`s of the schemas the
48
+ * fragment sits under, so the `$ref`s and `$id`s within resolve against the
49
+ * same base as they would from the document root.
54
50
  */
55
51
  ancestorSchema$ids;
56
-
57
- /**
58
- * Tracks element ancestors across dive-deep traversal boundaries.
59
- * Used for cycle detection: if a referenced element is found in
60
- * the ancestor lineage, a circular reference is detected.
61
- */
62
- ancestors;
63
52
  constructor({
64
53
  reference,
65
54
  options,
66
- indirections = [],
67
55
  schema$idIndex = new WeakMap(),
68
- ancestors = new AncestorLineage(),
69
- visited = new WeakSet(),
70
56
  ancestorSchema$ids = []
71
57
  }) {
72
- this.indirections = indirections;
73
- this.reference = reference;
74
- this.baseURI = resolveArazzo$selfField(reference.uri, reference.value?.result);
75
- this.options = options;
58
+ super({
59
+ reference,
60
+ options
61
+ });
76
62
  this.schema$idIndex = schema$idIndex;
77
- this.ancestors = new AncestorLineage(...ancestors);
78
- this.visited = visited;
79
63
  this.ancestorSchema$ids = ancestorSchema$ids;
80
64
  }
81
- toAncestorLineage(path) {
82
- const ancestorNodes = path.getAncestorNodes();
83
- const directAncestors = new Set(ancestorNodes.filter(isElement));
84
- const ancestorsLineage = new AncestorLineage(...this.ancestors, directAncestors);
85
- return [ancestorsLineage, directAncestors];
65
+ get strategyName() {
66
+ return 'arazzo-1';
86
67
  }
87
- toBaseURI(uri) {
88
- return url.resolve(this.baseURI, url.sanitize(url.stripHash(uri)));
68
+ createVisitor(reference, {
69
+ ancestorSchema$ids
70
+ }) {
71
+ return new Arazzo1DereferenceVisitor({
72
+ reference,
73
+ options: this.options,
74
+ schema$idIndex: this.schema$idIndex,
75
+ ancestorSchema$ids
76
+ });
89
77
  }
90
- async toReference(uri) {
91
- // detect maximum depth of resolution
92
- if (this.reference.depth >= this.options.resolve.maxDepth) {
93
- throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`, {
94
- maxDepth: this.options.resolve.maxDepth,
95
- uri: this.reference.uri
96
- });
97
- }
98
- const baseURI = this.toBaseURI(uri);
99
- const {
100
- refSet
101
- } = this.reference;
102
-
103
- // we've already processed this Reference in past
104
- if (refSet.has(baseURI)) {
105
- return refSet.find(propEq(baseURI, 'uri'));
106
- }
107
-
108
- // identity-based referencing: URI matches `$self` of an already processed Arazzo document
109
- const referenceBy$self = refSet.find(identifiedBy$self(baseURI));
110
- if (referenceBy$self !== undefined) {
111
- return referenceBy$self;
112
- }
113
- const parseResult = await parse(url.unsanitize(baseURI), {
114
- ...this.options,
115
- parse: {
116
- ...this.options.parse,
117
- mediaType: 'text/plain'
118
- }
78
+ refractTo(element, type) {
79
+ return type === JSON_SCHEMA_TYPE ? refractJSONSchema(element) : refract(element, {
80
+ element: type
119
81
  });
82
+ }
83
+ isOfType(element, type) {
84
+ return type === JSON_SCHEMA_TYPE ? isJSONSchemaElement(element) || isBooleanJSONSchemaElement(element) : element.element === type;
85
+ }
120
86
 
121
- // register new mutable reference with a refSet
122
- //
123
- // NOTE(known limitation): the mutable reference is mutated in place during traversal
124
- // (via `{ mutable: true }`). When an external document evaluates a JSON pointer back
125
- // into this document, it may receive an already-resolved element instead of the original
126
- // $ref. That resolved element was produced using the entry document's resolution context
127
- // (ancestors, indirections), which may differ from the external document's context.
128
- // This can affect cycle detection in rare cross-document circular reference patterns.
129
- //
130
- // Remediation: evaluate JSON pointers against the immutable (original) parse tree
131
- // instead of the mutable working copy. The `immutable://` reference below preserves
132
- // the original tree and could be used for pointer evaluation, ensuring every resolution
133
- // context always sees raw, unresolved elements and processes them with its own
134
- // ancestors/indirections. The trade-off is that elements referenced by multiple
135
- // documents would be resolved once per context instead of being reused.
136
- const mutableReference = new Reference({
137
- uri: baseURI,
138
- value: this.options.dereference.immutable ? cloneDeep(parseResult) : parseResult,
139
- depth: this.reference.depth + 1
140
- });
141
- refSet.add(mutableReference);
142
- if (this.options.dereference.immutable) {
143
- // register new immutable reference with a refSet
144
- const immutableReference = new Reference({
145
- uri: `immutable://${baseURI}`,
146
- value: parseResult,
147
- depth: this.reference.depth + 1
148
- });
149
- refSet.add(immutableReference);
150
- }
151
- return mutableReference;
87
+ /**
88
+ * Honors the Arazzo `$self` field when present, otherwise falls back to the retrieval URI.
89
+ */
90
+ toDocumentBaseURI(reference) {
91
+ return resolveArazzo$selfField(reference.uri, reference.value?.result);
152
92
  }
153
93
 
154
94
  /**
155
- * Handles an error according to the continueOnError option.
156
- *
157
- * For new errors: wraps in UnresolvableReferenceError with structured context
158
- * (type, uri, location, codeFrame, refFieldName, refFieldValue, trace).
159
- * For errors already wrapped by a nested visitor: prepends the current hop to the trace.
160
- *
161
- * Inner/intermediate visitors always throw to let the trace accumulate.
162
- * Only the entry document visitor respects continueOnError (callback/swallow/throw).
95
+ * Identity-based referencing: URI matches `$self` of an already processed Arazzo document.
163
96
  */
164
- handleError(message, error, referencingElement, refFieldName, refFieldValue, visitorPath) {
165
- const {
166
- continueOnError
167
- } = this.options.dereference;
168
- const isEntryDocument = url.stripHash(this.reference.refSet?.rootRef?.uri ?? '') === this.reference.uri;
169
- const uri = this.reference.uri;
170
- const type = referencingElement.element;
171
- const codeFrame = toYAML(referencingElement);
97
+ findReferenceByIdentity(uri) {
98
+ return this.reference.refSet?.find(identifiedBy$self(uri));
99
+ }
172
100
 
173
- // find element location by identity in the document tree.
174
- // guarded: this.reference.value may not be a ParseResultElement or may lack a result.
175
- // falls back to visitorPath which may produce an incomplete path when
176
- // dereferenceApiDOM is called with a fragment (cloneShallow creates a new root identity).
177
- let location;
178
- const root = this.reference.value.result;
179
- if (isElement(root)) {
180
- traverse(root, {
181
- enter(p) {
182
- if (p.node === referencingElement) {
183
- location = p.formatPath();
184
- p.stop();
185
- }
186
- }
187
- });
101
+ /**
102
+ * A document as JSON Schema for locating schemas within it: a generic document is
103
+ * refracted to JSON Schema once per dereference run, a semantic one is used as it is.
104
+ */
105
+ toJSONSchemaDocument(document) {
106
+ if (!isPrimitiveElement(document)) return document;
107
+ let byType = this.run.refracted.get(document);
108
+ if (byType === undefined) {
109
+ byType = new Map();
110
+ this.run.refracted.set(document, byType);
111
+ }
112
+ let refracted = byType.get(JSON_SCHEMA_TYPE);
113
+ if (refracted === undefined) {
114
+ refracted = refractJSONSchema(document);
115
+ byType.set(JSON_SCHEMA_TYPE, refracted);
188
116
  }
189
- location ??= visitorPath.formatPath();
190
- const hop = {
191
- uri,
192
- type,
193
- refFieldName,
194
- refFieldValue,
195
- location,
196
- codeFrame
197
- };
117
+ return refracted;
118
+ }
198
119
 
199
- // enrich existing error from nested visitor or create new one
200
- let unresolvedError;
201
- if (error instanceof UnresolvableReferenceError) {
202
- // prefix relative locations for entries belonging to the referenced document
203
- const refBaseURI = this.toBaseURI(refFieldValue);
204
- const fragment = URIFragmentIdentifier.fromURIReference(refFieldValue);
205
- if (fragment) {
206
- if (refBaseURI === error.uri && error.location) {
207
- error.location = fragment + error.location;
208
- }
209
- for (const h of error.trace) {
210
- if (h.uri === refBaseURI && h.location) h.location = fragment + h.location;
120
+ /**
121
+ * Indexes the `$id`-bearing schemas of a document for the run. Transcluded elements aren't
122
+ * indexed: they're copies of structures indexed where they're defined,
123
+ * and in a document dereferenced in place their shared structure would be walked once per path.
124
+ */
125
+ index$ids(element) {
126
+ if (this.schema$idIndex.has(element)) return;
127
+ const entries = [];
128
+ traverse(element, {
129
+ enter: path => {
130
+ if (this.isTranscluded(path.node)) {
131
+ path.skip();
132
+ } else if (isJSONSchemaElementWith$id(path.node)) {
133
+ entries.push({
134
+ schema: path.node,
135
+ $ids: collectSchema$ids(path)
136
+ });
211
137
  }
212
138
  }
213
- // @ts-ignore
214
- error.trace = [hop, ...error.trace];
215
- unresolvedError = error;
216
- } else {
217
- unresolvedError = new UnresolvableReferenceError(message, {
218
- cause: error,
219
- type,
220
- uri,
221
- location,
222
- codeFrame,
223
- refFieldName,
224
- refFieldValue,
225
- trace: []
226
- });
139
+ });
140
+ this.schema$idIndex.set(element, entries);
141
+ }
142
+ enter(path) {
143
+ if (this.isTranscluded(path.node)) {
144
+ path.skip();
145
+ return;
146
+ }
147
+ // a Source Description parsed into the document has its own components: it's dereferenced
148
+ // in its own context by the strategy, never by the document referencing it
149
+ if (path.parentPath !== null && isParseResultElement(path.node) && includesClasses(path.node, ['source-description'])) {
150
+ path.skip();
227
151
  }
228
- if (!isEntryDocument || continueOnError === false) throw unresolvedError;
229
- if (typeof continueOnError === 'function') continueOnError(unresolvedError);
230
152
  }
231
- ReusableElement(path) {
153
+ async ReusableElement(path) {
232
154
  const referencingElement = path.node;
233
155
 
156
+ // the fields of a Reusable Object hold no references
157
+ path.skip();
158
+
234
159
  // skip current Reusable Object if reference field is not defined as a string
235
160
  if (!isStringElement(referencingElement.reference)) {
236
- path.skip();
237
161
  return;
238
162
  }
239
163
 
240
164
  // ignore if resolve.internal is false (Reusable Objects are internal references only)
241
165
  if (!this.options.resolve.internal) {
242
- path.skip();
243
166
  return;
244
167
  }
245
168
  const runtimeExpression = toValue(referencingElement.reference);
246
- let referencedElement;
247
169
  try {
248
170
  // parse the runtime expression
249
171
  const {
@@ -265,6 +187,7 @@ class Arazzo1DereferenceVisitor {
265
187
 
266
188
  // evaluate runtime expression as JSON Pointer to get the referenced element
267
189
  const jsonPointer = jsonPointerCompile(['components', tree.componentType, tree.componentName]);
190
+ let referencedElement;
268
191
  try {
269
192
  referencedElement = jsonPointerEvaluate(this.reference.value.result, jsonPointer);
270
193
  } catch {
@@ -272,50 +195,62 @@ class Arazzo1DereferenceVisitor {
272
195
  runtimeExpression
273
196
  });
274
197
  }
198
+
199
+ // the reference decides the type of the referenced component
200
+ const type = toValue(referencingElement.meta.get('referenced-element')) ?? referencedElement.element;
201
+ await this.addTransclusion(path, {
202
+ source: referencedElement,
203
+ type,
204
+ reference: this.reference,
205
+ context: {
206
+ ancestorSchema$ids: []
207
+ }
208
+ }, {
209
+ toTranscludedElement: (element, target) => {
210
+ const reusableElement = element;
211
+ const mergedElement = cloneShallow(target);
212
+ // annotate with info about original referencing element
213
+ mergedElement.meta.set('ref-fields', {
214
+ reference: runtimeExpression,
215
+ value: toValue(reusableElement.value)
216
+ });
217
+ // annotate with info about origin
218
+ mergedElement.meta.set('ref-origin', this.reference.uri);
219
+ mergedElement.meta.set('ref-type', element.element);
220
+
221
+ // override value field if present for Parameter Objects
222
+ if (isParameterElement(mergedElement) && reusableElement.hasKey('value')) {
223
+ mergedElement.remove('value');
224
+ mergedElement.set('value', reusableElement.get('value'));
225
+ }
226
+ return mergedElement;
227
+ },
228
+ circular: {
229
+ uri: runtimeExpression,
230
+ meta: {
231
+ uri: this.reference.uri,
232
+ reference: runtimeExpression
233
+ },
234
+ details: {
235
+ runtimeExpression
236
+ }
237
+ }
238
+ });
275
239
  } catch (error) {
276
240
  this.handleError(`Error while dereferencing Reusable Object. Cannot resolve reference "${runtimeExpression}": ${error.message}`, error, referencingElement, 'reference', runtimeExpression, path);
277
- return;
278
241
  }
279
-
280
- /**
281
- * Create a shallow clone of the referenced element to avoid modifying the original.
282
- */
283
- const mergedElement = cloneShallow(referencedElement);
284
- // annotate with info about original referencing element
285
- mergedElement.meta.set('ref-fields', {
286
- reference: runtimeExpression,
287
- value: toValue(referencingElement.value)
288
- });
289
- // annotate with info about origin
290
- mergedElement.meta.set('ref-origin', this.reference.uri);
291
- mergedElement.meta.set('ref-type', referencingElement.element);
292
-
293
- // override value field if present for Parameter Objects
294
- if (isParameterElement(mergedElement) && referencingElement.hasKey('value')) {
295
- mergedElement.remove('value');
296
- mergedElement.set('value', referencingElement.get('value'));
297
- }
298
-
299
- /**
300
- * Transclude referencing element with merged referenced element.
301
- */
302
- path.replaceWith(mergedElement);
303
- path.skip();
304
242
  }
305
243
  async JSONSchemaElement(path) {
306
244
  const referencingElement = path.node;
307
-
308
- // skip current referencing schema as $ref keyword was not defined
309
- if (!isStringElement(referencingElement.$ref)) {
245
+ if (this.isTranscluded(referencingElement)) {
246
+ path.skip();
310
247
  return;
311
248
  }
312
249
 
313
- // skip current referencing element as it's already been accessed
314
- if (this.indirections.includes(referencingElement)) {
315
- path.skip();
250
+ // skip current referencing schema as $ref keyword was not defined
251
+ if (!isStringElement(referencingElement.$ref)) {
316
252
  return;
317
253
  }
318
- const indirectionsSize = this.indirections.length;
319
254
  try {
320
255
  // compute baseURI using rules around $self, $id and $ref keywords
321
256
  let reference = await this.toReference(url.unsanitize(this.reference.uri));
@@ -346,11 +281,12 @@ class Arazzo1DereferenceVisitor {
346
281
  retrievalURI = this.toBaseURI($refBaseURI);
347
282
  const selector = $refBaseURI;
348
283
  const documentElement = reference.value.result;
349
- const referenceAsSchema = maybeRefractToJSONSchemaElement(documentElement);
284
+ const referenceAsSchema = this.toJSONSchemaDocument(documentElement);
285
+ this.index$ids(referenceAsSchema);
350
286
  // a document that is itself a fragment detached from its source carries
351
287
  // the $ids enclosing it only as `ancestorSchema$ids`: its $ids are then
352
288
  // indexed against the base that chain leaves in effect, and the chain
353
- // prefixes the one of the located schema. A nested visitor rooted at a
289
+ // prefixes the one of the located schema. A visitor rooted at a
354
290
  // fragment of a whole document indexes that document against its own base.
355
291
  const rootNode = path.getAncestorNodes().at(-1) ?? path.node;
356
292
  const isDocumentRoot = (isParseResultElement(rootNode) ? rootNode.result : rootNode) === documentElement;
@@ -364,7 +300,6 @@ class Arazzo1DereferenceVisitor {
364
300
  if (isDocumentRoot) {
365
301
  ancestorSchema$ids = [...this.ancestorSchema$ids, ...ancestorSchema$ids];
366
302
  }
367
- referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
368
303
 
369
304
  // ignore resolving internal Schema Objects
370
305
  if (!this.options.resolve.internal && isInternalReference) {
@@ -394,12 +329,11 @@ class Arazzo1DereferenceVisitor {
394
329
  }
395
330
  reference = await this.toReference(url.unsanitize($refBaseURI));
396
331
  const selector = URIFragmentIdentifier.fromURIReference($refBaseURI);
397
- const referenceAsSchema = maybeRefractToJSONSchemaElement(reference.value.result);
332
+ const referenceAsSchema = this.toJSONSchemaDocument(reference.value.result);
398
333
  ({
399
334
  element: referencedElement,
400
335
  ancestorSchema$ids
401
336
  } = jsonPointerLocate(referenceAsSchema, selector));
402
- referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
403
337
  }
404
338
  } catch (error) {
405
339
  /**
@@ -424,12 +358,11 @@ class Arazzo1DereferenceVisitor {
424
358
  }
425
359
  reference = await this.toReference(url.unsanitize($refBaseURI));
426
360
  const selector = uriToAnchor($refBaseURI);
427
- const referenceAsSchema = maybeRefractToJSONSchemaElement(reference.value.result);
361
+ const referenceAsSchema = this.toJSONSchemaDocument(reference.value.result);
428
362
  ({
429
363
  element: referencedElement,
430
364
  ancestorSchema$ids
431
365
  } = $anchorLocate(selector, referenceAsSchema));
432
- referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
433
366
  } else {
434
367
  // we're assuming here that we're dealing with JSON Pointer here
435
368
  retrievalURI = this.toBaseURI($refBaseURI);
@@ -448,140 +381,84 @@ class Arazzo1DereferenceVisitor {
448
381
  }
449
382
  reference = await this.toReference(url.unsanitize($refBaseURI));
450
383
  const selector = URIFragmentIdentifier.fromURIReference($refBaseURI);
451
- const referenceAsSchema = maybeRefractToJSONSchemaElement(reference.value.result);
384
+ const referenceAsSchema = this.toJSONSchemaDocument(reference.value.result);
452
385
  ({
453
386
  element: referencedElement,
454
387
  ancestorSchema$ids
455
388
  } = jsonPointerLocate(referenceAsSchema, selector));
456
- referencedElement = maybeRefractToJSONSchemaElement(referencedElement);
457
389
  }
458
390
  } else {
459
391
  throw error;
460
392
  }
461
393
  }
462
- this.indirections.push(referencingElement);
463
394
 
464
- // detect direct or indirect reference
395
+ // detect direct reference
465
396
  if (referencingElement === referencedElement) {
466
397
  throw new ApiDOMStructuredError('Recursive JSON Schema reference detected', {
467
398
  $ref: toValue(referencingElement.$ref)
468
399
  });
469
400
  }
470
401
 
471
- // detect maximum depth of dereferencing
472
- if (this.indirections.length > this.options.dereference.maxDepth) {
473
- throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
474
- maxDepth: this.options.dereference.maxDepth,
475
- uri: this.reference.uri
476
- });
477
- }
478
-
479
- // detect cross-boundary cycle
480
- const [ancestorsLineage, directAncestors] = this.toAncestorLineage(path);
481
- if (ancestorsLineage.includes(referencedElement)) {
482
- reference.refSet.circular = true;
483
- if (this.options.dereference.circular === 'error') {
484
- throw new ApiDOMStructuredError('Circular reference detected', {
485
- $ref: toValue(referencingElement.$ref)
402
+ // a JSON Schema $ref reads whatever it points at as a JSON Schema
403
+ const $ref = toValue(referencingElement.$ref);
404
+ const origin = reference.uri;
405
+ await this.addTransclusion(path, {
406
+ source: referencedElement,
407
+ type: JSON_SCHEMA_TYPE,
408
+ reference,
409
+ context: {
410
+ ancestorSchema$ids
411
+ }
412
+ }, {
413
+ toTranscludedElement: (element, target) => {
414
+ // Boolean JSON Schemas
415
+ if (isBooleanJSONSchemaElement(target)) {
416
+ const booleanJsonSchemaElement = cloneDeep(target);
417
+ // annotate referenced element with info about original referencing element
418
+ booleanJsonSchemaElement.meta.set('ref-fields', {
419
+ $ref
420
+ });
421
+ // annotate referenced element with info about origin
422
+ booleanJsonSchemaElement.meta.set('ref-origin', origin);
423
+ booleanJsonSchemaElement.meta.set('ref-type', element.element);
424
+ return booleanJsonSchemaElement;
425
+ }
426
+ if (!isJSONSchemaElement(target)) return target;
427
+
428
+ /**
429
+ * Creating a new version of JSON Schema by merging fields from referenced Schema with referencing one.
430
+ */
431
+ const mergedElement = cloneShallow(target);
432
+ // existing keywords from referencing schema overrides ones from referenced schema
433
+ element.forEach((value, keyElement, item) => {
434
+ mergedElement.remove(toValue(keyElement));
435
+ mergedElement.content.push(item);
486
436
  });
487
- } else if (this.options.dereference.circular === 'replace') {
488
- const refElement = new RefElement($refBaseURI, {
489
- type: referencingElement.element,
490
- uri: reference.uri,
491
- $ref: toValue(referencingElement.$ref)
437
+ mergedElement.remove('$ref');
438
+ // annotate referenced element with info about original referencing element
439
+ mergedElement.meta.set('ref-fields', {
440
+ $ref
492
441
  });
493
- const replacer = this.options.dereference.strategyOpts['arazzo-1']?.circularReplacer ?? this.options.dereference.circularReplacer;
494
- const replacement = replacer(refElement);
495
- path.replaceWith(replacement);
496
- path.skip();
497
- return;
498
- }
499
- }
500
-
501
- /**
502
- * Dive deep into the fragment.
503
- *
504
- * Cases to consider:
505
- * 1. We're crossing document boundary
506
- * 2. Fragment is from non-entry document
507
- * 3. Fragment is a JSON Schema with $ref field. We need to follow it to get the eventual value
508
- * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode
509
- */
510
- const isNonEntryDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri;
511
- const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular);
512
- // content transplanted into the referencing site is traversed under that
513
- // site's $id chain; a fragment enclosed by a different chain has its own
514
- // $refs resolved in place first, by a nested visitor seeded with that chain
515
- const hasDifferentEnclosingBase = resolveSchema$ids(resolveArazzo$selfField(reference.uri, reference.value.result), ancestorSchema$ids) !== resolveSchema$ids(rootBaseURI, schema$idsOf(path.getAncestorNodes().reverse()));
516
- if ((isExternalReference || isNonEntryDocument || hasDifferentEnclosingBase || isJSONSchemaElement(referencedElement) && isStringElement(referencedElement.$ref) || shouldDetectCircular && !this.visited.has(referencedElement)) && !ancestorsLineage.includesCycle(referencedElement)) {
517
- if (shouldDetectCircular) {
518
- this.visited.add(referencedElement);
442
+ // annotate fragment with info about origin
443
+ mergedElement.meta.set('ref-origin', origin);
444
+ mergedElement.meta.set('ref-type', element.element);
445
+ return mergedElement;
446
+ },
447
+ aliasField: '$ref',
448
+ circular: {
449
+ uri: $refBaseURI,
450
+ meta: {
451
+ uri: origin,
452
+ $ref
453
+ },
454
+ details: {
455
+ $ref
456
+ }
519
457
  }
520
-
521
- // append referencing reference to ancestors lineage
522
- directAncestors.add(referencingElement);
523
- const visitor = new Arazzo1DereferenceVisitor({
524
- reference,
525
- indirections: [...this.indirections],
526
- options: this.options,
527
- schema$idIndex: this.schema$idIndex,
528
- visited: this.visited,
529
- ancestors: ancestorsLineage,
530
- ancestorSchema$ids
531
- });
532
- referencedElement = await traverseAsync(referencedElement, visitor, {
533
- mutable: true,
534
- skipVisited: 'skip'
535
- });
536
-
537
- // remove referencing reference from ancestors lineage
538
- directAncestors.delete(referencingElement);
539
- }
540
-
541
- // Boolean JSON Schemas
542
- if (isBooleanJSONSchemaElement(referencedElement)) {
543
- const booleanJsonSchemaElement = cloneDeep(referencedElement);
544
- // annotate referenced element with info about original referencing element
545
- booleanJsonSchemaElement.meta.set('ref-fields', {
546
- $ref: toValue(referencingElement.$ref)
547
- });
548
- // annotate referenced element with info about origin
549
- booleanJsonSchemaElement.meta.set('ref-origin', reference.uri);
550
- booleanJsonSchemaElement.meta.set('ref-type', referencingElement.element);
551
- path.replaceWith(booleanJsonSchemaElement);
552
- path.skip();
553
- return;
554
- }
555
-
556
- /**
557
- * Creating a new version of JSON Schema by merging fields from referenced Schema with referencing one.
558
- */
559
- if (isJSONSchemaElement(referencedElement)) {
560
- const mergedElement = cloneShallow(referencedElement);
561
- // existing keywords from referencing schema overrides ones from referenced schema
562
- referencingElement.forEach((value, keyElement, item) => {
563
- mergedElement.remove(toValue(keyElement));
564
- mergedElement.content.push(item);
565
- });
566
- mergedElement.remove('$ref');
567
- // annotate referenced element with info about original referencing element
568
- mergedElement.meta.set('ref-fields', {
569
- $ref: toValue(referencingElement.$ref)
570
- });
571
- // annotate fragment with info about origin
572
- mergedElement.meta.set('ref-origin', reference.uri);
573
- mergedElement.meta.set('ref-type', referencingElement.element);
574
- referencedElement = mergedElement;
575
- }
576
- /**
577
- * Transclude referencing element with merged referenced element.
578
- */
579
- path.replaceWith(referencedElement);
458
+ });
580
459
  } catch (error) {
581
460
  const $ref = toValue(referencingElement.$ref);
582
461
  this.handleError(`Error while dereferencing Schema Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
583
- } finally {
584
- if (this.indirections.length > indirectionsSize) this.indirections.pop();
585
462
  }
586
463
  }
587
464
  }