@speclynx/apidom-reference 4.10.1 → 4.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +215 -3
  3. package/dist/apidom-reference.browser.js +1515 -191
  4. package/dist/apidom-reference.browser.min.js +1 -1
  5. package/package.json +36 -31
  6. package/src/File.cjs +3 -1
  7. package/src/Reference.cjs +3 -1
  8. package/src/ReferenceSet.cjs +3 -1
  9. package/src/bundle/index.cjs +9 -1
  10. package/src/bundle/index.mjs +6 -0
  11. package/src/bundle/strategies/BundleStrategy.cjs +3 -1
  12. package/src/bundle/strategies/openapi-3-0/index.cjs +94 -0
  13. package/src/bundle/strategies/openapi-3-0/index.mjs +81 -0
  14. package/src/bundle/strategies/openapi-3-0/visitor.cjs +650 -0
  15. package/src/bundle/strategies/openapi-3-0/visitor.mjs +642 -0
  16. package/src/bundle/strategies/openapi-3-1/index.cjs +3 -1
  17. package/src/bundle/util.cjs +42 -0
  18. package/src/bundle/util.mjs +33 -0
  19. package/src/configuration/empty.cjs +9 -2
  20. package/src/configuration/saturated.cjs +19 -11
  21. package/src/configuration/saturated.mjs +2 -1
  22. package/src/dereference/index.cjs +3 -1
  23. package/src/dereference/strategies/DereferenceStrategy.cjs +3 -1
  24. package/src/dereference/strategies/apidom/index.cjs +9 -2
  25. package/src/dereference/strategies/apidom/selectors/element-id.cjs +9 -2
  26. package/src/dereference/strategies/apidom/visitor.cjs +3 -1
  27. package/src/dereference/strategies/arazzo-1/index.cjs +35 -8
  28. package/src/dereference/strategies/arazzo-1/index.mjs +1 -1
  29. package/src/dereference/strategies/arazzo-1/selectors/$anchor.cjs +46 -10
  30. package/src/dereference/strategies/arazzo-1/selectors/uri.cjs +22 -6
  31. package/src/dereference/strategies/arazzo-1/source-descriptions.cjs +3 -1
  32. package/src/dereference/strategies/arazzo-1/util.cjs +16 -4
  33. package/src/dereference/strategies/arazzo-1/visitor.cjs +4 -2
  34. package/src/dereference/strategies/arazzo-1/visitor.mjs +1 -1
  35. package/src/dereference/strategies/asyncapi-2/index.cjs +10 -3
  36. package/src/dereference/strategies/asyncapi-2/index.mjs +1 -1
  37. package/src/dereference/strategies/asyncapi-2/visitor.cjs +5 -3
  38. package/src/dereference/strategies/asyncapi-2/visitor.mjs +2 -2
  39. package/src/dereference/strategies/openapi-2/index.cjs +10 -3
  40. package/src/dereference/strategies/openapi-2/index.mjs +1 -1
  41. package/src/dereference/strategies/openapi-2/visitor.cjs +6 -4
  42. package/src/dereference/strategies/openapi-2/visitor.mjs +3 -3
  43. package/src/dereference/strategies/openapi-3-0/index.cjs +10 -3
  44. package/src/dereference/strategies/openapi-3-0/index.mjs +1 -1
  45. package/src/dereference/strategies/openapi-3-0/visitor.cjs +5 -3
  46. package/src/dereference/strategies/openapi-3-0/visitor.mjs +2 -2
  47. package/src/dereference/strategies/openapi-3-1/index.cjs +29 -7
  48. package/src/dereference/strategies/openapi-3-1/index.mjs +1 -1
  49. package/src/dereference/strategies/openapi-3-1/selectors/$anchor.cjs +22 -5
  50. package/src/dereference/strategies/openapi-3-1/selectors/uri.cjs +16 -4
  51. package/src/dereference/strategies/openapi-3-1/util.cjs +3 -1
  52. package/src/dereference/strategies/openapi-3-1/visitor.cjs +6 -4
  53. package/src/dereference/strategies/openapi-3-1/visitor.mjs +3 -3
  54. package/src/dereference/strategies/overlay-1/extends.cjs +3 -1
  55. package/src/dereference/strategies/overlay-1/index.cjs +9 -2
  56. package/src/dereference/util.cjs +3 -1
  57. package/src/errors/BundleError.cjs +4 -2
  58. package/src/errors/BundleError.mjs +2 -2
  59. package/src/errors/DereferenceError.cjs +3 -1
  60. package/src/errors/EvaluationElementIdError.cjs +3 -1
  61. package/src/errors/EvaluationJsonSchema$anchorError.cjs +3 -1
  62. package/src/errors/EvaluationJsonSchemaUriError.cjs +3 -1
  63. package/src/errors/InvalidJsonSchema$anchorError.cjs +3 -1
  64. package/src/errors/JsonSchema$anchorError.cjs +3 -1
  65. package/src/errors/JsonSchemaUriError.cjs +3 -1
  66. package/src/errors/MaximumBundleDepthError.cjs +3 -1
  67. package/src/errors/MaximumDereferenceDepthError.cjs +3 -1
  68. package/src/errors/MaximumResolveDepthError.cjs +3 -1
  69. package/src/errors/ParseError.cjs +3 -1
  70. package/src/errors/ParserError.cjs +3 -1
  71. package/src/errors/PluginError.cjs +3 -1
  72. package/src/errors/ResolveError.cjs +3 -1
  73. package/src/errors/ResolverError.cjs +3 -1
  74. package/src/errors/UnmatchedBundleStrategyError.cjs +3 -1
  75. package/src/errors/UnmatchedDereferenceStrategyError.cjs +3 -1
  76. package/src/errors/UnmatchedParserError.cjs +3 -1
  77. package/src/errors/UnmatchedResolveStrategyError.cjs +3 -1
  78. package/src/errors/UnmatchedResolverError.cjs +3 -1
  79. package/src/errors/UnresolvableReferenceError.cjs +8 -3
  80. package/src/errors/UnresolvableReferenceError.mjs +5 -2
  81. package/src/index.cjs +209 -36
  82. package/src/options/index.cjs +71 -2
  83. package/src/options/index.mjs +68 -1
  84. package/src/options/util.cjs +3 -1
  85. package/src/parse/index.cjs +3 -1
  86. package/src/parse/parsers/Parser.cjs +3 -1
  87. package/src/parse/parsers/apidom-json/index.cjs +3 -1
  88. package/src/parse/parsers/arazzo-json-1/index.cjs +9 -2
  89. package/src/parse/parsers/arazzo-json-1/source-descriptions.cjs +3 -1
  90. package/src/parse/parsers/arazzo-yaml-1/index.cjs +9 -2
  91. package/src/parse/parsers/arazzo-yaml-1/source-descriptions.cjs +3 -1
  92. package/src/parse/parsers/asyncapi-json-2/index.cjs +3 -1
  93. package/src/parse/parsers/asyncapi-yaml-2/index.cjs +3 -1
  94. package/src/parse/parsers/binary/index-browser.cjs +3 -1
  95. package/src/parse/parsers/binary/index-node.cjs +3 -1
  96. package/src/parse/parsers/json/index.cjs +3 -1
  97. package/src/parse/parsers/openapi-json-2/index.cjs +3 -1
  98. package/src/parse/parsers/openapi-json-3-0/index.cjs +3 -1
  99. package/src/parse/parsers/openapi-json-3-1/index.cjs +3 -1
  100. package/src/parse/parsers/openapi-yaml-2/index.cjs +3 -1
  101. package/src/parse/parsers/openapi-yaml-3-0/index.cjs +3 -1
  102. package/src/parse/parsers/openapi-yaml-3-1/index.cjs +3 -1
  103. package/src/parse/parsers/overlay-json-1/extends.cjs +3 -1
  104. package/src/parse/parsers/overlay-json-1/index.cjs +9 -2
  105. package/src/parse/parsers/overlay-yaml-1/extends.cjs +10 -4
  106. package/src/parse/parsers/overlay-yaml-1/index.cjs +9 -2
  107. package/src/parse/parsers/yaml-1-2/index.cjs +3 -1
  108. package/src/resolve/index.cjs +3 -1
  109. package/src/resolve/resolvers/HTTPResolver.cjs +3 -1
  110. package/src/resolve/resolvers/Resolver.cjs +3 -1
  111. package/src/resolve/resolvers/file/index-browser.cjs +3 -1
  112. package/src/resolve/resolvers/file/index-node.cjs +3 -1
  113. package/src/resolve/resolvers/http-axios/cache/MemoryCache.cjs +3 -1
  114. package/src/resolve/resolvers/http-axios/index.cjs +3 -1
  115. package/src/resolve/strategies/ResolveStrategy.cjs +3 -1
  116. package/src/resolve/strategies/apidom/index.cjs +3 -1
  117. package/src/resolve/strategies/arazzo-1/index.cjs +3 -1
  118. package/src/resolve/strategies/asyncapi-2/index.cjs +3 -1
  119. package/src/resolve/strategies/openapi-2/index.cjs +3 -1
  120. package/src/resolve/strategies/openapi-3-0/index.cjs +3 -1
  121. package/src/resolve/strategies/openapi-3-1/index.cjs +3 -1
  122. package/src/resolve/strategies/overlay-1/index.cjs +3 -1
  123. package/src/resolve/util.cjs +3 -1
  124. package/src/util/plugins.cjs +3 -1
  125. package/src/util/url.cjs +3 -1
  126. package/types/apidom-reference.d.ts +41 -2
  127. package/types/bundle/strategies/openapi-3-0/index.d.ts +31 -0
  128. package/types/bundle/strategies/openapi-3-0/visitor.d.ts +131 -0
  129. package/types/bundle/util.d.ts +22 -0
  130. package/types/errors/BundleError.d.ts +2 -2
  131. package/types/errors/UnresolvableReferenceError.d.ts +5 -2
  132. package/types/index.d.ts +1 -0
  133. package/types/options/index.d.ts +33 -1
