@speclynx/apidom-reference 4.12.1 → 4.14.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 +12 -0
- package/README.md +99 -4
- package/dist/apidom-reference.browser.js +2353 -293
- package/dist/apidom-reference.browser.min.js +1 -1
- package/package.json +33 -28
- package/src/bundle/strategies/openapi-2/index.cjs +94 -0
- package/src/bundle/strategies/openapi-2/index.mjs +81 -0
- package/src/bundle/strategies/openapi-2/visitor.cjs +559 -0
- package/src/bundle/strategies/openapi-2/visitor.mjs +551 -0
- package/src/bundle/strategies/openapi-3-0/visitor.cjs +10 -18
- package/src/bundle/strategies/openapi-3-0/visitor.mjs +10 -18
- package/src/bundle/strategies/openapi-3-1/index.cjs +59 -2
- package/src/bundle/strategies/openapi-3-1/index.mjs +54 -2
- package/src/bundle/strategies/openapi-3-1/visitor.cjs +811 -0
- package/src/bundle/strategies/openapi-3-1/visitor.mjs +803 -0
- package/src/configuration/saturated.cjs +12 -11
- package/src/configuration/saturated.mjs +2 -1
- package/src/util/url.cjs +26 -4
- package/src/util/url.mjs +23 -2
- package/types/apidom-reference.d.ts +10 -0
- package/types/bundle/strategies/openapi-2/index.d.ts +31 -0
- package/types/bundle/strategies/openapi-2/visitor.d.ts +131 -0
- package/types/bundle/strategies/openapi-3-0/visitor.d.ts +2 -2
- package/types/bundle/strategies/openapi-3-1/index.d.ts +6 -1
- package/types/bundle/strategies/openapi-3-1/visitor.d.ts +141 -0
- package/types/util/url.d.ts +8 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { propEq } from 'ramda';
|
|
2
|
+
import { isElement, isObjectElement, isStringElement, ObjectElement, AnnotationElement, cloneDeep } from '@speclynx/apidom-datamodel';
|
|
3
|
+
import { toValue, toYAML, fixedFields } from '@speclynx/apidom-core';
|
|
4
|
+
import { traverseAsync } from '@speclynx/apidom-traverse';
|
|
5
|
+
import { evaluate, escape, parse as parseJSONPointer, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
|
|
6
|
+
import { DefinitionsElement, ParametersDefinitionsElement, ResponsesDefinitionsElement, SwaggerElement, isReferenceElement, isJSONReferenceElement, isReferenceLikeElement, isJSONReferenceLikeElement, isPathItemElement, refract, refractReference, refractJSONReference, refractPathItem } from '@speclynx/apidom-ns-openapi-2';
|
|
7
|
+
import BundleError from "../../../errors/BundleError.mjs";
|
|
8
|
+
import UnresolvableReferenceError from "../../../errors/UnresolvableReferenceError.mjs";
|
|
9
|
+
import MaximumBundleDepthError from "../../../errors/MaximumBundleDepthError.mjs";
|
|
10
|
+
import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
|
|
11
|
+
import * as url from "../../../util/url.mjs";
|
|
12
|
+
import parse from "../../../parse/index.mjs";
|
|
13
|
+
import Reference from "../../../Reference.mjs";
|
|
14
|
+
import { toPascalCase, sanitizeComponentName, uniqueName as resolveUniqueName } from "../../util.mjs";
|
|
15
|
+
/**
|
|
16
|
+
* Maps the `referenced-element` meta value (set during refraction) to the
|
|
17
|
+
* top-level Swagger Object field where the referenced element should be
|
|
18
|
+
* hoisted. Unlike OpenAPI 3.x, OpenAPI 2.0 has no single Components Object;
|
|
19
|
+
* reusable definitions live directly under the document root. Field names are
|
|
20
|
+
* sourced from the Swagger Object fixed fields so they cannot drift from the
|
|
21
|
+
* namespace definition.
|
|
22
|
+
*/
|
|
23
|
+
const sf = fixedFields(SwaggerElement, {
|
|
24
|
+
indexed: true
|
|
25
|
+
});
|
|
26
|
+
const fieldByReferencedElement = {
|
|
27
|
+
schema: sf.definitions.name,
|
|
28
|
+
parameter: sf.parameters.name,
|
|
29
|
+
response: sf.responses.name
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Maps a top-level Swagger Object field to the named-content element the
|
|
34
|
+
* namespace uses for it, so a bundle-created field matches a parsed/refracted
|
|
35
|
+
* one (e.g. carries the `definitions` class) rather than being a generic
|
|
36
|
+
* object.
|
|
37
|
+
*/
|
|
38
|
+
const fieldElementByField = {
|
|
39
|
+
[sf.definitions.name]: DefinitionsElement,
|
|
40
|
+
[sf.parameters.name]: ParametersDefinitionsElement,
|
|
41
|
+
[sf.responses.name]: ResponsesDefinitionsElement
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @public
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
class OpenAPI2BundleVisitor {
|
|
52
|
+
reference;
|
|
53
|
+
options;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The base URI (without hash) of the entry document. A reference is
|
|
57
|
+
* considered internal when it resolves to this URI and external otherwise.
|
|
58
|
+
* Derived from the shared refSet's root reference.
|
|
59
|
+
*/
|
|
60
|
+
get entryURI() {
|
|
61
|
+
return url.stripHash(this.reference.refSet?.rootRef?.uri ?? '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The entry parse result. Derived from the shared refSet's root reference.
|
|
66
|
+
*/
|
|
67
|
+
get entryParseResult() {
|
|
68
|
+
return this.reference.refSet.rootRef.value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The entry document element. Hoisted external fragments are placed into its
|
|
73
|
+
* top-level definition fields. The `canBundle` guard guarantees it is a
|
|
74
|
+
* SwaggerElement.
|
|
75
|
+
*/
|
|
76
|
+
get entryResult() {
|
|
77
|
+
return this.entryParseResult.result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Shared across the entry document and every external document visitor.
|
|
82
|
+
* Guarantees each external fragment is hoisted exactly once and gives us a
|
|
83
|
+
* stable internal pointer to rewrite every referencing element to.
|
|
84
|
+
*/
|
|
85
|
+
assignments;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Component names already reserved per definition field. Reserved before
|
|
89
|
+
* recursion so collision suffixing (`Pet`, `Pet-2`, ...) accounts for
|
|
90
|
+
* components that are still being bundled.
|
|
91
|
+
*/
|
|
92
|
+
reservedNames;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Caches refracted fragments keyed by source element and target element type.
|
|
96
|
+
* The type is part of the key because the same source can be refracted into
|
|
97
|
+
* different semantic elements depending on the referencing context (e.g. a
|
|
98
|
+
* generic object referenced once as a Parameter and once as a Response).
|
|
99
|
+
*/
|
|
100
|
+
refractCache;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Canonical URIs of Path Item Objects currently being inlined. Path Items
|
|
104
|
+
* are not a top-level definition field in OpenAPI 2.0, so the `assignments`
|
|
105
|
+
* map can't break their cycles. This stack guards against circular external
|
|
106
|
+
* Path Item references.
|
|
107
|
+
*/
|
|
108
|
+
inlineStack;
|
|
109
|
+
constructor({
|
|
110
|
+
reference,
|
|
111
|
+
options,
|
|
112
|
+
assignments = new Map(),
|
|
113
|
+
reservedNames = new Map(),
|
|
114
|
+
refractCache = new WeakMap(),
|
|
115
|
+
inlineStack = new Set()
|
|
116
|
+
}) {
|
|
117
|
+
this.reference = reference;
|
|
118
|
+
this.options = options;
|
|
119
|
+
this.inlineStack = inlineStack;
|
|
120
|
+
this.assignments = assignments;
|
|
121
|
+
this.reservedNames = reservedNames;
|
|
122
|
+
this.refractCache = refractCache;
|
|
123
|
+
}
|
|
124
|
+
getRefracted(source, type) {
|
|
125
|
+
return this.refractCache.get(source)?.get(type);
|
|
126
|
+
}
|
|
127
|
+
setRefracted(source, type, refracted) {
|
|
128
|
+
let byType = this.refractCache.get(source);
|
|
129
|
+
if (byType === undefined) {
|
|
130
|
+
byType = new Map();
|
|
131
|
+
this.refractCache.set(source, byType);
|
|
132
|
+
}
|
|
133
|
+
byType.set(type, refracted);
|
|
134
|
+
}
|
|
135
|
+
toBaseURI(uri) {
|
|
136
|
+
return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
|
|
137
|
+
}
|
|
138
|
+
async toReference(uri) {
|
|
139
|
+
// detect maximum depth of resolution
|
|
140
|
+
if (this.reference.depth >= this.options.resolve.maxDepth) {
|
|
141
|
+
throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`, {
|
|
142
|
+
maxDepth: this.options.resolve.maxDepth,
|
|
143
|
+
uri: this.reference.uri
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const baseURI = this.toBaseURI(uri);
|
|
147
|
+
const {
|
|
148
|
+
refSet
|
|
149
|
+
} = this.reference;
|
|
150
|
+
|
|
151
|
+
// we've already processed this Reference in past
|
|
152
|
+
if (refSet.has(baseURI)) {
|
|
153
|
+
return refSet.find(propEq(baseURI, 'uri'));
|
|
154
|
+
}
|
|
155
|
+
const parseResult = await parse(url.unsanitize(baseURI), {
|
|
156
|
+
...this.options,
|
|
157
|
+
parse: {
|
|
158
|
+
...this.options.parse,
|
|
159
|
+
mediaType: 'text/plain'
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
const reference = new Reference({
|
|
163
|
+
uri: baseURI,
|
|
164
|
+
value: parseResult,
|
|
165
|
+
depth: this.reference.depth + 1
|
|
166
|
+
});
|
|
167
|
+
refSet.add(reference);
|
|
168
|
+
return reference;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Normalizes a self-reference (e.g. `./root.json#/definitions/Pet`)
|
|
173
|
+
* to a bare fragment (`#/definitions/Pet`) so the bundled document stays
|
|
174
|
+
* transferable. Bare fragments are returned unchanged.
|
|
175
|
+
*/
|
|
176
|
+
normalizeSelfReference(ref) {
|
|
177
|
+
return ref.startsWith('#') ? ref : url.getHash(ref);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Handles an error according to the `bundle.continueOnError` option.
|
|
182
|
+
*
|
|
183
|
+
* For new errors: wraps in UnresolvableReferenceError with structured
|
|
184
|
+
* context. For errors already wrapped by a nested visitor: prepends the
|
|
185
|
+
* current hop to the trace.
|
|
186
|
+
*
|
|
187
|
+
* Inner/intermediate visitors always throw to let the trace accumulate. Only
|
|
188
|
+
* the entry document visitor respects continueOnError (callback/swallow/throw).
|
|
189
|
+
*/
|
|
190
|
+
handleError(message, error, referencingElement, refFieldName, refFieldValue, visitorPath) {
|
|
191
|
+
// a collision reported as `error` is a deliberate hard stop with no
|
|
192
|
+
// resolution-failure analog, so pass it through unchanged. A maxDepth
|
|
193
|
+
// breach (MaximumBundleDepthError, which also extends BundleError) is a
|
|
194
|
+
// resolution failure and flows through below — wrapped and subject to
|
|
195
|
+
// continueOnError, like dereferencing.
|
|
196
|
+
if (error instanceof BundleError && !(error instanceof MaximumBundleDepthError)) {
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
const {
|
|
200
|
+
continueOnError
|
|
201
|
+
} = this.options.bundle;
|
|
202
|
+
const isEntryDocument = url.stripHash(this.reference.refSet?.rootRef?.uri ?? '') === this.reference.uri;
|
|
203
|
+
const uri = this.reference.uri;
|
|
204
|
+
const type = referencingElement.element;
|
|
205
|
+
const codeFrame = toYAML(referencingElement);
|
|
206
|
+
const location = visitorPath.formatPath();
|
|
207
|
+
const hop = {
|
|
208
|
+
uri,
|
|
209
|
+
type,
|
|
210
|
+
refFieldName,
|
|
211
|
+
refFieldValue,
|
|
212
|
+
location,
|
|
213
|
+
codeFrame
|
|
214
|
+
};
|
|
215
|
+
let unresolvedError;
|
|
216
|
+
if (error instanceof UnresolvableReferenceError) {
|
|
217
|
+
// @ts-ignore
|
|
218
|
+
error.trace = [hop, ...error.trace];
|
|
219
|
+
unresolvedError = error;
|
|
220
|
+
} else {
|
|
221
|
+
unresolvedError = new UnresolvableReferenceError(message, {
|
|
222
|
+
cause: error,
|
|
223
|
+
type,
|
|
224
|
+
uri,
|
|
225
|
+
location,
|
|
226
|
+
codeFrame,
|
|
227
|
+
refFieldName,
|
|
228
|
+
refFieldValue,
|
|
229
|
+
trace: []
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (!isEntryDocument || continueOnError === false) throw unresolvedError;
|
|
233
|
+
if (typeof continueOnError === 'function') continueOnError(unresolvedError);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Lazily creates the requested top-level definition field on the entry
|
|
238
|
+
* document, returning the field where a fragment is hoisted.
|
|
239
|
+
*/
|
|
240
|
+
ensureField(field) {
|
|
241
|
+
const entryResult = this.entryResult;
|
|
242
|
+
let fieldElement = entryResult.get(field);
|
|
243
|
+
if (!isObjectElement(fieldElement)) {
|
|
244
|
+
const FieldElement = fieldElementByField[field] ?? ObjectElement;
|
|
245
|
+
fieldElement = new FieldElement();
|
|
246
|
+
entryResult.set(field, fieldElement);
|
|
247
|
+
}
|
|
248
|
+
return fieldElement;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Derives a component name from the referenced JSON Pointer's last token,
|
|
253
|
+
* falling back to the referenced file's basename.
|
|
254
|
+
*/
|
|
255
|
+
basenameOf(jsonPointer, baseURI) {
|
|
256
|
+
const tokens = parseJSONPointer(jsonPointer).tree;
|
|
257
|
+
const lastToken = tokens.length > 0 ? tokens[tokens.length - 1] : '';
|
|
258
|
+
return lastToken || url.getBasename(baseURI) || 'Schema';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Computes the base component name (before collision suffixing) according to
|
|
263
|
+
* the configured naming strategy.
|
|
264
|
+
*/
|
|
265
|
+
baseName(element, field, jsonPointer, baseURI) {
|
|
266
|
+
// strategy specific options take precedence over the top-level bundle options
|
|
267
|
+
const componentNamesStrategy = this.options.bundle.strategyOpts['openapi-2']?.componentNamesStrategy ?? this.options.bundle.componentNamesStrategy;
|
|
268
|
+
if (typeof componentNamesStrategy === 'function') {
|
|
269
|
+
const resolved = componentNamesStrategy({
|
|
270
|
+
element,
|
|
271
|
+
field,
|
|
272
|
+
jsonPointer,
|
|
273
|
+
baseURI
|
|
274
|
+
});
|
|
275
|
+
if (typeof resolved === 'string' && resolved !== '') {
|
|
276
|
+
return resolved;
|
|
277
|
+
}
|
|
278
|
+
return this.basenameOf(jsonPointer, baseURI);
|
|
279
|
+
}
|
|
280
|
+
if (componentNamesStrategy === 'title') {
|
|
281
|
+
const title = isObjectElement(element) ? toValue(element.get('title')) : undefined;
|
|
282
|
+
if (typeof title === 'string' && title.trim() !== '') {
|
|
283
|
+
const name = sanitizeComponentName(toPascalCase(title));
|
|
284
|
+
if (name !== '') return name;
|
|
285
|
+
}
|
|
286
|
+
// fall back to basename when no usable title is present
|
|
287
|
+
}
|
|
288
|
+
return this.basenameOf(jsonPointer, baseURI);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Computes a collision-free component name within the target field. A name is
|
|
293
|
+
* taken if it's already placed in the field or reserved by a component that
|
|
294
|
+
* is still being bundled (reserved before recursion).
|
|
295
|
+
*/
|
|
296
|
+
uniqueName(candidate, field) {
|
|
297
|
+
const fieldElement = this.ensureField(field);
|
|
298
|
+
const reserved = this.reservedNames.get(field) ?? new Set();
|
|
299
|
+
return resolveUniqueName(candidate, name => fieldElement.hasKey(name) || reserved.has(name));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Bundles a Reference Object (parameters/responses) or a JSON Reference
|
|
304
|
+
* Object (schemas). The two reference flavors differ only in their predicate
|
|
305
|
+
* and refraction helpers, so the shared logic is parameterized here.
|
|
306
|
+
*/
|
|
307
|
+
async bundleReference(path, referencingElement, isSemanticReference, isReferenceLike, refractReferenceFn, label) {
|
|
308
|
+
const $ref = toValue(referencingElement.$ref);
|
|
309
|
+
const retrievalURI = this.toBaseURI($ref);
|
|
310
|
+
const isInternalReference = this.entryURI === retrievalURI;
|
|
311
|
+
const isExternalReference = !isInternalReference;
|
|
312
|
+
if (isInternalReference) {
|
|
313
|
+
// normalize self-references to a bare fragment so the bundled
|
|
314
|
+
// document stays transferable; bare fragments are left untouched
|
|
315
|
+
referencingElement.set('$ref', this.normalizeSelfReference($ref));
|
|
316
|
+
path.skip();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// honor the resolve.external switch
|
|
321
|
+
if (!this.options.resolve.external && isExternalReference) {
|
|
322
|
+
path.skip();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const $refBaseURI = url.resolve(retrievalURI, $ref);
|
|
326
|
+
const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
|
|
327
|
+
|
|
328
|
+
// the target field depends on the referencing context, so it's part of the
|
|
329
|
+
// identity: the same external target referenced as e.g. both a parameter and
|
|
330
|
+
// a response must be hoisted into both fields. The refractor sets
|
|
331
|
+
// `referenced-element` for every Reference Object position; the `schema`
|
|
332
|
+
// fallback is a defensive default for the (unexpected) untagged case.
|
|
333
|
+
const referencedElementType = referencingElement.meta.get('referenced-element');
|
|
334
|
+
const field = fieldByReferencedElement[referencedElementType] ?? fieldByReferencedElement.schema;
|
|
335
|
+
const canonicalKey = `${field}\t${retrievalURI}#${jsonPointer}`;
|
|
336
|
+
|
|
337
|
+
// already hoisted (or being hoisted) — just rewrite the pointer.
|
|
338
|
+
//
|
|
339
|
+
// this is also how circular references terminate: the assignment is
|
|
340
|
+
// reserved BEFORE recursing into the fragment (see below), so a reference
|
|
341
|
+
// that cycles back (directly or indirectly) finds its pointer already
|
|
342
|
+
// reserved here and is rewritten without recursing. Cycles are deliberately
|
|
343
|
+
// preserved as internal `$ref`s pointing at each other — that is valid,
|
|
344
|
+
// serializable output; resolving the cycle (if desired) is dereferencing's
|
|
345
|
+
// job, not bundling's.
|
|
346
|
+
if (this.assignments.has(canonicalKey)) {
|
|
347
|
+
referencingElement.set('$ref', this.assignments.get(canonicalKey));
|
|
348
|
+
path.skip();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
// detect maximum depth of bundling
|
|
353
|
+
if (this.reference.depth >= this.options.bundle.maxDepth) {
|
|
354
|
+
throw new MaximumBundleDepthError(`Maximum bundle depth of "${this.options.bundle.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
|
|
355
|
+
maxDepth: this.options.bundle.maxDepth,
|
|
356
|
+
uri: this.reference.uri
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const reference = await this.toReference($ref);
|
|
360
|
+
|
|
361
|
+
// possibly non-semantic fragment
|
|
362
|
+
let referencedElement = evaluate(reference.value.result, jsonPointer);
|
|
363
|
+
|
|
364
|
+
// apply semantics to the fragment so nested references become Reference Objects
|
|
365
|
+
if (referencedElement.element !== referencedElementType && !isSemanticReference(referencedElement)) {
|
|
366
|
+
const cached = this.getRefracted(referencedElement, referencedElementType);
|
|
367
|
+
if (cached !== undefined) {
|
|
368
|
+
referencedElement = cached;
|
|
369
|
+
} else if (isReferenceLike(referencedElement)) {
|
|
370
|
+
const sourceElement = referencedElement;
|
|
371
|
+
referencedElement = refractReferenceFn(referencedElement);
|
|
372
|
+
referencedElement.meta.set('referenced-element', referencedElementType);
|
|
373
|
+
this.setRefracted(sourceElement, referencedElementType, referencedElement);
|
|
374
|
+
} else {
|
|
375
|
+
const sourceElement = referencedElement;
|
|
376
|
+
referencedElement = refract(referencedElement, {
|
|
377
|
+
element: referencedElementType
|
|
378
|
+
});
|
|
379
|
+
this.setRefracted(sourceElement, referencedElementType, referencedElement);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const preferredName = this.baseName(referencedElement, field, jsonPointer, retrievalURI);
|
|
383
|
+
const componentName = this.uniqueName(preferredName, field);
|
|
384
|
+
const internalPointer = `#/${field}/${escape(componentName)}`;
|
|
385
|
+
|
|
386
|
+
// a rename means two distinct targets resolved to the same name; each
|
|
387
|
+
// keeps its own component (and origin), so report the rename per severity.
|
|
388
|
+
// strategy specific options take precedence over the top-level bundle options
|
|
389
|
+
const onComponentNameCollision = this.options.bundle.strategyOpts['openapi-2']?.onComponentNameCollision ?? this.options.bundle.onComponentNameCollision;
|
|
390
|
+
if (componentName !== preferredName && onComponentNameCollision !== 'off') {
|
|
391
|
+
const message = `Component "${preferredName}" in ${field} is referenced with the same name but different content. Renamed to "${componentName}".`;
|
|
392
|
+
if (onComponentNameCollision === 'error') {
|
|
393
|
+
throw new BundleError(message);
|
|
394
|
+
}
|
|
395
|
+
const annotation = new AnnotationElement(message);
|
|
396
|
+
annotation.classes.push('warning');
|
|
397
|
+
annotation.code = 'bundle-component-name-collision';
|
|
398
|
+
// append (never prepend) so the result element stays ahead of annotations
|
|
399
|
+
this.entryParseResult.content.push(annotation);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// reserve the assignment and component name before recursing so cyclic
|
|
403
|
+
// references terminate
|
|
404
|
+
this.assignments.set(canonicalKey, internalPointer);
|
|
405
|
+
if (!this.reservedNames.has(field)) this.reservedNames.set(field, new Set());
|
|
406
|
+
this.reservedNames.get(field).add(componentName);
|
|
407
|
+
|
|
408
|
+
// own a copy of the fragment and bundle its own external references
|
|
409
|
+
const hoistedElement = cloneDeep(referencedElement);
|
|
410
|
+
const visitor = new OpenAPI2BundleVisitor({
|
|
411
|
+
reference,
|
|
412
|
+
options: this.options,
|
|
413
|
+
assignments: this.assignments,
|
|
414
|
+
reservedNames: this.reservedNames,
|
|
415
|
+
refractCache: this.refractCache,
|
|
416
|
+
inlineStack: this.inlineStack
|
|
417
|
+
});
|
|
418
|
+
const bundledElement = await traverseAsync(hoistedElement, visitor, {
|
|
419
|
+
mutable: true
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// annotate the hoisted fragment with info about its origin
|
|
423
|
+
if (isElement(bundledElement)) {
|
|
424
|
+
bundledElement.meta.set('ref-origin', reference.uri);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// place the bundled fragment into the entry document's definitions
|
|
428
|
+
this.ensureField(field).set(componentName, bundledElement);
|
|
429
|
+
|
|
430
|
+
// rewrite the referencing element to point at the hoisted fragment
|
|
431
|
+
referencingElement.set('$ref', internalPointer);
|
|
432
|
+
path.skip();
|
|
433
|
+
} catch (error) {
|
|
434
|
+
this.handleError(`Error while bundling ${label}. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
|
|
435
|
+
path.skip();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Reference Object covers reusable Parameter and Response Objects, which are
|
|
441
|
+
* hoisted into the top-level `parameters` and `responses` fields.
|
|
442
|
+
*/
|
|
443
|
+
async ReferenceElement(path) {
|
|
444
|
+
await this.bundleReference(path, path.node, isReferenceElement, isReferenceLikeElement, refractReference, 'Reference Object');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* JSON Reference Object covers reusable Schema Objects, which are hoisted
|
|
449
|
+
* into the top-level `definitions` field.
|
|
450
|
+
*/
|
|
451
|
+
async JSONReferenceElement(path) {
|
|
452
|
+
await this.bundleReference(path, path.node, isJSONReferenceElement, isJSONReferenceLikeElement, refractJSONReference, 'JSON Reference Object');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Path Item Object is not a top-level definition field in OpenAPI 2.0, so an
|
|
457
|
+
* external Path Item reference cannot be hoisted; it is inlined in place
|
|
458
|
+
* instead, after bundling its own external references.
|
|
459
|
+
*/
|
|
460
|
+
async PathItemElement(path) {
|
|
461
|
+
const pathItemElement = path.node;
|
|
462
|
+
|
|
463
|
+
// ignore Path Item Objects without a $ref field
|
|
464
|
+
if (!isStringElement(pathItemElement.$ref)) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const $ref = toValue(pathItemElement.$ref);
|
|
468
|
+
const retrievalURI = this.toBaseURI($ref);
|
|
469
|
+
const isInternalReference = this.entryURI === retrievalURI;
|
|
470
|
+
const isExternalReference = !isInternalReference;
|
|
471
|
+
if (isInternalReference) {
|
|
472
|
+
// normalize self-references to a bare fragment so the bundled
|
|
473
|
+
// document stays transferable; bare fragments are left untouched
|
|
474
|
+
pathItemElement.set('$ref', this.normalizeSelfReference($ref));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// honor the resolve.external switch
|
|
479
|
+
if (!this.options.resolve.external && isExternalReference) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const $refBaseURI = url.resolve(retrievalURI, $ref);
|
|
483
|
+
const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
|
|
484
|
+
const canonicalKey = `${retrievalURI}#${jsonPointer}`;
|
|
485
|
+
|
|
486
|
+
// a circular external Path Item reference cannot be inlined without
|
|
487
|
+
// recursing forever; leave the $ref in place to break the cycle
|
|
488
|
+
if (this.inlineStack.has(canonicalKey)) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
// detect maximum depth of bundling
|
|
493
|
+
if (this.reference.depth >= this.options.bundle.maxDepth) {
|
|
494
|
+
throw new MaximumBundleDepthError(`Maximum bundle depth of "${this.options.bundle.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
|
|
495
|
+
maxDepth: this.options.bundle.maxDepth,
|
|
496
|
+
uri: this.reference.uri
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
const reference = await this.toReference($ref);
|
|
500
|
+
let referencedElement = evaluate(reference.value.result, jsonPointer);
|
|
501
|
+
|
|
502
|
+
// apply Path Item semantics to the referenced fragment
|
|
503
|
+
if (!isPathItemElement(referencedElement)) {
|
|
504
|
+
const cached = this.getRefracted(referencedElement, 'pathItem');
|
|
505
|
+
if (cached !== undefined) {
|
|
506
|
+
referencedElement = cached;
|
|
507
|
+
} else {
|
|
508
|
+
const sourceElement = referencedElement;
|
|
509
|
+
referencedElement = refractPathItem(referencedElement);
|
|
510
|
+
this.setRefracted(sourceElement, 'pathItem', referencedElement);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// own a copy and bundle the external references it contains
|
|
515
|
+
const inlinedElement = cloneDeep(referencedElement);
|
|
516
|
+
this.inlineStack.add(canonicalKey);
|
|
517
|
+
const visitor = new OpenAPI2BundleVisitor({
|
|
518
|
+
reference,
|
|
519
|
+
options: this.options,
|
|
520
|
+
assignments: this.assignments,
|
|
521
|
+
reservedNames: this.reservedNames,
|
|
522
|
+
refractCache: this.refractCache,
|
|
523
|
+
inlineStack: this.inlineStack
|
|
524
|
+
});
|
|
525
|
+
let bundledElement;
|
|
526
|
+
try {
|
|
527
|
+
bundledElement = await traverseAsync(inlinedElement, visitor, {
|
|
528
|
+
mutable: true
|
|
529
|
+
});
|
|
530
|
+
} finally {
|
|
531
|
+
this.inlineStack.delete(canonicalKey);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// merge sibling fields from the referencing Path Item over the referenced
|
|
535
|
+
// one. The referencing `$ref` is being resolved away and is skipped; the
|
|
536
|
+
// referenced element keeps its own `$ref` if one survived (i.e. it was
|
|
537
|
+
// left in place to break a circular external Path Item chain).
|
|
538
|
+
pathItemElement.forEach((_value, keyElement, item) => {
|
|
539
|
+
const key = toValue(keyElement);
|
|
540
|
+
if (key === '$ref') return;
|
|
541
|
+
bundledElement.remove(key);
|
|
542
|
+
bundledElement.content.push(item);
|
|
543
|
+
});
|
|
544
|
+
bundledElement.meta.set('ref-origin', reference.uri);
|
|
545
|
+
path.replaceWith(bundledElement);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
this.handleError(`Error while bundling Path Item Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, pathItemElement, '$ref', $ref, path);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
export default OpenAPI2BundleVisitor;
|
|
@@ -188,11 +188,11 @@ class OpenAPI3_0BundleVisitor {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
|
-
* Normalizes a self-
|
|
191
|
+
* Normalizes a self-reference (e.g. `./root.json#/components/schemas/Pet`)
|
|
192
192
|
* to a bare fragment (`#/components/schemas/Pet`) so the bundled document stays
|
|
193
193
|
* transferable. Bare fragments are returned unchanged.
|
|
194
194
|
*/
|
|
195
|
-
|
|
195
|
+
normalizeSelfReference(ref) {
|
|
196
196
|
return ref.startsWith('#') ? ref : url.getHash(ref);
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -278,16 +278,8 @@ class OpenAPI3_0BundleVisitor {
|
|
|
278
278
|
*/
|
|
279
279
|
basenameOf(jsonPointer, baseURI) {
|
|
280
280
|
const tokens = (0, _apidomJsonPointer.parse)(jsonPointer).tree;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const lastSlash = baseURI.lastIndexOf('/');
|
|
284
|
-
const lastDot = baseURI.lastIndexOf('.');
|
|
285
|
-
const fileName = baseURI.slice(lastSlash + 1);
|
|
286
|
-
// strip the extension only when the last dot belongs to the file name
|
|
287
|
-
candidate = lastDot > lastSlash ? baseURI.slice(lastSlash + 1, lastDot) : fileName;
|
|
288
|
-
}
|
|
289
|
-
if (candidate === '') candidate = 'Schema';
|
|
290
|
-
return candidate;
|
|
281
|
+
const lastToken = tokens.length > 0 ? tokens[tokens.length - 1] : '';
|
|
282
|
+
return lastToken || url.getBasename(baseURI) || 'Schema';
|
|
291
283
|
}
|
|
292
284
|
|
|
293
285
|
/**
|
|
@@ -337,9 +329,9 @@ class OpenAPI3_0BundleVisitor {
|
|
|
337
329
|
const isInternalReference = this.entryURI === retrievalURI;
|
|
338
330
|
const isExternalReference = !isInternalReference;
|
|
339
331
|
if (isInternalReference) {
|
|
340
|
-
// normalize self-
|
|
332
|
+
// normalize self-references to a bare fragment so the bundled
|
|
341
333
|
// document stays transferable; bare fragments are left untouched
|
|
342
|
-
referencingElement.set('$ref', this.
|
|
334
|
+
referencingElement.set('$ref', this.normalizeSelfReference($ref));
|
|
343
335
|
path.skip();
|
|
344
336
|
return;
|
|
345
337
|
}
|
|
@@ -481,9 +473,9 @@ class OpenAPI3_0BundleVisitor {
|
|
|
481
473
|
const isInternalReference = this.entryURI === retrievalURI;
|
|
482
474
|
const isExternalReference = !isInternalReference;
|
|
483
475
|
if (isInternalReference) {
|
|
484
|
-
// normalize self-
|
|
476
|
+
// normalize self-references to a bare fragment so the bundled
|
|
485
477
|
// document stays transferable; bare fragments are left untouched
|
|
486
|
-
pathItemElement.set('$ref', this.
|
|
478
|
+
pathItemElement.set('$ref', this.normalizeSelfReference($ref));
|
|
487
479
|
return;
|
|
488
480
|
}
|
|
489
481
|
|
|
@@ -628,9 +620,9 @@ class OpenAPI3_0BundleVisitor {
|
|
|
628
620
|
const retrievalURI = this.toBaseURI(operationRef);
|
|
629
621
|
const isInternalReference = this.entryURI === retrievalURI;
|
|
630
622
|
if (isInternalReference) {
|
|
631
|
-
// normalize self-
|
|
623
|
+
// normalize a self-reference operationRef to a bare fragment so the bundled
|
|
632
624
|
// document stays transferable; bare fragments are left untouched
|
|
633
|
-
linkElement.set('operationRef', this.
|
|
625
|
+
linkElement.set('operationRef', this.normalizeSelfReference(operationRef));
|
|
634
626
|
return;
|
|
635
627
|
}
|
|
636
628
|
|
|
@@ -180,11 +180,11 @@ class OpenAPI3_0BundleVisitor {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Normalizes a self-
|
|
183
|
+
* Normalizes a self-reference (e.g. `./root.json#/components/schemas/Pet`)
|
|
184
184
|
* to a bare fragment (`#/components/schemas/Pet`) so the bundled document stays
|
|
185
185
|
* transferable. Bare fragments are returned unchanged.
|
|
186
186
|
*/
|
|
187
|
-
|
|
187
|
+
normalizeSelfReference(ref) {
|
|
188
188
|
return ref.startsWith('#') ? ref : url.getHash(ref);
|
|
189
189
|
}
|
|
190
190
|
|
|
@@ -270,16 +270,8 @@ class OpenAPI3_0BundleVisitor {
|
|
|
270
270
|
*/
|
|
271
271
|
basenameOf(jsonPointer, baseURI) {
|
|
272
272
|
const tokens = parseJSONPointer(jsonPointer).tree;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const lastSlash = baseURI.lastIndexOf('/');
|
|
276
|
-
const lastDot = baseURI.lastIndexOf('.');
|
|
277
|
-
const fileName = baseURI.slice(lastSlash + 1);
|
|
278
|
-
// strip the extension only when the last dot belongs to the file name
|
|
279
|
-
candidate = lastDot > lastSlash ? baseURI.slice(lastSlash + 1, lastDot) : fileName;
|
|
280
|
-
}
|
|
281
|
-
if (candidate === '') candidate = 'Schema';
|
|
282
|
-
return candidate;
|
|
273
|
+
const lastToken = tokens.length > 0 ? tokens[tokens.length - 1] : '';
|
|
274
|
+
return lastToken || url.getBasename(baseURI) || 'Schema';
|
|
283
275
|
}
|
|
284
276
|
|
|
285
277
|
/**
|
|
@@ -329,9 +321,9 @@ class OpenAPI3_0BundleVisitor {
|
|
|
329
321
|
const isInternalReference = this.entryURI === retrievalURI;
|
|
330
322
|
const isExternalReference = !isInternalReference;
|
|
331
323
|
if (isInternalReference) {
|
|
332
|
-
// normalize self-
|
|
324
|
+
// normalize self-references to a bare fragment so the bundled
|
|
333
325
|
// document stays transferable; bare fragments are left untouched
|
|
334
|
-
referencingElement.set('$ref', this.
|
|
326
|
+
referencingElement.set('$ref', this.normalizeSelfReference($ref));
|
|
335
327
|
path.skip();
|
|
336
328
|
return;
|
|
337
329
|
}
|
|
@@ -473,9 +465,9 @@ class OpenAPI3_0BundleVisitor {
|
|
|
473
465
|
const isInternalReference = this.entryURI === retrievalURI;
|
|
474
466
|
const isExternalReference = !isInternalReference;
|
|
475
467
|
if (isInternalReference) {
|
|
476
|
-
// normalize self-
|
|
468
|
+
// normalize self-references to a bare fragment so the bundled
|
|
477
469
|
// document stays transferable; bare fragments are left untouched
|
|
478
|
-
pathItemElement.set('$ref', this.
|
|
470
|
+
pathItemElement.set('$ref', this.normalizeSelfReference($ref));
|
|
479
471
|
return;
|
|
480
472
|
}
|
|
481
473
|
|
|
@@ -620,9 +612,9 @@ class OpenAPI3_0BundleVisitor {
|
|
|
620
612
|
const retrievalURI = this.toBaseURI(operationRef);
|
|
621
613
|
const isInternalReference = this.entryURI === retrievalURI;
|
|
622
614
|
if (isInternalReference) {
|
|
623
|
-
// normalize self-
|
|
615
|
+
// normalize a self-reference operationRef to a bare fragment so the bundled
|
|
624
616
|
// document stays transferable; bare fragments are left untouched
|
|
625
|
-
linkElement.set('operationRef', this.
|
|
617
|
+
linkElement.set('operationRef', this.normalizeSelfReference(operationRef));
|
|
626
618
|
return;
|
|
627
619
|
}
|
|
628
620
|
|