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