@@ -0,0 +1,642 @@
1
+ import { propEq } from 'ramda';
2
+ import { isElement, isObjectElement, isStringElement, ObjectElement, AnnotationElement, cloneDeep, cloneShallow } from '@speclynx/apidom-datamodel';
3
+ import { toValue, toYAML, fixedFields } from '@speclynx/apidom-core';
4
+ import { ApiDOMStructuredError } from '@speclynx/apidom-error';
5
+ import { traverseAsync } from '@speclynx/apidom-traverse';
6
+ import { evaluate, escape, parse as parseJSONPointer, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
7
+ import { ComponentsElement, ComponentsSchemasElement, ComponentsResponsesElement, ComponentsParametersElement, ComponentsExamplesElement, ComponentsRequestBodiesElement, ComponentsHeadersElement, ComponentsSecuritySchemesElement, ComponentsLinksElement, ComponentsCallbacksElement, isReferenceElement, isReferenceLikeElement, isPathItemElement, refract, refractReference, refractPathItem } from '@speclynx/apidom-ns-openapi-3-0';
8
+ import BundleError from "../../../errors/BundleError.mjs";
9
+ import UnresolvableReferenceError from "../../../errors/UnresolvableReferenceError.mjs";
10
+ import MaximumBundleDepthError from "../../../errors/MaximumBundleDepthError.mjs";
11
+ import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
12
+ import * as url from "../../../util/url.mjs";
13
+ import parse from "../../../parse/index.mjs";
14
+ import Reference from "../../../Reference.mjs";
15
+ import { toPascalCase, sanitizeComponentName, uniqueName as resolveUniqueName } from "../../util.mjs";
16
+ /**
17
+ * Maps the `referenced-element` meta value (set during refraction) to the
18
+ * Components Object field where the referenced element should be hoisted.
19
+ * Field names are sourced from the Components Object fixed fields so they
20
+ * cannot drift from the namespace definition.
21
+ */
22
+ const cf = fixedFields(ComponentsElement, {
23
+ indexed: true
24
+ });
25
+ const componentFieldByReferencedElement = {
26
+ schema: cf.schemas.name,
27
+ response: cf.responses.name,
28
+ parameter: cf.parameters.name,
29
+ example: cf.examples.name,
30
+ requestBody: cf.requestBodies.name,
31
+ header: cf.headers.name,
32
+ securityScheme: cf.securitySchemes.name,
33
+ link: cf.links.name,
34
+ callback: cf.callbacks.name
35
+ };
36
+
37
+ /**
38
+ * Maps a Components Object field to the named-content element the namespace
39
+ * uses for it, so a bundle-created field matches a parsed/refracted one
40
+ * (e.g. carries the `components-schemas` class) rather than being a generic
41
+ * object.
42
+ */
43
+ const componentFieldElementByField = {
44
+ [cf.schemas.name]: ComponentsSchemasElement,
45
+ [cf.responses.name]: ComponentsResponsesElement,
46
+ [cf.parameters.name]: ComponentsParametersElement,
47
+ [cf.examples.name]: ComponentsExamplesElement,
48
+ [cf.requestBodies.name]: ComponentsRequestBodiesElement,
49
+ [cf.headers.name]: ComponentsHeadersElement,
50
+ [cf.securitySchemes.name]: ComponentsSecuritySchemesElement,
51
+ [cf.links.name]: ComponentsLinksElement,
52
+ [cf.callbacks.name]: ComponentsCallbacksElement
53
+ };
54
+
55
+ /**
56
+ * @public
57
+ */
58
+
59
+ /**
60
+ * @public
61
+ */
62
+ class OpenAPI3_0BundleVisitor {
63
+ reference;
64
+ options;
65
+
66
+ /**
67
+ * The base URI (without hash) of the entry document. A reference is
68
+ * considered internal when it resolves to this URI and external otherwise.
69
+ * Derived from the shared refSet's root reference.
70
+ */
71
+ get entryURI() {
72
+ return url.stripHash(this.reference.refSet?.rootRef?.uri ?? '');
73
+ }
74
+
75
+ /**
76
+ * The entry parse result. Derived from the shared refSet's root reference.
77
+ */
78
+ get entryParseResult() {
79
+ return this.reference.refSet.rootRef.value;
80
+ }
81
+
82
+ /**
83
+ * The entry document element. Hoisted external fragments are placed into its
84
+ * `components` object. The `canBundle` guard guarantees it is an
85
+ * OpenApi3_0Element.
86
+ */
87
+ get entryResult() {
88
+ return this.entryParseResult.result;
89
+ }
90
+
91
+ /**
92
+ * Shared across the entry document and every external document visitor.
93
+ * Guarantees each external fragment is hoisted exactly once and gives us a
94
+ * stable internal pointer to rewrite every referencing element to.
95
+ */
96
+ assignments;
97
+
98
+ /**
99
+ * Component names already reserved per Components Object field. Reserved
100
+ * before recursion so collision suffixing (`Pet`, `Pet-2`, ...) accounts for
101
+ * components that are still being bundled.
102
+ */
103
+ reservedNames;
104
+
105
+ /**
106
+ * Caches refracted fragments keyed by source element and target element type.
107
+ * The type is part of the key because the same source can be refracted into
108
+ * different semantic elements depending on the referencing context (e.g. a
109
+ * generic object referenced once as a Parameter and once as a Response).
110
+ */
111
+ refractCache;
112
+
113
+ /**
114
+ * Canonical URIs of Path Item Objects currently being inlined. Path Items
115
+ * are not hoisted into Components Object (no `components.pathItems` in
116
+ * OpenAPI 3.0), so the `assignments` map can't break their cycles. This
117
+ * stack guards against circular external Path Item references.
118
+ */
119
+ inlineStack;
120
+ constructor({
121
+ reference,
122
+ options,
123
+ assignments = new Map(),
124
+ reservedNames = new Map(),
125
+ refractCache = new WeakMap(),
126
+ inlineStack = new Set()
127
+ }) {
128
+ this.reference = reference;
129
+ this.options = options;
130
+ this.inlineStack = inlineStack;
131
+ this.assignments = assignments;
132
+ this.reservedNames = reservedNames;
133
+ this.refractCache = refractCache;
134
+ }
135
+ getRefracted(source, type) {
136
+ return this.refractCache.get(source)?.get(type);
137
+ }
138
+ setRefracted(source, type, refracted) {
139
+ let byType = this.refractCache.get(source);
140
+ if (byType === undefined) {
141
+ byType = new Map();
142
+ this.refractCache.set(source, byType);
143
+ }
144
+ byType.set(type, refracted);
145
+ }
146
+ toBaseURI(uri) {
147
+ return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
148
+ }
149
+ async toReference(uri) {
150
+ // detect maximum depth of resolution
151
+ if (this.reference.depth >= this.options.resolve.maxDepth) {
152
+ throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`, {
153
+ maxDepth: this.options.resolve.maxDepth,
154
+ uri: this.reference.uri
155
+ });
156
+ }
157
+ const baseURI = this.toBaseURI(uri);
158
+ const {
159
+ refSet
160
+ } = this.reference;
161
+
162
+ // we've already processed this Reference in past
163
+ if (refSet.has(baseURI)) {
164
+ return refSet.find(propEq(baseURI, 'uri'));
165
+ }
166
+ const parseResult = await parse(url.unsanitize(baseURI), {
167
+ ...this.options,
168
+ parse: {
169
+ ...this.options.parse,
170
+ mediaType: 'text/plain'
171
+ }
172
+ });
173
+ const reference = new Reference({
174
+ uri: baseURI,
175
+ value: parseResult,
176
+ depth: this.reference.depth + 1
177
+ });
178
+ refSet.add(reference);
179
+ return reference;
180
+ }
181
+
182
+ /**
183
+ * Normalizes a self-file reference (e.g. `./root.json#/components/schemas/Pet`)
184
+ * to a bare fragment (`#/components/schemas/Pet`) so the bundled document stays
185
+ * transferable. Bare fragments are returned unchanged.
186
+ */
187
+ normalizeSelfFileRef(ref) {
188
+ return ref.startsWith('#') ? ref : url.getHash(ref);
189
+ }
190
+
191
+ /**
192
+ * Handles an error according to the `bundle.continueOnError` option.
193
+ *
194
+ * For new errors: wraps in UnresolvableReferenceError with structured
195
+ * context. For errors already wrapped by a nested visitor: prepends the
196
+ * current hop to the trace.
197
+ *
198
+ * Inner/intermediate visitors always throw to let the trace accumulate. Only
199
+ * the entry document visitor respects continueOnError (callback/swallow/throw).
200
+ */
201
+ handleError(message, error, referencingElement, refFieldName, refFieldValue, visitorPath) {
202
+ // a collision reported as `error` is a deliberate hard stop with no
203
+ // resolution-failure analog, so pass it through unchanged. A maxDepth
204
+ // breach (MaximumBundleDepthError, which also extends BundleError) is a
205
+ // resolution failure and flows through below — wrapped and subject to
206
+ // continueOnError, like dereferencing.
207
+ if (error instanceof BundleError && !(error instanceof MaximumBundleDepthError)) {
208
+ throw error;
209
+ }
210
+ const {
211
+ continueOnError
212
+ } = this.options.bundle;
213
+ const isEntryDocument = url.stripHash(this.reference.refSet?.rootRef?.uri ?? '') === this.reference.uri;
214
+ const uri = this.reference.uri;
215
+ const type = referencingElement.element;
216
+ const codeFrame = toYAML(referencingElement);
217
+ const location = visitorPath.formatPath();
218
+ const hop = {
219
+ uri,
220
+ type,
221
+ refFieldName,
222
+ refFieldValue,
223
+ location,
224
+ codeFrame
225
+ };
226
+ let unresolvedError;
227
+ if (error instanceof UnresolvableReferenceError) {
228
+ // @ts-ignore
229
+ error.trace = [hop, ...error.trace];
230
+ unresolvedError = error;
231
+ } else {
232
+ unresolvedError = new UnresolvableReferenceError(message, {
233
+ cause: error,
234
+ type,
235
+ uri,
236
+ location,
237
+ codeFrame,
238
+ refFieldName,
239
+ refFieldValue,
240
+ trace: []
241
+ });
242
+ }
243
+ if (!isEntryDocument || continueOnError === false) throw unresolvedError;
244
+ if (typeof continueOnError === 'function') continueOnError(unresolvedError);
245
+ }
246
+
247
+ /**
248
+ * Lazily creates the Components Object (and the requested field within it)
249
+ * on the entry document, returning the field where a fragment is hoisted.
250
+ */
251
+ ensureComponentsField(field) {
252
+ const entryResult = this.entryResult;
253
+ let components = entryResult.components;
254
+ if (!isObjectElement(components)) {
255
+ components = new ComponentsElement();
256
+ entryResult.components = components;
257
+ }
258
+ let fieldElement = components.get(field);
259
+ if (!isObjectElement(fieldElement)) {
260
+ const FieldElement = componentFieldElementByField[field] ?? ObjectElement;
261
+ fieldElement = new FieldElement();
262
+ components.set(field, fieldElement);
263
+ }
264
+ return fieldElement;
265
+ }
266
+
267
+ /**
268
+ * Derives a component name from the referenced JSON Pointer's last token,
269
+ * falling back to the referenced file's basename.
270
+ */
271
+ basenameOf(jsonPointer, baseURI) {
272
+ const tokens = parseJSONPointer(jsonPointer).tree;
273
+ let candidate = tokens.length > 0 ? tokens[tokens.length - 1] : '';
274
+ if (candidate === '') {
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;
283
+ }
284
+
285
+ /**
286
+ * Computes the base component name (before collision suffixing) according to
287
+ * the configured naming strategy.
288
+ */
289
+ baseName(element, field, jsonPointer, baseURI) {
290
+ // strategy specific options take precedence over the top-level bundle options
291
+ const componentNamesStrategy = this.options.bundle.strategyOpts['openapi-3-0']?.componentNamesStrategy ?? this.options.bundle.componentNamesStrategy;
292
+ if (typeof componentNamesStrategy === 'function') {
293
+ const resolved = componentNamesStrategy({
294
+ element,
295
+ field,
296
+ jsonPointer,
297
+ baseURI
298
+ });
299
+ if (typeof resolved === 'string' && resolved !== '') {
300
+ return resolved;
301
+ }
302
+ return this.basenameOf(jsonPointer, baseURI);
303
+ }
304
+ if (componentNamesStrategy === 'title') {
305
+ const title = isObjectElement(element) ? toValue(element.get('title')) : undefined;
306
+ if (typeof title === 'string' && title.trim() !== '') {
307
+ const name = sanitizeComponentName(toPascalCase(title));
308
+ if (name !== '') return name;
309
+ }
310
+ // fall back to basename when no usable title is present
311
+ }
312
+ return this.basenameOf(jsonPointer, baseURI);
313
+ }
314
+
315
+ /**
316
+ * Computes a collision-free component name within the target field. A name is
317
+ * taken if it's already placed in components or reserved by a component that
318
+ * is still being bundled (reserved before recursion).
319
+ */
320
+ uniqueName(candidate, field) {
321
+ const fieldElement = this.ensureComponentsField(field);
322
+ const reserved = this.reservedNames.get(field) ?? new Set();
323
+ return resolveUniqueName(candidate, name => fieldElement.hasKey(name) || reserved.has(name));
324
+ }
325
+ async ReferenceElement(path) {
326
+ const referencingElement = path.node;
327
+ const $ref = toValue(referencingElement.$ref);
328
+ const retrievalURI = this.toBaseURI($ref);
329
+ const isInternalReference = this.entryURI === retrievalURI;
330
+ const isExternalReference = !isInternalReference;
331
+ if (isInternalReference) {
332
+ // normalize self-file references to a bare fragment so the bundled
333
+ // document stays transferable; bare fragments are left untouched
334
+ referencingElement.set('$ref', this.normalizeSelfFileRef($ref));
335
+ path.skip();
336
+ return;
337
+ }
338
+
339
+ // honor the resolve.external switch
340
+ if (!this.options.resolve.external && isExternalReference) {
341
+ path.skip();
342
+ return;
343
+ }
344
+ const $refBaseURI = url.resolve(retrievalURI, $ref);
345
+ const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
346
+
347
+ // the target bucket depends on the referencing context, so it's part of the
348
+ // identity: the same external target referenced as e.g. both a parameter and
349
+ // a response must be hoisted into both components fields. The refractor sets
350
+ // `referenced-element` for every Reference Object position; the `schemas`
351
+ // fallback is a defensive default for the (unexpected) untagged case.
352
+ const referencedElementType = referencingElement.meta.get('referenced-element');
353
+ const field = componentFieldByReferencedElement[referencedElementType] ?? componentFieldByReferencedElement.schema;
354
+ const canonicalKey = `${field}\t${retrievalURI}#${jsonPointer}`;
355
+
356
+ // already hoisted (or being hoisted) — just rewrite the pointer.
357
+ //
358
+ // this is also how circular references terminate: the assignment is
359
+ // reserved BEFORE recursing into the fragment (see below), so a reference
360
+ // that cycles back (directly or indirectly) finds its pointer already
361
+ // reserved here and is rewritten without recursing. Cycles are deliberately
362
+ // preserved as internal `$ref`s pointing at each other — that is valid,
363
+ // serializable output; resolving the cycle (if desired) is dereferencing's
364
+ // job, not bundling's.
365
+ if (this.assignments.has(canonicalKey)) {
366
+ referencingElement.set('$ref', this.assignments.get(canonicalKey));
367
+ path.skip();
368
+ return;
369
+ }
370
+ try {
371
+ // detect maximum depth of bundling
372
+ if (this.reference.depth >= this.options.bundle.maxDepth) {
373
+ throw new MaximumBundleDepthError(`Maximum bundle depth of "${this.options.bundle.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
374
+ maxDepth: this.options.bundle.maxDepth,
375
+ uri: this.reference.uri
376
+ });
377
+ }
378
+ const reference = await this.toReference($ref);
379
+
380
+ // possibly non-semantic fragment
381
+ let referencedElement = evaluate(reference.value.result, jsonPointer);
382
+
383
+ // apply semantics to the fragment so nested references become Reference Objects
384
+ if (referencedElement.element !== referencedElementType && !isReferenceElement(referencedElement)) {
385
+ const cached = this.getRefracted(referencedElement, referencedElementType);
386
+ if (cached !== undefined) {
387
+ referencedElement = cached;
388
+ } else if (isReferenceLikeElement(referencedElement)) {
389
+ const sourceElement = referencedElement;
390
+ referencedElement = refractReference(referencedElement);
391
+ referencedElement.meta.set('referenced-element', referencedElementType);
392
+ this.setRefracted(sourceElement, referencedElementType, referencedElement);
393
+ } else {
394
+ const sourceElement = referencedElement;
395
+ referencedElement = refract(referencedElement, {
396
+ element: referencedElementType
397
+ });
398
+ this.setRefracted(sourceElement, referencedElementType, referencedElement);
399
+ }
400
+ }
401
+ const preferredName = this.baseName(referencedElement, field, jsonPointer, retrievalURI);
402
+ const componentName = this.uniqueName(preferredName, field);
403
+ const internalPointer = `#/components/${field}/${escape(componentName)}`;
404
+
405
+ // a rename means two distinct targets resolved to the same name; each
406
+ // keeps its own component (and origin), so report the rename per severity.
407
+ // strategy specific options take precedence over the top-level bundle options
408
+ const onComponentNameCollision = this.options.bundle.strategyOpts['openapi-3-0']?.onComponentNameCollision ?? this.options.bundle.onComponentNameCollision;
409
+ if (componentName !== preferredName && onComponentNameCollision !== 'off') {
410
+ const message = `Component "${preferredName}" in components/${field} is referenced with the same name but different content. Renamed to "${componentName}".`;
411
+ if (onComponentNameCollision === 'error') {
412
+ throw new BundleError(message);
413
+ }
414
+ const annotation = new AnnotationElement(message);
415
+ annotation.classes.push('warning');
416
+ annotation.code = 'bundle-component-name-collision';
417
+ // append (never prepend) so the result element stays ahead of annotations
418
+ this.entryParseResult.content.push(annotation);
419
+ }
420
+
421
+ // reserve the assignment and component name before recursing so cyclic
422
+ // references terminate
423
+ this.assignments.set(canonicalKey, internalPointer);
424
+ if (!this.reservedNames.has(field)) this.reservedNames.set(field, new Set());
425
+ this.reservedNames.get(field).add(componentName);
426
+
427
+ // own a copy of the fragment and bundle its own external references
428
+ const hoistedElement = cloneDeep(referencedElement);
429
+ const visitor = new OpenAPI3_0BundleVisitor({
430
+ reference,
431
+ options: this.options,
432
+ assignments: this.assignments,
433
+ reservedNames: this.reservedNames,
434
+ refractCache: this.refractCache,
435
+ inlineStack: this.inlineStack
436
+ });
437
+ const bundledElement = await traverseAsync(hoistedElement, visitor, {
438
+ mutable: true
439
+ });
440
+
441
+ // annotate the hoisted fragment with info about its origin
442
+ if (isElement(bundledElement)) {
443
+ bundledElement.meta.set('ref-origin', reference.uri);
444
+ }
445
+
446
+ // place the bundled fragment into the entry document's components
447
+ this.ensureComponentsField(field).set(componentName, bundledElement);
448
+
449
+ // rewrite the referencing element to point at the hoisted fragment
450
+ referencingElement.set('$ref', internalPointer);
451
+ path.skip();
452
+ } catch (error) {
453
+ this.handleError(`Error while bundling Reference Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
454
+ path.skip();
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Path Item Object is not a Components Object field in OpenAPI 3.0
460
+ * (unlike 3.1 which has `components.pathItems`). An external Path Item
461
+ * reference therefore cannot be hoisted; it is inlined in place instead,
462
+ * after bundling its own external references.
463
+ */
464
+ async PathItemElement(path) {
465
+ const pathItemElement = path.node;
466
+
467
+ // ignore Path Item Objects without a $ref field
468
+ if (!isStringElement(pathItemElement.$ref)) {
469
+ return;
470
+ }
471
+ const $ref = toValue(pathItemElement.$ref);
472
+ const retrievalURI = this.toBaseURI($ref);
473
+ const isInternalReference = this.entryURI === retrievalURI;
474
+ const isExternalReference = !isInternalReference;
475
+ if (isInternalReference) {
476
+ // normalize self-file references to a bare fragment so the bundled
477
+ // document stays transferable; bare fragments are left untouched
478
+ pathItemElement.set('$ref', this.normalizeSelfFileRef($ref));
479
+ return;
480
+ }
481
+
482
+ // honor the resolve.external switch
483
+ if (!this.options.resolve.external && isExternalReference) {
484
+ return;
485
+ }
486
+ const $refBaseURI = url.resolve(retrievalURI, $ref);
487
+ const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
488
+ const canonicalKey = `${retrievalURI}#${jsonPointer}`;
489
+
490
+ // a circular external Path Item reference cannot be inlined without
491
+ // recursing forever; leave the $ref in place to break the cycle
492
+ if (this.inlineStack.has(canonicalKey)) {
493
+ return;
494
+ }
495
+ try {
496
+ // detect maximum depth of bundling
497
+ if (this.reference.depth >= this.options.bundle.maxDepth) {
498
+ throw new MaximumBundleDepthError(`Maximum bundle depth of "${this.options.bundle.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
499
+ maxDepth: this.options.bundle.maxDepth,
500
+ uri: this.reference.uri
501
+ });
502
+ }
503
+ const reference = await this.toReference($ref);
504
+ let referencedElement = evaluate(reference.value.result, jsonPointer);
505
+
506
+ // apply Path Item semantics to the referenced fragment
507
+ if (!isPathItemElement(referencedElement)) {
508
+ const cached = this.getRefracted(referencedElement, 'pathItem');
509
+ if (cached !== undefined) {
510
+ referencedElement = cached;
511
+ } else {
512
+ const sourceElement = referencedElement;
513
+ referencedElement = refractPathItem(referencedElement);
514
+ this.setRefracted(sourceElement, 'pathItem', referencedElement);
515
+ }
516
+ }
517
+
518
+ // own a copy and bundle the external references it contains
519
+ const inlinedElement = cloneDeep(referencedElement);
520
+ this.inlineStack.add(canonicalKey);
521
+ const visitor = new OpenAPI3_0BundleVisitor({
522
+ reference,
523
+ options: this.options,
524
+ assignments: this.assignments,
525
+ reservedNames: this.reservedNames,
526
+ refractCache: this.refractCache,
527
+ inlineStack: this.inlineStack
528
+ });
529
+ let bundledElement;
530
+ try {
531
+ bundledElement = await traverseAsync(inlinedElement, visitor, {
532
+ mutable: true
533
+ });
534
+ } finally {
535
+ this.inlineStack.delete(canonicalKey);
536
+ }
537
+
538
+ // merge sibling fields from the referencing Path Item over the referenced
539
+ // one. The referencing `$ref` is being resolved away and is skipped; the
540
+ // referenced element keeps its own `$ref` if one survived (i.e. it was
541
+ // left in place to break a circular external Path Item chain).
542
+ pathItemElement.forEach((_value, keyElement, item) => {
543
+ const key = toValue(keyElement);
544
+ if (key === '$ref') return;
545
+ bundledElement.remove(key);
546
+ bundledElement.content.push(item);
547
+ });
548
+ bundledElement.meta.set('ref-origin', reference.uri);
549
+ path.replaceWith(bundledElement);
550
+ } catch (error) {
551
+ this.handleError(`Error while bundling Path Item Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, pathItemElement, '$ref', $ref, path);
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Example Object `externalValue` points at arbitrary external content that
557
+ * has no place in the Components Object. The referenced value is inlined into
558
+ * the Example's `value` field.
559
+ */
560
+ async ExampleElement(path) {
561
+ const exampleElement = path.node;
562
+
563
+ // ignore Example Objects without an externalValue field
564
+ if (!isStringElement(exampleElement.externalValue)) {
565
+ return;
566
+ }
567
+
568
+ // value and externalValue fields are mutually exclusive
569
+ if (exampleElement.hasKey('value') && isStringElement(exampleElement.externalValue)) {
570
+ throw new ApiDOMStructuredError('ExampleElement value and externalValue fields are mutually exclusive', {
571
+ value: toValue(exampleElement.value),
572
+ externalValue: toValue(exampleElement.externalValue)
573
+ });
574
+ }
575
+ const externalValue = toValue(exampleElement.externalValue);
576
+ const retrievalURI = this.toBaseURI(externalValue);
577
+ const isInternalReference = this.entryURI === retrievalURI;
578
+ const isExternalReference = !isInternalReference;
579
+
580
+ // honor the resolve.external switch
581
+ if (!this.options.resolve.external && isExternalReference) {
582
+ return;
583
+ }
584
+ try {
585
+ const reference = await this.toReference(externalValue);
586
+ const valueElement = cloneShallow(reference.value.result);
587
+ valueElement.meta.set('ref-origin', reference.uri);
588
+ const exampleElementCopy = cloneShallow(exampleElement);
589
+ exampleElementCopy.value = valueElement;
590
+ exampleElementCopy.remove('externalValue');
591
+ path.replaceWith(exampleElementCopy);
592
+ } catch (error) {
593
+ this.handleError(`Error while bundling Example Object. Cannot resolve externalValue "${externalValue}": ${error.message}`, error, exampleElement, 'externalValue', externalValue, path);
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Link Object `operationRef` points at an Operation Object, which has no
599
+ * Components Object field in OpenAPI 3.0 and therefore cannot be hoisted or
600
+ * inlined. To keep the link resolvable from the bundled document, an external
601
+ * `operationRef` is rewritten to its absolute URI. Internal `operationRef`
602
+ * values and `operationId` links are left untouched.
603
+ */
604
+ async LinkElement(path) {
605
+ const linkElement = path.node;
606
+
607
+ // operationRef and operationId fields are mutually exclusive
608
+ if (isStringElement(linkElement.operationRef) && isStringElement(linkElement.operationId)) {
609
+ throw new ApiDOMStructuredError('LinkElement operationRef and operationId fields are mutually exclusive', {
610
+ operationRef: toValue(linkElement.operationRef),
611
+ operationId: toValue(linkElement.operationId)
612
+ });
613
+ }
614
+
615
+ // only external operationRef links need adjusting
616
+ if (!isStringElement(linkElement.operationRef)) {
617
+ return;
618
+ }
619
+ const operationRef = toValue(linkElement.operationRef);
620
+ const retrievalURI = this.toBaseURI(operationRef);
621
+ const isInternalReference = this.entryURI === retrievalURI;
622
+ if (isInternalReference) {
623
+ // normalize self-file operationRef to a bare fragment so the bundled
624
+ // document stays transferable; bare fragments are left untouched
625
+ linkElement.set('operationRef', this.normalizeSelfFileRef(operationRef));
626
+ return;
627
+ }
628
+
629
+ // honor the resolve.external switch
630
+ if (!this.options.resolve.external) {
631
+ return;
632
+ }
633
+
634
+ // rewrite to an absolute URI so the link keeps resolving once the document
635
+ // is treated as self-contained
636
+ const absoluteURI = url.resolve(this.reference.uri, operationRef);
637
+ if (absoluteURI !== operationRef) {
638
+ linkElement.set('operationRef', absoluteURI);
639
+ }
640
+ }
641
+ }
642
+ export default OpenAPI3_0BundleVisitor;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
4
- exports.__esModule = true;
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
5
7
  exports.default = void 0;
6
8
  var _apidomNsOpenapi = require("@speclynx/apidom-ns-openapi-3-1");
7
9
  var _BundleStrategy = _interopRequireDefault(require("../BundleStrategy.cjs"));
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.uniqueName = exports.toPascalCase = exports.sanitizeComponentName = void 0;
7
+ /**
8
+ * Upper-cases the first letter of each whitespace-separated word and joins
9
+ * them, e.g. `pet model` -> `PetModel`.
10
+ *
11
+ * @public
12
+ */
13
+ const toPascalCase = value => value.trim().split(/\s+/).map(word => word.replace(/^[a-z]/, char => char.toUpperCase())).join('');
14
+
15
+ /**
16
+ * Replaces characters that are invalid in a Components Object name with `-`,
17
+ * then collapses repeated separators and trims leading/trailing ones.
18
+ *
19
+ * @public
20
+ */
21
+ exports.toPascalCase = toPascalCase;
22
+ const sanitizeComponentName = value => value.replace(/[^a-zA-Z0-9.\-_]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
23
+
24
+ /**
25
+ * Returns a collision-free name by suffixing `-2`, `-3`, ... onto `candidate`
26
+ * until `isTaken` reports it free. `isTaken` decides what counts as taken
27
+ * (e.g. names already placed in or reserved for a Components Object field).
28
+ *
29
+ * @public
30
+ */
31
+ exports.sanitizeComponentName = sanitizeComponentName;
32
+ const uniqueName = (candidate, isTaken) => {
33
+ if (!isTaken(candidate)) {
34
+ return candidate;
35
+ }
36
+ let counter = 2;
37
+ while (isTaken(`${candidate}-${counter}`)) {
38
+ counter += 1;
39
+ }
40
+ return `${candidate}-${counter}`;
41
+ };
42
+ exports.uniqueName = uniqueName;