@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.
- package/CHANGELOG.md +63 -0
- package/README.md +57 -0
- package/dist/apidom-reference.browser.js +5783 -3741
- package/dist/apidom-reference.browser.min.js +1 -1
- package/package.json +31 -30
- package/src/File.cjs +5 -2
- package/src/File.mjs +5 -2
- package/src/bundle/index.cjs +6 -4
- package/src/bundle/index.mjs +6 -4
- package/src/bundle/strategies/arazzo-1/index.cjs +3 -2
- package/src/bundle/strategies/arazzo-1/index.mjs +3 -2
- package/src/bundle/strategies/asyncapi-2/index.cjs +3 -2
- package/src/bundle/strategies/asyncapi-2/index.mjs +3 -2
- package/src/bundle/strategies/openapi-2/index.cjs +3 -2
- package/src/bundle/strategies/openapi-2/index.mjs +3 -2
- package/src/bundle/strategies/openapi-3-0/index.cjs +3 -2
- package/src/bundle/strategies/openapi-3-0/index.mjs +3 -2
- package/src/bundle/strategies/openapi-3-1/index.cjs +3 -2
- package/src/bundle/strategies/openapi-3-1/index.mjs +3 -2
- package/src/dereference/index.cjs +8 -5
- package/src/dereference/index.mjs +8 -5
- package/src/dereference/strategies/DereferenceVisitor.cjs +588 -0
- package/src/dereference/strategies/DereferenceVisitor.mjs +580 -0
- package/src/dereference/strategies/arazzo-1/index.cjs +14 -39
- package/src/dereference/strategies/arazzo-1/index.mjs +15 -40
- package/src/dereference/strategies/arazzo-1/visitor.cjs +207 -331
- package/src/dereference/strategies/arazzo-1/visitor.mjs +215 -338
- package/src/dereference/strategies/asyncapi-2/index.cjs +11 -39
- package/src/dereference/strategies/asyncapi-2/index.mjs +11 -39
- package/src/dereference/strategies/asyncapi-2/visitor.cjs +133 -425
- package/src/dereference/strategies/asyncapi-2/visitor.mjs +137 -429
- package/src/dereference/strategies/openapi-2/index.cjs +12 -42
- package/src/dereference/strategies/openapi-2/index.mjs +12 -42
- package/src/dereference/strategies/openapi-2/visitor.cjs +168 -532
- package/src/dereference/strategies/openapi-2/visitor.mjs +172 -536
- package/src/dereference/strategies/openapi-3-0/index.cjs +12 -42
- package/src/dereference/strategies/openapi-3-0/index.mjs +12 -42
- package/src/dereference/strategies/openapi-3-0/visitor.cjs +194 -468
- package/src/dereference/strategies/openapi-3-0/visitor.mjs +199 -473
- package/src/dereference/strategies/openapi-3-1/index.cjs +16 -42
- package/src/dereference/strategies/openapi-3-1/index.mjs +16 -42
- package/src/dereference/strategies/openapi-3-1/visitor.cjs +350 -602
- package/src/dereference/strategies/openapi-3-1/visitor.mjs +357 -608
- package/src/dereference/strategies/overlay-1/index.cjs +3 -2
- package/src/dereference/strategies/overlay-1/index.mjs +3 -2
- package/src/index.cjs +0 -7
- package/src/index.mjs +0 -1
- package/src/options/index.cjs +4 -0
- package/src/options/index.mjs +4 -0
- package/src/options/util.cjs +7 -1
- package/src/options/util.mjs +8 -2
- package/src/parse/index.cjs +9 -1
- package/src/parse/index.mjs +9 -1
- package/src/parse/parsers/apidom-json/index.cjs +2 -1
- package/src/parse/parsers/apidom-json/index.mjs +2 -1
- package/src/parse/parsers/arazzo-json-1/index.cjs +2 -1
- package/src/parse/parsers/arazzo-json-1/index.mjs +2 -1
- package/src/parse/parsers/arazzo-yaml-1/index.cjs +2 -1
- package/src/parse/parsers/arazzo-yaml-1/index.mjs +2 -1
- package/src/parse/parsers/asyncapi-json-2/index.cjs +2 -1
- package/src/parse/parsers/asyncapi-json-2/index.mjs +2 -1
- package/src/parse/parsers/asyncapi-yaml-2/index.cjs +2 -1
- package/src/parse/parsers/asyncapi-yaml-2/index.mjs +2 -1
- package/src/parse/parsers/json/index.cjs +2 -1
- package/src/parse/parsers/json/index.mjs +2 -1
- package/src/parse/parsers/openapi-json-2/index.cjs +2 -1
- package/src/parse/parsers/openapi-json-2/index.mjs +2 -1
- package/src/parse/parsers/openapi-json-3-0/index.cjs +2 -1
- package/src/parse/parsers/openapi-json-3-0/index.mjs +2 -1
- package/src/parse/parsers/openapi-json-3-1/index.cjs +2 -1
- package/src/parse/parsers/openapi-json-3-1/index.mjs +2 -1
- package/src/parse/parsers/openapi-yaml-2/index.cjs +2 -1
- package/src/parse/parsers/openapi-yaml-2/index.mjs +2 -1
- package/src/parse/parsers/openapi-yaml-3-0/index.cjs +2 -1
- package/src/parse/parsers/openapi-yaml-3-0/index.mjs +2 -1
- package/src/parse/parsers/openapi-yaml-3-1/index.cjs +2 -1
- package/src/parse/parsers/openapi-yaml-3-1/index.mjs +2 -1
- package/src/parse/parsers/overlay-json-1/index.cjs +2 -1
- package/src/parse/parsers/overlay-json-1/index.mjs +2 -1
- package/src/parse/parsers/overlay-yaml-1/index.cjs +2 -1
- package/src/parse/parsers/overlay-yaml-1/index.mjs +2 -1
- package/src/parse/parsers/yaml-1-2/index.cjs +2 -1
- package/src/parse/parsers/yaml-1-2/index.mjs +2 -1
- package/src/resolve/index.cjs +6 -3
- package/src/resolve/index.mjs +6 -3
- package/src/util/url.cjs +38 -8
- package/src/util/url.mjs +35 -6
- package/types/apidom-reference.d.ts +13 -9
- package/types/dereference/strategies/DereferenceVisitor.d.ts +224 -0
- package/types/dereference/strategies/arazzo-1/index.d.ts +0 -1
- package/types/dereference/strategies/arazzo-1/visitor.d.ts +38 -38
- package/types/dereference/strategies/asyncapi-2/index.d.ts +0 -1
- package/types/dereference/strategies/asyncapi-2/visitor.d.ts +13 -29
- package/types/dereference/strategies/openapi-2/index.d.ts +0 -1
- package/types/dereference/strategies/openapi-2/visitor.d.ts +14 -29
- package/types/dereference/strategies/openapi-3-0/index.d.ts +0 -1
- package/types/dereference/strategies/openapi-3-0/visitor.d.ts +13 -28
- package/types/dereference/strategies/openapi-3-1/index.d.ts +0 -1
- package/types/dereference/strategies/openapi-3-1/visitor.d.ts +37 -33
- package/types/index.d.ts +0 -1
- package/types/util/url.d.ts +11 -0
- package/LICENSES/AFL-3.0.txt +0 -182
- package/LICENSES/BSD-3-Clause.txt +0 -26
- package/src/dereference/util.cjs +0 -33
- package/src/dereference/util.mjs +0 -27
- package/types/dereference/util.d.ts +0 -9
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { propEq } from 'ramda';
|
|
2
|
+
import { isElement, RefElement, ParseResultElement, ObjectElement, ArrayElement, MemberElement, StringElement, NumberElement, BooleanElement, NullElement, cloneDeep, cloneShallow, isObjectElement } from '@speclynx/apidom-datamodel';
|
|
3
|
+
import { toYAML, toGeneric } from '@speclynx/apidom-core';
|
|
4
|
+
import { ApiDOMStructuredError } from '@speclynx/apidom-error';
|
|
5
|
+
import { traverse, traverseAsync } from '@speclynx/apidom-traverse';
|
|
6
|
+
import { URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
|
|
7
|
+
import UnresolvableReferenceError from "../../errors/UnresolvableReferenceError.mjs";
|
|
8
|
+
import MaximumDereferenceDepthError from "../../errors/MaximumDereferenceDepthError.mjs";
|
|
9
|
+
import MaximumResolveDepthError from "../../errors/MaximumResolveDepthError.mjs";
|
|
10
|
+
import * as url from "../../util/url.mjs";
|
|
11
|
+
import parse from "../../parse/index.mjs";
|
|
12
|
+
import Reference from "../../Reference.mjs";
|
|
13
|
+
const genericClasses = new Set([ObjectElement, ArrayElement, MemberElement, StringElement, NumberElement, BooleanElement, NullElement]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether the element is generic ApiDOM: an instance of a primitive element class itself.
|
|
17
|
+
* Semantic elements may carry a primitive element name (e.g. a map of components named
|
|
18
|
+
* `object`), so the name alone doesn't tell.
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export const isGenericElement = element => genericClasses.has(element.constructor);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A source dereferenced as one type: the source as that semantic type, with every reference
|
|
25
|
+
* within it transcluded. `element` exists from the start, so references leading back to
|
|
26
|
+
* an element still being dereferenced (cycles) can be transcluded with it.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How a referencing element is transcluded with the dereferenced target of its reference.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A transclusion to perform.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The target of a reference: the source element, the type the reference expects, and the
|
|
42
|
+
* document the source belongs to. `context` is passed to the visitor dereferencing the source.
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* State shared by all the visitors of a single dereference run.
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
// errors and trace hops whose location is relative to the source a nested visitor traversed
|
|
56
|
+
const relativeLocations = new WeakSet();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Dereferences a document by the type each reference expects of its target. With the immutable
|
|
60
|
+
* option the documents aren't edited and the result is a new tree; otherwise they're
|
|
61
|
+
* dereferenced in place.
|
|
62
|
+
*
|
|
63
|
+
* Every referenced structure is dereferenced once per type the references reaching it expect.
|
|
64
|
+
* A source is dereferenced as a type in two steps. First it's traversed, and for each reference
|
|
65
|
+
* within it the target is located, dereferenced as the expected type, and recorded as
|
|
66
|
+
* a transclusion. Then the referencing elements are transcluded with the dereferenced targets.
|
|
67
|
+
* A dereferenced element is shared by all the references leading to it; a reference leading
|
|
68
|
+
* to an element still being dereferenced is a cycle.
|
|
69
|
+
*
|
|
70
|
+
* Strategies provide the handlers locating the targets of their references, the refraction to
|
|
71
|
+
* their semantic types and the visitors dereferencing the targets.
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
class DereferenceVisitor {
|
|
75
|
+
reference;
|
|
76
|
+
options;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Base URI of the current document for resolving relative references.
|
|
80
|
+
*/
|
|
81
|
+
baseURI;
|
|
82
|
+
run;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Map from the elements of the source the visitor traverses to their copies.
|
|
86
|
+
*/
|
|
87
|
+
cloneMap;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Source the visitor traverses a refraction of, when it's dereferenced as another type.
|
|
91
|
+
*/
|
|
92
|
+
refractedFrom;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Transclusions to perform, keyed by the (copy of the) referencing element.
|
|
96
|
+
*/
|
|
97
|
+
transclusions = new Map();
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Number of references followed to reach the source the visitor traverses.
|
|
101
|
+
*/
|
|
102
|
+
depth = 0;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The source the visitor traverses, dereferenced.
|
|
106
|
+
*/
|
|
107
|
+
current;
|
|
108
|
+
constructor({
|
|
109
|
+
reference,
|
|
110
|
+
options
|
|
111
|
+
}) {
|
|
112
|
+
this.reference = reference;
|
|
113
|
+
this.options = options;
|
|
114
|
+
this.baseURI = this.toDocumentBaseURI(reference);
|
|
115
|
+
this.run = {
|
|
116
|
+
dereferenced: new WeakMap(),
|
|
117
|
+
refracted: new WeakMap(),
|
|
118
|
+
inPlace: !options.dereference.immutable,
|
|
119
|
+
copies: new WeakMap(),
|
|
120
|
+
transcluded: new WeakSet(),
|
|
121
|
+
failed: new WeakSet(),
|
|
122
|
+
reported: new Set(),
|
|
123
|
+
undo: []
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Name of the strategy, for strategy specific options.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Creates the visitor dereferencing a source of the given document.
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Refracts a generic element to the given type.
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Whether the element is of the given type, so it needs no refraction.
|
|
141
|
+
*/
|
|
142
|
+
isOfType(element, type) {
|
|
143
|
+
return element.element === type;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Base URI of a document: its retrieval URI unless the document identifies itself otherwise.
|
|
148
|
+
*/
|
|
149
|
+
toDocumentBaseURI(reference) {
|
|
150
|
+
return reference.uri;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A document already loaded in the run identified by the URI other than by its retrieval URI.
|
|
155
|
+
*/
|
|
156
|
+
findReferenceByIdentity(_uri) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Dereferences the element, which belongs to the document of the visitor's reference.
|
|
162
|
+
* With the immutable option the element isn't edited and its dereferenced copy is returned;
|
|
163
|
+
* otherwise it's dereferenced in place and returned.
|
|
164
|
+
*/
|
|
165
|
+
async dereference(element, context) {
|
|
166
|
+
// frozen elements are left as they are: they're dereferenced as with the immutable option
|
|
167
|
+
const isFrozen = value => isElement(value) && (value.isFrozen || value instanceof ParseResultElement && value.result?.isFrozen === true);
|
|
168
|
+
if (this.run.inPlace && (isFrozen(element) || this.reference.refSet?.refs.some(ref => isFrozen(ref.value)))) {
|
|
169
|
+
this.run = {
|
|
170
|
+
...this.run,
|
|
171
|
+
inPlace: false
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const dereferenced = await this.dereferenceAs({
|
|
175
|
+
source: element,
|
|
176
|
+
type: element.element,
|
|
177
|
+
reference: this.reference,
|
|
178
|
+
context
|
|
179
|
+
}, 0);
|
|
180
|
+
return dereferenced.element;
|
|
181
|
+
}
|
|
182
|
+
toBaseURI(uri) {
|
|
183
|
+
return url.resolve(this.baseURI, url.sanitize(url.stripHash(uri)));
|
|
184
|
+
}
|
|
185
|
+
async toReference(uri) {
|
|
186
|
+
// detect maximum depth of resolution
|
|
187
|
+
if (this.reference.depth >= this.options.resolve.maxDepth) {
|
|
188
|
+
throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`, {
|
|
189
|
+
maxDepth: this.options.resolve.maxDepth,
|
|
190
|
+
uri: this.reference.uri
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const baseURI = this.toBaseURI(uri);
|
|
194
|
+
const {
|
|
195
|
+
refSet
|
|
196
|
+
} = this.reference;
|
|
197
|
+
|
|
198
|
+
// we've already processed this Reference in past
|
|
199
|
+
if (refSet.has(baseURI)) {
|
|
200
|
+
return refSet.find(propEq(baseURI, 'uri'));
|
|
201
|
+
}
|
|
202
|
+
const referenceByIdentity = this.findReferenceByIdentity(baseURI);
|
|
203
|
+
if (referenceByIdentity !== undefined) {
|
|
204
|
+
return referenceByIdentity;
|
|
205
|
+
}
|
|
206
|
+
const parseResult = await parse(url.unsanitize(baseURI), {
|
|
207
|
+
...this.options,
|
|
208
|
+
parse: {
|
|
209
|
+
...this.options.parse,
|
|
210
|
+
mediaType: 'text/plain'
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// registered as parsed: with the immutable option it's only ever copied, never edited
|
|
215
|
+
const reference = new Reference({
|
|
216
|
+
uri: baseURI,
|
|
217
|
+
value: parseResult,
|
|
218
|
+
depth: this.reference.depth + 1
|
|
219
|
+
});
|
|
220
|
+
refSet.add(reference);
|
|
221
|
+
return reference;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Handles an error according to the continueOnError option.
|
|
226
|
+
*
|
|
227
|
+
* For new errors: wraps in UnresolvableReferenceError with structured context
|
|
228
|
+
* (type, uri, location, codeFrame, refFieldName, refFieldValue, trace).
|
|
229
|
+
* For errors already wrapped by a nested visitor: prepends the current hop to the trace.
|
|
230
|
+
*
|
|
231
|
+
* Inner/intermediate visitors always throw to let the trace accumulate.
|
|
232
|
+
* Only the entry document visitor respects continueOnError (callback/swallow/throw),
|
|
233
|
+
* and reports the failure of a referencing element once.
|
|
234
|
+
*/
|
|
235
|
+
handleError(message, error, referencingElement, refFieldName, refFieldValue, visitorPath) {
|
|
236
|
+
const {
|
|
237
|
+
continueOnError
|
|
238
|
+
} = this.options.dereference;
|
|
239
|
+
const isEntryDocument = url.stripHash(this.reference.refSet?.rootRef?.uri ?? '') === this.reference.uri;
|
|
240
|
+
const uri = this.reference.uri;
|
|
241
|
+
const type = referencingElement.element;
|
|
242
|
+
const codeFrame = toYAML(referencingElement);
|
|
243
|
+
|
|
244
|
+
// find element location by identity in the document tree.
|
|
245
|
+
// guarded: this.reference.value may not be a ParseResultElement or may lack a result.
|
|
246
|
+
// falls back to visitorPath, which is relative to the source being traversed
|
|
247
|
+
let location;
|
|
248
|
+
let refractedFromLocation;
|
|
249
|
+
const root = this.reference.value.result;
|
|
250
|
+
if (isElement(root)) {
|
|
251
|
+
traverse(root, {
|
|
252
|
+
enter: p => {
|
|
253
|
+
if (p.node === referencingElement) {
|
|
254
|
+
location = p.formatPath();
|
|
255
|
+
p.stop();
|
|
256
|
+
} else if (p.node === this.refractedFrom) {
|
|
257
|
+
refractedFromLocation = p.formatPath();
|
|
258
|
+
} else if (this.isTranscluded(p.node)) {
|
|
259
|
+
p.skip();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// not found, the location is relative to the traversed source: an enclosing visitor prefixes it
|
|
265
|
+
const isRelative = location === undefined && refractedFromLocation === undefined;
|
|
266
|
+
// within a refraction, the visitor path is relative to the refracted source
|
|
267
|
+
location ??= (refractedFromLocation ?? '') + visitorPath.formatPath();
|
|
268
|
+
const hop = {
|
|
269
|
+
uri,
|
|
270
|
+
type,
|
|
271
|
+
refFieldName,
|
|
272
|
+
refFieldValue,
|
|
273
|
+
location,
|
|
274
|
+
codeFrame
|
|
275
|
+
};
|
|
276
|
+
if (isRelative) relativeLocations.add(hop);
|
|
277
|
+
|
|
278
|
+
// enrich existing error from nested visitor or create new one
|
|
279
|
+
let unresolvedError;
|
|
280
|
+
if (error instanceof UnresolvableReferenceError) {
|
|
281
|
+
// prefix relative locations for entries belonging to the referenced document
|
|
282
|
+
const refBaseURI = this.toBaseURI(refFieldValue);
|
|
283
|
+
const fragment = URIFragmentIdentifier.fromURIReference(refFieldValue);
|
|
284
|
+
if (fragment) {
|
|
285
|
+
for (const entry of [error, ...error.trace]) {
|
|
286
|
+
if (relativeLocations.has(entry) && entry.uri === refBaseURI && entry.location) {
|
|
287
|
+
entry.location = fragment + entry.location;
|
|
288
|
+
relativeLocations.delete(entry);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// @ts-ignore
|
|
293
|
+
error.trace = [hop, ...error.trace];
|
|
294
|
+
unresolvedError = error;
|
|
295
|
+
} else {
|
|
296
|
+
unresolvedError = new UnresolvableReferenceError(message, {
|
|
297
|
+
cause: error,
|
|
298
|
+
type,
|
|
299
|
+
uri,
|
|
300
|
+
location,
|
|
301
|
+
codeFrame,
|
|
302
|
+
refFieldName,
|
|
303
|
+
refFieldValue,
|
|
304
|
+
trace: []
|
|
305
|
+
});
|
|
306
|
+
if (isRelative) relativeLocations.add(unresolvedError);
|
|
307
|
+
}
|
|
308
|
+
if (!isEntryDocument || continueOnError === false) throw unresolvedError;
|
|
309
|
+
// a referencing element is traversed again by every source containing it: its failure is reported once
|
|
310
|
+
this.run.failed.add(referencingElement);
|
|
311
|
+
const failure = JSON.stringify([uri, location, refFieldName, refFieldValue]);
|
|
312
|
+
if (this.run.reported.has(failure)) return;
|
|
313
|
+
this.run.reported.add(failure);
|
|
314
|
+
if (typeof continueOnError === 'function') continueOnError(unresolvedError);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The source as the given semantic type: the source itself when it's of that type, otherwise
|
|
319
|
+
* its refraction to that type, made once per dereference run. The refraction is made from
|
|
320
|
+
* the source dereferenced as its own type, so it doesn't depend on which reference
|
|
321
|
+
* reaches the source first.
|
|
322
|
+
*/
|
|
323
|
+
async toSemantic(target, depth) {
|
|
324
|
+
const {
|
|
325
|
+
source,
|
|
326
|
+
type
|
|
327
|
+
} = target;
|
|
328
|
+
if (this.isOfType(source, type)) return source;
|
|
329
|
+
let byType = this.run.refracted.get(source);
|
|
330
|
+
if (byType === undefined) {
|
|
331
|
+
byType = new Map();
|
|
332
|
+
this.run.refracted.set(source, byType);
|
|
333
|
+
}
|
|
334
|
+
const cached = byType.get(type);
|
|
335
|
+
if (cached !== undefined) return cached;
|
|
336
|
+
const dereferenced = isGenericElement(source) ? source : (await this.dereferenceAs({
|
|
337
|
+
...target,
|
|
338
|
+
type: source.element
|
|
339
|
+
}, depth)).element;
|
|
340
|
+
// dereferencing the source may have led back to this very refraction
|
|
341
|
+
const reached = byType.get(type);
|
|
342
|
+
if (reached !== undefined) return reached;
|
|
343
|
+
const semantic = this.refractTo(toGeneric(dereferenced), type);
|
|
344
|
+
byType.set(type, semantic);
|
|
345
|
+
this.run.undo.push(() => byType.delete(type));
|
|
346
|
+
return semantic;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Dereferences the source as the given type, once per dereference run. `depth` is the number
|
|
351
|
+
* of references followed to reach the source.
|
|
352
|
+
*/
|
|
353
|
+
async dereferenceAs(target, depth) {
|
|
354
|
+
const {
|
|
355
|
+
source,
|
|
356
|
+
type,
|
|
357
|
+
reference,
|
|
358
|
+
context
|
|
359
|
+
} = target;
|
|
360
|
+
let byType = this.run.dereferenced.get(source);
|
|
361
|
+
if (byType === undefined) {
|
|
362
|
+
byType = new Map();
|
|
363
|
+
this.run.dereferenced.set(source, byType);
|
|
364
|
+
}
|
|
365
|
+
const existing = byType.get(type);
|
|
366
|
+
if (existing !== undefined) return existing;
|
|
367
|
+
const semantic = await this.toSemantic(target, depth);
|
|
368
|
+
// a refraction is made for the run, so it's dereferenced as is
|
|
369
|
+
const isOwned = semantic !== source || this.run.inPlace;
|
|
370
|
+
const cloneMap = isOwned ? undefined : this.run.copies;
|
|
371
|
+
const dereferenced = {
|
|
372
|
+
element: isOwned ? semantic : cloneDeep(semantic, {
|
|
373
|
+
visited: cloneMap
|
|
374
|
+
}),
|
|
375
|
+
done: false,
|
|
376
|
+
height: 0,
|
|
377
|
+
alias: false,
|
|
378
|
+
unresolved: false
|
|
379
|
+
};
|
|
380
|
+
// a failure discards whatever got dereferenced or refracted meanwhile, as it may lead to this element
|
|
381
|
+
const undoSize = this.run.undo.length;
|
|
382
|
+
byType.set(type, dereferenced);
|
|
383
|
+
this.run.undo.push(() => byType.delete(type));
|
|
384
|
+
const visitor = this.createVisitor(reference, context);
|
|
385
|
+
visitor.run = this.run;
|
|
386
|
+
visitor.cloneMap = cloneMap;
|
|
387
|
+
visitor.refractedFrom = semantic !== source ? source : undefined;
|
|
388
|
+
visitor.depth = depth;
|
|
389
|
+
visitor.current = dereferenced;
|
|
390
|
+
try {
|
|
391
|
+
// the handlers only record transclusions
|
|
392
|
+
await traverseAsync(semantic, visitor);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
this.run.undo.splice(undoSize).reverse().forEach(undo => undo());
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
for (const transclusion of visitor.transclusions.values()) {
|
|
398
|
+
if (!transclusion.back && !transclusion.attachment) {
|
|
399
|
+
dereferenced.height = Math.max(dereferenced.height, transclusion.target.height + 1);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const rootTransclusion = visitor.transclusions.get(dereferenced.element);
|
|
403
|
+
dereferenced.unresolved = rootTransclusion !== undefined && (rootTransclusion.keep || rootTransclusion.target.unresolved);
|
|
404
|
+
dereferenced.element = visitor.transclude(dereferenced.element);
|
|
405
|
+
dereferenced.done = true;
|
|
406
|
+
return dereferenced;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Dereferences the target of a reference as the expected type and records the transclusion.
|
|
411
|
+
*/
|
|
412
|
+
async addTransclusion(path, target, spec) {
|
|
413
|
+
const referencingCopy = this.cloneMap?.get(path.node) ?? path.node;
|
|
414
|
+
const {
|
|
415
|
+
maxDepth
|
|
416
|
+
} = this.options.dereference;
|
|
417
|
+
|
|
418
|
+
// the source being traversed is itself a referencing element: an alias of the target
|
|
419
|
+
if (path.parentPath === null && this.current !== undefined) this.current.alias = true;
|
|
420
|
+
|
|
421
|
+
// detect maximum depth of dereferencing
|
|
422
|
+
if (this.depth + 1 > maxDepth) {
|
|
423
|
+
throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
|
|
424
|
+
maxDepth,
|
|
425
|
+
uri: this.reference.uri
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
const existing = this.run.dereferenced.get(target.source)?.get(target.type);
|
|
429
|
+
|
|
430
|
+
// a target dereferenced earlier counts the references followed within it as well
|
|
431
|
+
if (existing?.done && this.depth + 1 + existing.height > maxDepth) {
|
|
432
|
+
throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
|
|
433
|
+
maxDepth,
|
|
434
|
+
uri: this.reference.uri
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// a reference leading to an element still being dereferenced is a cycle
|
|
439
|
+
if (existing !== undefined && !existing.done) {
|
|
440
|
+
this.reference.refSet.circular = true;
|
|
441
|
+
if (this.options.dereference.circular === 'error') {
|
|
442
|
+
throw new ApiDOMStructuredError('Circular reference detected', spec.circular.details);
|
|
443
|
+
}
|
|
444
|
+
// an alias has no value before its own reference is transcluded: the referencing element
|
|
445
|
+
// is kept, to be transcluded by an enclosing dereferenced element, if any
|
|
446
|
+
const keep = existing.alias && this.options.dereference.circular === 'ignore';
|
|
447
|
+
this.transclusions.set(referencingCopy, {
|
|
448
|
+
...spec,
|
|
449
|
+
target: existing,
|
|
450
|
+
back: true,
|
|
451
|
+
keep,
|
|
452
|
+
attachment: false
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const dereferenced = existing ?? (await this.dereferenceAs(target, this.depth + 1));
|
|
457
|
+
// the referencing element failed while its target was dereferenced, e.g. as a circular reference
|
|
458
|
+
if (this.run.failed.has(path.node)) return;
|
|
459
|
+
this.transclusions.set(referencingCopy, {
|
|
460
|
+
...spec,
|
|
461
|
+
target: dereferenced,
|
|
462
|
+
back: false,
|
|
463
|
+
keep: false,
|
|
464
|
+
attachment: false
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Records the transclusion of a referencing element with a target that isn't dereferenced:
|
|
470
|
+
* the target as the expected type is attached as it is, e.g. an Operation Object to the Link
|
|
471
|
+
* Object pointing at it. Attached targets never form cycles.
|
|
472
|
+
*/
|
|
473
|
+
async addAttachment(path, target, toTranscludedElement) {
|
|
474
|
+
const referencingCopy = this.cloneMap?.get(path.node) ?? path.node;
|
|
475
|
+
const semantic = await this.toSemantic(target, this.depth + 1);
|
|
476
|
+
// with the immutable option, the attached target is its copy in the result when it has one
|
|
477
|
+
const element = this.run.inPlace ? semantic : this.run.copies.get(semantic) ?? semantic;
|
|
478
|
+
this.transclusions.set(referencingCopy, {
|
|
479
|
+
toTranscludedElement,
|
|
480
|
+
circular: {
|
|
481
|
+
uri: '',
|
|
482
|
+
meta: {},
|
|
483
|
+
details: {}
|
|
484
|
+
},
|
|
485
|
+
target: {
|
|
486
|
+
element,
|
|
487
|
+
done: true,
|
|
488
|
+
height: 0,
|
|
489
|
+
alias: false,
|
|
490
|
+
unresolved: false
|
|
491
|
+
},
|
|
492
|
+
back: false,
|
|
493
|
+
keep: false,
|
|
494
|
+
attachment: true
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Transcludes every referencing element within the element with its dereferenced target.
|
|
500
|
+
*/
|
|
501
|
+
transclude(element) {
|
|
502
|
+
if (this.transclusions.size === 0) return element;
|
|
503
|
+
|
|
504
|
+
// replaced on leave: nested referencing elements first, so merges see them replaced
|
|
505
|
+
traverse(element, {
|
|
506
|
+
enter: path => {
|
|
507
|
+
if (this.isTranscluded(path.node)) path.skip();
|
|
508
|
+
},
|
|
509
|
+
leave: path => {
|
|
510
|
+
const transclusion = this.transclusions.get(path.node);
|
|
511
|
+
if (path.node !== element && transclusion !== undefined) {
|
|
512
|
+
const transcludedElement = this.toTranscludedElement(path.node, transclusion);
|
|
513
|
+
// a kept referencing element is left to be transcluded by an enclosing element
|
|
514
|
+
if (transcludedElement === path.node) return;
|
|
515
|
+
this.run.transcluded.add(transcludedElement);
|
|
516
|
+
path.replaceWith(transcludedElement);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}, {
|
|
520
|
+
mutable: true
|
|
521
|
+
});
|
|
522
|
+
const rootTransclusion = this.transclusions.get(element);
|
|
523
|
+
if (rootTransclusion === undefined) return element;
|
|
524
|
+
|
|
525
|
+
// references leading back to the element may already be transcluded with it, so it's filled in place
|
|
526
|
+
const transcludedElement = this.toTranscludedElement(element, rootTransclusion);
|
|
527
|
+
if (transcludedElement === element) return element;
|
|
528
|
+
if (transcludedElement.constructor !== element.constructor) return transcludedElement;
|
|
529
|
+
element.element = transcludedElement.element;
|
|
530
|
+
element.content = transcludedElement.content;
|
|
531
|
+
element.meta = transcludedElement.meta;
|
|
532
|
+
element.attributes = transcludedElement.attributes;
|
|
533
|
+
return element;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The element transcluded in place of a referencing element.
|
|
538
|
+
*/
|
|
539
|
+
toTranscludedElement(referencingElement, transclusion) {
|
|
540
|
+
// the referencing element waits for an enclosing element to transclude it
|
|
541
|
+
if (transclusion.keep) return referencingElement;
|
|
542
|
+
// an alias whose reference never reaches a value
|
|
543
|
+
if (transclusion.target.unresolved) {
|
|
544
|
+
const {
|
|
545
|
+
aliasField
|
|
546
|
+
} = transclusion;
|
|
547
|
+
const alias = transclusion.target.element;
|
|
548
|
+
if (aliasField === undefined || !isObjectElement(alias)) return referencingElement;
|
|
549
|
+
const fields = cloneShallow(alias);
|
|
550
|
+
fields.remove(aliasField);
|
|
551
|
+
return transclusion.toTranscludedElement(referencingElement, fields);
|
|
552
|
+
}
|
|
553
|
+
if (transclusion.back && this.options.dereference.circular === 'replace') {
|
|
554
|
+
const refElement = new RefElement(transclusion.circular.uri, {
|
|
555
|
+
type: referencingElement.element,
|
|
556
|
+
...transclusion.circular.meta
|
|
557
|
+
});
|
|
558
|
+
const replacer = this.options.dereference.strategyOpts[this.strategyName]?.circularReplacer ?? this.options.dereference.circularReplacer;
|
|
559
|
+
return replacer(refElement);
|
|
560
|
+
}
|
|
561
|
+
return transclusion.toTranscludedElement(referencingElement, transclusion.target.element);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Whether the element is transcluded in place of a referencing element, in this run or,
|
|
566
|
+
* when dereferencing in place, in an earlier one: transcluded elements carry the `ref-type` meta,
|
|
567
|
+
* which only dereferencing sets (bundling annotates hoisted elements with `ref-origin` only).
|
|
568
|
+
*/
|
|
569
|
+
isTranscluded(element) {
|
|
570
|
+
return this.run.transcluded.has(element) || !element.isMetaEmpty && element.meta.hasKey('ref-type');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Dereferenced in place, a source may already hold transcluded elements.
|
|
575
|
+
*/
|
|
576
|
+
enter(path) {
|
|
577
|
+
if (this.isTranscluded(path.node)) path.skip();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
export default DereferenceVisitor;
|
|
@@ -48,8 +48,8 @@ Object.defineProperty(exports, "resolveSchema$refField", {
|
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
var _apidomDatamodel = require("@speclynx/apidom-datamodel");
|
|
51
|
-
var _apidomTraverse = require("@speclynx/apidom-traverse");
|
|
52
51
|
var _apidomNsArazzo = require("@speclynx/apidom-ns-arazzo-1");
|
|
52
|
+
var _mediaTypes = require("@swaggerexpert/media-types");
|
|
53
53
|
var _DereferenceStrategy = _interopRequireDefault(require("../DereferenceStrategy.cjs"));
|
|
54
54
|
var _Reference = _interopRequireDefault(require("../../../Reference.cjs"));
|
|
55
55
|
var _ReferenceSet = _interopRequireDefault(require("../../../ReferenceSet.cjs"));
|
|
@@ -73,65 +73,41 @@ class Arazzo1DereferenceStrategy extends _DereferenceStrategy.default {
|
|
|
73
73
|
}
|
|
74
74
|
canDereference(file) {
|
|
75
75
|
// assert by media type
|
|
76
|
-
if (
|
|
77
|
-
return _apidomNsArazzo.mediaTypes
|
|
76
|
+
if (!(0, _mediaTypes.matches)('text/plain', file.mediaType)) {
|
|
77
|
+
return (0, _mediaTypes.selectBestMatch)(_apidomNsArazzo.mediaTypes, file.mediaType) !== null;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// assert by inspecting ApiDOM
|
|
81
81
|
return (0, _apidomNsArazzo.isArazzoSpecification1Element)(file.parseResult?.result);
|
|
82
82
|
}
|
|
83
83
|
async dereference(file, options) {
|
|
84
|
-
const
|
|
85
|
-
const mutableRefSet = new _ReferenceSet.default();
|
|
86
|
-
let refSet = immutableRefSet;
|
|
84
|
+
const refSet = options.dereference.refSet ?? new _ReferenceSet.default();
|
|
87
85
|
let reference;
|
|
88
|
-
if (!
|
|
86
|
+
if (!refSet.has(file.uri)) {
|
|
89
87
|
reference = new _Reference.default({
|
|
90
88
|
uri: file.uri,
|
|
91
89
|
value: file.parseResult
|
|
92
90
|
});
|
|
93
|
-
|
|
91
|
+
refSet.add(reference);
|
|
94
92
|
} else {
|
|
95
93
|
// pre-computed refSet was provided as configuration option
|
|
96
|
-
reference =
|
|
94
|
+
reference = refSet.find(ref => ref.uri === file.uri);
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
98
|
+
* With the immutable option the dereferenced element is a new tree,
|
|
99
|
+
* otherwise the dereferenced root is dereferenced in place.
|
|
102
100
|
*/
|
|
103
|
-
|
|
104
|
-
immutableRefSet.refs.map(ref => new _Reference.default({
|
|
105
|
-
...ref,
|
|
106
|
-
value: (0, _apidomDatamodel.cloneDeep)(ref.value)
|
|
107
|
-
})).forEach(ref => mutableRefSet.add(ref));
|
|
108
|
-
reference = mutableRefSet.find(ref => ref.uri === file.uri);
|
|
109
|
-
refSet = mutableRefSet;
|
|
110
|
-
}
|
|
111
|
-
const shouldDetectCircular = ['error', 'replace'].includes(options.dereference.circular);
|
|
101
|
+
const root = refSet.rootRef.value;
|
|
112
102
|
const visitor = new _visitor.default({
|
|
113
103
|
reference,
|
|
114
104
|
options,
|
|
115
|
-
ancestorSchema$ids: (0, _util.detachedRootSchema$ids)(
|
|
105
|
+
ancestorSchema$ids: (0, _util.detachedRootSchema$ids)(root.result)
|
|
116
106
|
});
|
|
117
|
-
const dereferencedElement = await
|
|
118
|
-
|
|
119
|
-
...(shouldDetectCircular && {
|
|
120
|
-
skipVisited: 'skip'
|
|
121
|
-
})
|
|
107
|
+
const dereferencedElement = await visitor.dereference(root, {
|
|
108
|
+
ancestorSchema$ids: (0, _util.detachedRootSchema$ids)(root.result)
|
|
122
109
|
});
|
|
123
110
|
|
|
124
|
-
/**
|
|
125
|
-
* If immutable option is set, replay refs from the refSet.
|
|
126
|
-
* Replayed before source descriptions are dereferenced, so they can reuse the refs.
|
|
127
|
-
*/
|
|
128
|
-
if (options.dereference.immutable) {
|
|
129
|
-
mutableRefSet.refs.filter(ref => ref.uri.startsWith('immutable://')).map(ref => new _Reference.default({
|
|
130
|
-
...ref,
|
|
131
|
-
uri: ref.uri.replace(/^immutable:\/\//, '')
|
|
132
|
-
})).forEach(ref => immutableRefSet.add(ref));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
111
|
/**
|
|
136
112
|
* Dereference source descriptions if option is enabled.
|
|
137
113
|
*/
|
|
@@ -151,9 +127,8 @@ class Arazzo1DereferenceStrategy extends _DereferenceStrategy.default {
|
|
|
151
127
|
* If provided as configuration option, then provider is responsible for cleanup.
|
|
152
128
|
*/
|
|
153
129
|
if (options.dereference.refSet === null) {
|
|
154
|
-
|
|
130
|
+
refSet.clean();
|
|
155
131
|
}
|
|
156
|
-
mutableRefSet.clean();
|
|
157
132
|
return dereferencedElement;
|
|
158
133
|
}
|
|
159
134
|
}
|