schema-components 1.20.0 → 1.22.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/README.md +1 -1
- package/dist/core/adapter.d.mts +28 -4
- package/dist/core/adapter.mjs +408 -71
- package/dist/core/constraints.d.mts +2 -2
- package/dist/core/constraints.mjs +0 -2
- package/dist/core/diagnostics.d.mts +1 -1
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/errors.mjs +9 -15
- package/dist/core/fieldOrder.d.mts +1 -1
- package/dist/core/formats.d.mts +22 -1
- package/dist/core/formats.mjs +21 -0
- package/dist/core/limits.d.mts +2 -0
- package/dist/core/limits.mjs +23 -0
- package/dist/core/merge.d.mts +11 -2
- package/dist/core/merge.mjs +11 -0
- package/dist/core/normalise.d.mts +36 -4
- package/dist/core/normalise.mjs +2 -2
- package/dist/core/openapi30.d.mts +24 -1
- package/dist/core/openapi30.mjs +2 -2
- package/dist/core/ref.d.mts +1 -1
- package/dist/core/ref.mjs +35 -9
- package/dist/core/renderer.d.mts +1 -1
- package/dist/core/renderer.mjs +0 -2
- package/dist/core/swagger2.d.mts +1 -1
- package/dist/core/swagger2.mjs +1 -1
- package/dist/core/typeInference.d.mts +2 -2
- package/dist/core/types.d.mts +2 -2
- package/dist/core/types.mjs +1 -4
- package/dist/core/version.d.mts +1 -1
- package/dist/core/walkBuilders.d.mts +13 -5
- package/dist/core/walkBuilders.mjs +11 -3
- package/dist/core/walker.d.mts +1 -1
- package/dist/core/walker.mjs +110 -26
- package/dist/{diagnostics-CbBPsxSt.d.mts → diagnostics-D0QCYGv0.d.mts} +1 -1
- package/dist/{errors-C2iABcn9.d.mts → errors-DpFwqs5C.d.mts} +7 -11
- package/dist/html/a11y.d.mts +2 -2
- package/dist/html/a11y.mjs +10 -3
- package/dist/html/renderToHtml.d.mts +10 -3
- package/dist/html/renderToHtml.mjs +13 -3
- package/dist/html/renderToHtmlStream.d.mts +2 -2
- package/dist/html/renderers.d.mts +2 -2
- package/dist/html/renderers.mjs +1 -6
- package/dist/html/streamRenderers.d.mts +5 -4
- package/dist/html/streamRenderers.mjs +91 -30
- package/dist/limits-Cw5QZND8.d.mts +29 -0
- package/dist/{normalise-CMMEl4cd.mjs → normalise-DVEJQmF7.mjs} +791 -141
- package/dist/openapi/ApiCallbacks.d.mts +1 -1
- package/dist/openapi/ApiLinks.d.mts +1 -1
- package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
- package/dist/openapi/ApiSecurity.d.mts +1 -1
- package/dist/openapi/ApiSecurity.mjs +127 -7
- package/dist/openapi/components.d.mts +175 -21
- package/dist/openapi/components.mjs +145 -21
- package/dist/openapi/parser.d.mts +1 -1
- package/dist/openapi/parser.mjs +74 -7
- package/dist/openapi/resolve.d.mts +70 -12
- package/dist/openapi/resolve.mjs +265 -42
- package/dist/react/SchemaComponent.d.mts +100 -35
- package/dist/react/SchemaComponent.mjs +66 -24
- package/dist/react/SchemaView.d.mts +3 -3
- package/dist/react/SchemaView.mjs +2 -2
- package/dist/react/fieldPath.d.mts +1 -1
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headless.mjs +1 -2
- package/dist/react/headlessRenderers.d.mts +3 -4
- package/dist/react/headlessRenderers.mjs +11 -31
- package/dist/{ref-C8JbwfiS.d.mts → ref-D-_JBZkF.d.mts} +7 -2
- package/dist/{renderer-SOIbJBtk.d.mts → renderer-BaRlQIuN.d.mts} +2 -2
- package/dist/themes/mantine.d.mts +1 -1
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/typeInference-DkcUHfaM.d.mts +982 -0
- package/dist/{types-C9zw9wbX.d.mts → types-BrRMV0en.d.mts} +15 -12
- package/package.json +1 -3
- package/dist/typeInference-CDoD_LZ_.d.mts +0 -533
- /package/dist/{version-D-u7aMfy.d.mts → version-D2jfdX6E.d.mts} +0 -0
|
@@ -29,6 +29,8 @@ function normaliseOpenApi30Node(node) {
|
|
|
29
29
|
return node;
|
|
30
30
|
}
|
|
31
31
|
const nullOption = { type: "null" };
|
|
32
|
+
if (typeof node.$ref === "string") return { anyOf: [{ $ref: node.$ref }, nullOption] };
|
|
33
|
+
if (Array.isArray(node.enum) && !node.enum.includes(null)) node.enum = [...node.enum, null];
|
|
32
34
|
if (Array.isArray(node.anyOf)) {
|
|
33
35
|
node.anyOf = [...node.anyOf, nullOption];
|
|
34
36
|
delete node.nullable;
|
|
@@ -113,10 +115,270 @@ function normaliseOpenApi30Discriminator(node) {
|
|
|
113
115
|
}
|
|
114
116
|
if ("oneOf" in node) node.oneOf = normalisedComposite;
|
|
115
117
|
else if ("anyOf" in node) node.anyOf = normalisedComposite;
|
|
116
|
-
|
|
118
|
+
const extensions = {};
|
|
119
|
+
for (const [key, value] of Object.entries(discriminator)) if (key.startsWith("x-")) extensions[key] = value;
|
|
120
|
+
if (Object.keys(extensions).length > 0) node.discriminator = {
|
|
121
|
+
propertyName,
|
|
122
|
+
...extensions
|
|
123
|
+
};
|
|
124
|
+
else delete node.discriminator;
|
|
117
125
|
return node;
|
|
118
126
|
}
|
|
119
127
|
/**
|
|
128
|
+
* Returns the schema name a `$ref` points at when it targets
|
|
129
|
+
* `#/components/schemas/<Name>`, or `undefined` otherwise.
|
|
130
|
+
*
|
|
131
|
+
* The walker only resolves intra-document refs and other allOf-base
|
|
132
|
+
* patterns; refs into `definitions` (Swagger 2.0) are already rewritten
|
|
133
|
+
* before this stage.
|
|
134
|
+
*/
|
|
135
|
+
function componentSchemaName(ref) {
|
|
136
|
+
if (typeof ref !== "string") return void 0;
|
|
137
|
+
if (!ref.startsWith("#/components/schemas/")) return void 0;
|
|
138
|
+
const name = ref.slice(21);
|
|
139
|
+
return name.length > 0 ? name : void 0;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Find every immediate `$ref` that an `allOf` array contains pointing
|
|
143
|
+
* back at a `components/schemas/<Name>` entry. Used to discover
|
|
144
|
+
* "Cat extends Pet"-style inheritance — the subtype's `allOf` lists
|
|
145
|
+
* the base by `$ref` alongside its own local fields.
|
|
146
|
+
*/
|
|
147
|
+
function listAllOfBaseRefs(schema) {
|
|
148
|
+
const allOf = schema.allOf;
|
|
149
|
+
if (!Array.isArray(allOf)) return [];
|
|
150
|
+
const result = [];
|
|
151
|
+
for (const entry of allOf) {
|
|
152
|
+
if (!isObject(entry)) continue;
|
|
153
|
+
const name = componentSchemaName(entry.$ref);
|
|
154
|
+
if (name !== void 0) result.push(name);
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Collect discriminator subtypes for a base schema. Entries come from:
|
|
160
|
+
*
|
|
161
|
+
* 1. The base's `discriminator.mapping` (explicit author intent — the
|
|
162
|
+
* mapping key supplies the `const` value, the ref names the subtype).
|
|
163
|
+
* 2. Component schemas whose `allOf` lists this base by `$ref` and
|
|
164
|
+
* were not already named in the mapping. The `const` value defaults
|
|
165
|
+
* to the subtype's component name.
|
|
166
|
+
*
|
|
167
|
+
* Returned in deterministic order: mapping entries first (preserving
|
|
168
|
+
* authored order), then implicit subtypes alphabetically.
|
|
169
|
+
*/
|
|
170
|
+
function collectDiscriminatorSubtypes(baseName, discriminator, componentSchemas) {
|
|
171
|
+
const result = [];
|
|
172
|
+
const seen = /* @__PURE__ */ new Set();
|
|
173
|
+
const mapping = isObject(discriminator.mapping) ? discriminator.mapping : void 0;
|
|
174
|
+
if (mapping !== void 0) for (const [constValue, ref] of Object.entries(mapping)) {
|
|
175
|
+
const name = componentSchemaName(ref);
|
|
176
|
+
if (name === void 0) continue;
|
|
177
|
+
if (!isObject(componentSchemas[name])) continue;
|
|
178
|
+
if (seen.has(name)) continue;
|
|
179
|
+
seen.add(name);
|
|
180
|
+
result.push({
|
|
181
|
+
name,
|
|
182
|
+
constValue
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const implicitNames = [];
|
|
186
|
+
for (const [name, schema] of Object.entries(componentSchemas)) {
|
|
187
|
+
if (!isObject(schema)) continue;
|
|
188
|
+
if (seen.has(name)) continue;
|
|
189
|
+
if (!listAllOfBaseRefs(schema).includes(baseName)) continue;
|
|
190
|
+
implicitNames.push(name);
|
|
191
|
+
}
|
|
192
|
+
implicitNames.sort();
|
|
193
|
+
for (const name of implicitNames) {
|
|
194
|
+
result.push({
|
|
195
|
+
name,
|
|
196
|
+
constValue: name
|
|
197
|
+
});
|
|
198
|
+
seen.add(name);
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Inject the discriminator `const` on a subtype schema in-place.
|
|
204
|
+
*
|
|
205
|
+
* When the subtype already declares a matching const we leave it
|
|
206
|
+
* alone. Otherwise the const is added in whichever location the walker
|
|
207
|
+
* will actually observe:
|
|
208
|
+
*
|
|
209
|
+
* - Subtype declares `allOf`: append a new `allOf` entry carrying just
|
|
210
|
+
* `{ properties: { [propertyName]: { const } } }`. The walker's
|
|
211
|
+
* `mergeAllOf` merges every entry's `properties` into the resolved
|
|
212
|
+
* schema, so the const propagates through to the merged result. A
|
|
213
|
+
* top-level `properties` sibling of `allOf` would be ignored by the
|
|
214
|
+
* merge.
|
|
215
|
+
* - Subtype does not declare `allOf`: extend the top-level `properties`
|
|
216
|
+
* block — the walker reads this directly.
|
|
217
|
+
*/
|
|
218
|
+
function injectSubtypeConst(subtype, propertyName, constValue) {
|
|
219
|
+
if (subtypeAlreadyDeclaresConst(subtype, propertyName)) return;
|
|
220
|
+
const constEntry = { properties: { [propertyName]: { const: constValue } } };
|
|
221
|
+
if (Array.isArray(subtype.allOf)) {
|
|
222
|
+
subtype.allOf = [...subtype.allOf, constEntry];
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const existingProps = isObject(subtype.properties) ? { ...subtype.properties } : {};
|
|
226
|
+
const existingDisc = existingProps[propertyName];
|
|
227
|
+
existingProps[propertyName] = {
|
|
228
|
+
...isObject(existingDisc) ? existingDisc : {},
|
|
229
|
+
const: constValue
|
|
230
|
+
};
|
|
231
|
+
subtype.properties = existingProps;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Check whether a subtype (or any of its `allOf` entries) already
|
|
235
|
+
* carries a `const` for the discriminator property. Used to avoid
|
|
236
|
+
* overwriting an author-supplied const.
|
|
237
|
+
*/
|
|
238
|
+
function subtypeAlreadyDeclaresConst(subtype, propertyName) {
|
|
239
|
+
if (hasConstProp(subtype.properties, propertyName)) return true;
|
|
240
|
+
if (Array.isArray(subtype.allOf)) for (const entry of subtype.allOf) {
|
|
241
|
+
if (!isObject(entry)) continue;
|
|
242
|
+
if (hasConstProp(entry.properties, propertyName)) return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
function hasConstProp(properties, propertyName) {
|
|
247
|
+
if (!isObject(properties)) return false;
|
|
248
|
+
const prop = properties[propertyName];
|
|
249
|
+
return isObject(prop) && "const" in prop;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Strip every `$ref` entry in a subtype's `allOf` that targets the
|
|
253
|
+
* discriminator base. The base's own schema content (properties,
|
|
254
|
+
* required, type) is replicated into a synthesised `allOf` entry so
|
|
255
|
+
* the subtype remains structurally complete — without this the base's
|
|
256
|
+
* synthesised `oneOf` would cycle through the subtype's `$ref`s on
|
|
257
|
+
* every walk (Dog → Pet.oneOf → Dog → ...).
|
|
258
|
+
*/
|
|
259
|
+
function rewriteSubtypeAllOf(subtype, baseName, baseInherited) {
|
|
260
|
+
const allOf = subtype.allOf;
|
|
261
|
+
if (!Array.isArray(allOf)) return;
|
|
262
|
+
const baseRefPrefix = `#/components/schemas/${baseName}`;
|
|
263
|
+
const rewritten = [];
|
|
264
|
+
let removedBaseRef = false;
|
|
265
|
+
for (const entry of allOf) {
|
|
266
|
+
if (isObject(entry) && typeof entry.$ref === "string" && entry.$ref === baseRefPrefix) {
|
|
267
|
+
removedBaseRef = true;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
rewritten.push(entry);
|
|
271
|
+
}
|
|
272
|
+
if (!removedBaseRef) return;
|
|
273
|
+
subtype.allOf = [baseInherited, ...rewritten];
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Capture the "inheritable" portion of a base schema before rewriting:
|
|
277
|
+
* `properties`, `required`, `type`, and any other constraint that
|
|
278
|
+
* subtypes used to inherit through `$ref`. The discriminator keyword
|
|
279
|
+
* and the synthesised `oneOf` are intentionally excluded — subtypes
|
|
280
|
+
* never inherited those and including them would re-introduce the
|
|
281
|
+
* Pet → Dog → Pet cycle.
|
|
282
|
+
*/
|
|
283
|
+
function captureBaseInherited(base) {
|
|
284
|
+
const inherited = {};
|
|
285
|
+
for (const [key, value] of Object.entries(base)) {
|
|
286
|
+
if (key === "discriminator") continue;
|
|
287
|
+
if (key === "oneOf") continue;
|
|
288
|
+
if (key === "anyOf") continue;
|
|
289
|
+
inherited[key] = value;
|
|
290
|
+
}
|
|
291
|
+
return inherited;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Build an inline `oneOf` option that targets a subtype via `$ref` and
|
|
295
|
+
* carries the discriminator property's `const` at the option's top
|
|
296
|
+
* level. The const sibling is what makes `detectDiscriminated` classify
|
|
297
|
+
* the parent `oneOf` as a discriminated union — it inspects each
|
|
298
|
+
* option's literal `properties`, not the resolved schema.
|
|
299
|
+
*/
|
|
300
|
+
function buildDiscriminatorOption(subtype, propertyName) {
|
|
301
|
+
return {
|
|
302
|
+
$ref: `#/components/schemas/${subtype.name}`,
|
|
303
|
+
properties: { [propertyName]: { const: subtype.constValue } }
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Document-level pre-pass for OpenAPI discriminators that are declared
|
|
308
|
+
* on a base schema and inherited by subtypes via `allOf`.
|
|
309
|
+
*
|
|
310
|
+
* The per-node {@link normaliseOpenApi30Discriminator} only handles
|
|
311
|
+
* discriminators that already sit alongside `oneOf`/`anyOf`. For the
|
|
312
|
+
* canonical "Cat extends Pet" pattern — where `Pet` carries the
|
|
313
|
+
* discriminator and `Cat`/`Dog` reference `Pet` via `allOf` — the
|
|
314
|
+
* discriminator is silently lost. This pre-pass:
|
|
315
|
+
*
|
|
316
|
+
* 1. Injects the discriminator `const` on each subtype's local
|
|
317
|
+
* `properties` (so a direct render of the subtype validates the
|
|
318
|
+
* discriminator value correctly).
|
|
319
|
+
* 2. Synthesises a `oneOf` on the base whenever it lacks one, listing
|
|
320
|
+
* each subtype as `{ $ref, properties: { propertyName: { const } } }`.
|
|
321
|
+
* The per-node discriminator transform then sees `oneOf` and clears
|
|
322
|
+
* the `discriminator` keyword, and the walker's
|
|
323
|
+
* `detectDiscriminated` finds the per-option `const`s.
|
|
324
|
+
*
|
|
325
|
+
* Mutates a shallow clone of `components/schemas` — the input document
|
|
326
|
+
* is never modified.
|
|
327
|
+
*/
|
|
328
|
+
function applyDiscriminatorAllOfPrepass(doc) {
|
|
329
|
+
const components = doc.components;
|
|
330
|
+
if (!isObject(components)) return doc;
|
|
331
|
+
const schemas = components.schemas;
|
|
332
|
+
if (!isObject(schemas)) return doc;
|
|
333
|
+
const plans = [];
|
|
334
|
+
for (const [baseName, base] of Object.entries(schemas)) {
|
|
335
|
+
if (!isObject(base)) continue;
|
|
336
|
+
const discriminator = base.discriminator;
|
|
337
|
+
if (!isObject(discriminator)) continue;
|
|
338
|
+
const propertyName = discriminator.propertyName;
|
|
339
|
+
if (typeof propertyName !== "string") continue;
|
|
340
|
+
const subtypes = collectDiscriminatorSubtypes(baseName, discriminator, schemas);
|
|
341
|
+
if (subtypes.length === 0) continue;
|
|
342
|
+
plans.push({
|
|
343
|
+
baseName,
|
|
344
|
+
propertyName,
|
|
345
|
+
subtypes,
|
|
346
|
+
baseHasOneOfOrAnyOf: Array.isArray(base.oneOf) || Array.isArray(base.anyOf),
|
|
347
|
+
baseInherited: captureBaseInherited(base)
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (plans.length === 0) return doc;
|
|
351
|
+
const newSchemas = { ...schemas };
|
|
352
|
+
const cloneSchema = (name) => {
|
|
353
|
+
const existing = newSchemas[name];
|
|
354
|
+
if (!isObject(existing)) throw new Error(`applyDiscriminatorAllOfPrepass: schema "${name}" disappeared between planning and rewrite`);
|
|
355
|
+
const clone = { ...existing };
|
|
356
|
+
newSchemas[name] = clone;
|
|
357
|
+
return clone;
|
|
358
|
+
};
|
|
359
|
+
for (const plan of plans) {
|
|
360
|
+
for (const subtype of plan.subtypes) {
|
|
361
|
+
const clone = cloneSchema(subtype.name);
|
|
362
|
+
rewriteSubtypeAllOf(clone, plan.baseName, plan.baseInherited);
|
|
363
|
+
injectSubtypeConst(clone, plan.propertyName, subtype.constValue);
|
|
364
|
+
}
|
|
365
|
+
if (!plan.baseHasOneOfOrAnyOf) {
|
|
366
|
+
const baseClone = cloneSchema(plan.baseName);
|
|
367
|
+
baseClone.oneOf = plan.subtypes.map((subtype) => buildDiscriminatorOption(subtype, plan.propertyName));
|
|
368
|
+
delete baseClone.properties;
|
|
369
|
+
delete baseClone.required;
|
|
370
|
+
delete baseClone.type;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
...doc,
|
|
375
|
+
components: {
|
|
376
|
+
...components,
|
|
377
|
+
schemas: newSchemas
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
120
382
|
* Combined OpenAPI 3.0.x node transform: Draft 04 + nullable + discriminator.
|
|
121
383
|
* Applied to every schema node in an OpenAPI 3.0 document.
|
|
122
384
|
*
|
|
@@ -226,7 +488,7 @@ function normaliseParameter(param, normaliseSchema) {
|
|
|
226
488
|
const content = param.content;
|
|
227
489
|
if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
|
|
228
490
|
if ("example" in result && !("examples" in result)) {
|
|
229
|
-
result.examples =
|
|
491
|
+
result.examples = { default: { value: result.example } };
|
|
230
492
|
delete result.example;
|
|
231
493
|
} else if ("example" in result) delete result.example;
|
|
232
494
|
return result;
|
|
@@ -252,7 +514,7 @@ function normaliseHeader(header, normaliseSchema) {
|
|
|
252
514
|
const content = header.content;
|
|
253
515
|
if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
|
|
254
516
|
if ("example" in result && !("examples" in result)) {
|
|
255
|
-
result.examples =
|
|
517
|
+
result.examples = { default: { value: result.example } };
|
|
256
518
|
delete result.example;
|
|
257
519
|
} else if ("example" in result) delete result.example;
|
|
258
520
|
return result;
|
|
@@ -325,9 +587,19 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
325
587
|
version: "0.0.0"
|
|
326
588
|
}
|
|
327
589
|
};
|
|
328
|
-
if (typeof doc.host
|
|
329
|
-
|
|
330
|
-
|
|
590
|
+
if (typeof doc.host !== "string") {
|
|
591
|
+
if (Array.isArray(doc.schemes) || typeof doc.basePath === "string") emitDiagnostic(diagnostics, {
|
|
592
|
+
code: "swagger-missing-host",
|
|
593
|
+
message: "Swagger 2.0 document declares schemes or basePath without host; skipping server URL synthesis",
|
|
594
|
+
pointer: "",
|
|
595
|
+
detail: {
|
|
596
|
+
hasSchemes: Array.isArray(doc.schemes),
|
|
597
|
+
hasBasePath: typeof doc.basePath === "string"
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
} else {
|
|
601
|
+
const host = doc.host;
|
|
602
|
+
const basePath = typeof doc.basePath === "string" ? doc.basePath : "";
|
|
331
603
|
const schemes = Array.isArray(doc.schemes) ? doc.schemes : ["https"];
|
|
332
604
|
result.servers = [{ url: `${typeof schemes[0] === "string" ? schemes[0] : "https"}://${host}${basePath}` }];
|
|
333
605
|
}
|
|
@@ -343,31 +615,62 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
343
615
|
const parameters = doc.parameters;
|
|
344
616
|
const requestBodies = {};
|
|
345
617
|
if (isObject(parameters)) {
|
|
346
|
-
const
|
|
618
|
+
const consumesResolution = resolveSwaggerContentTypes(void 0, doc.consumes);
|
|
619
|
+
const globalConsumes = consumesResolution.types;
|
|
347
620
|
const convertedParameters = {};
|
|
348
621
|
for (const [name, param] of Object.entries(parameters)) {
|
|
349
622
|
if (!isObject(param)) {
|
|
350
623
|
convertedParameters[name] = param;
|
|
351
624
|
continue;
|
|
352
625
|
}
|
|
353
|
-
const
|
|
626
|
+
const resolution = resolveSwaggerParameter(param, doc);
|
|
627
|
+
if (resolution.kind === "cycle") {
|
|
628
|
+
emitDiagnostic(diagnostics, {
|
|
629
|
+
code: "swagger-cyclic-parameter-ref",
|
|
630
|
+
message: `Cyclic Swagger 2.0 parameter $ref "${resolution.ref}"; skipping entry`,
|
|
631
|
+
pointer: appendPointer(appendPointer("", "parameters"), name),
|
|
632
|
+
detail: {
|
|
633
|
+
ref: resolution.ref,
|
|
634
|
+
name
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const resolved = resolution.param;
|
|
354
640
|
const location = resolved.in;
|
|
355
|
-
if (location === "body")
|
|
356
|
-
|
|
357
|
-
|
|
641
|
+
if (location === "body") {
|
|
642
|
+
if (consumesResolution.source === "synthesised") emitDiagnostic(diagnostics, {
|
|
643
|
+
code: "swagger-missing-consumes",
|
|
644
|
+
message: "Global body parameter declared but document-level `consumes` is absent; defaulting to application/json",
|
|
645
|
+
pointer: appendPointer(appendPointer("", "parameters"), name),
|
|
646
|
+
detail: {
|
|
647
|
+
level: "document",
|
|
648
|
+
name
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
requestBodies[name] = buildRequestBody(resolved, globalConsumes);
|
|
652
|
+
} else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
|
|
653
|
+
else {
|
|
654
|
+
const normalised = normaliseSwaggerParameter(resolved, doc, diagnostics, appendPointer(appendPointer("", "parameters"), name));
|
|
655
|
+
if (normalised !== void 0) convertedParameters[name] = normalised;
|
|
656
|
+
}
|
|
358
657
|
}
|
|
359
658
|
if (Object.keys(convertedParameters).length > 0) components.parameters = convertedParameters;
|
|
360
659
|
}
|
|
361
660
|
const responses = doc.responses;
|
|
362
661
|
if (isObject(responses)) {
|
|
363
|
-
const
|
|
662
|
+
const producesResolution = resolveSwaggerContentTypes(void 0, doc.produces);
|
|
364
663
|
const convertedResponses = {};
|
|
365
|
-
for (const [name, response] of Object.entries(responses)) convertedResponses[name] = isObject(response) ? normaliseSwaggerSingleResponse(response, doc,
|
|
664
|
+
for (const [name, response] of Object.entries(responses)) convertedResponses[name] = isObject(response) ? normaliseSwaggerSingleResponse(response, doc, producesResolution.types, producesResolution.source, diagnostics, void 0, void 0, name) : response;
|
|
366
665
|
components.responses = convertedResponses;
|
|
367
666
|
}
|
|
368
667
|
if (Object.keys(requestBodies).length > 0) components.requestBodies = requestBodies;
|
|
369
668
|
const securityDefinitions = doc.securityDefinitions;
|
|
370
|
-
if (isObject(securityDefinitions))
|
|
669
|
+
if (isObject(securityDefinitions)) {
|
|
670
|
+
const translated = {};
|
|
671
|
+
for (const [name, scheme] of Object.entries(securityDefinitions)) translated[name] = isObject(scheme) ? translateSwaggerSecurityScheme(scheme) : scheme;
|
|
672
|
+
components.securitySchemes = translated;
|
|
673
|
+
}
|
|
371
674
|
if (Object.keys(components).length > 0) result.components = components;
|
|
372
675
|
if (Array.isArray(doc.tags)) result.tags = doc.tags;
|
|
373
676
|
if (isObject(doc.externalDocs)) result.externalDocs = doc.externalDocs;
|
|
@@ -404,17 +707,29 @@ function normaliseSwaggerPaths(paths, doc, diagnostics) {
|
|
|
404
707
|
normalisedPath[method] = normaliseSwaggerOperation(operation, doc, path, method, diagnostics);
|
|
405
708
|
}
|
|
406
709
|
const pathParams = pathItem.parameters;
|
|
407
|
-
if (Array.isArray(pathParams))
|
|
710
|
+
if (Array.isArray(pathParams)) {
|
|
711
|
+
const paramsPointer = appendPointer(appendPointer(appendPointer("", "paths"), path), "parameters");
|
|
712
|
+
const out = [];
|
|
713
|
+
for (const [index, p] of pathParams.entries()) {
|
|
714
|
+
if (!isObject(p)) {
|
|
715
|
+
out.push(p);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
const normalised = normaliseSwaggerParameter(p, doc, diagnostics, appendPointer(paramsPointer, String(index)));
|
|
719
|
+
if (normalised !== void 0) out.push(normalised);
|
|
720
|
+
}
|
|
721
|
+
normalisedPath.parameters = out;
|
|
722
|
+
}
|
|
408
723
|
result[path] = normalisedPath;
|
|
409
724
|
}
|
|
410
725
|
return result;
|
|
411
726
|
}
|
|
412
727
|
function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
|
|
413
728
|
const result = {};
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
const produces =
|
|
417
|
-
const consumes =
|
|
729
|
+
const consumesResolution = resolveSwaggerContentTypes(operation.consumes, doc.consumes);
|
|
730
|
+
const producesResolution = resolveSwaggerContentTypes(operation.produces, doc.produces);
|
|
731
|
+
const produces = producesResolution.types;
|
|
732
|
+
const consumes = consumesResolution.types;
|
|
418
733
|
for (const [key, value] of Object.entries(operation)) if (key !== "parameters" && key !== "responses" && key !== "produces" && key !== "consumes") result[key] = value;
|
|
419
734
|
const params = operation.parameters;
|
|
420
735
|
if (Array.isArray(params)) {
|
|
@@ -427,7 +742,17 @@ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
|
|
|
427
742
|
nonBodyParams.push(param);
|
|
428
743
|
continue;
|
|
429
744
|
}
|
|
430
|
-
const
|
|
745
|
+
const paramResolution = resolveSwaggerParameter(param, doc);
|
|
746
|
+
if (paramResolution.kind === "cycle") {
|
|
747
|
+
emitDiagnostic(diagnostics, {
|
|
748
|
+
code: "swagger-cyclic-parameter-ref",
|
|
749
|
+
message: `Cyclic Swagger 2.0 parameter $ref "${paramResolution.ref}"; skipping entry`,
|
|
750
|
+
pointer: appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"), String(index)),
|
|
751
|
+
detail: { ref: paramResolution.ref }
|
|
752
|
+
});
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
const resolvedParam = paramResolution.param;
|
|
431
756
|
const location = resolvedParam.in;
|
|
432
757
|
if (location === "body") {
|
|
433
758
|
if (bodyParam !== void 0) {
|
|
@@ -451,16 +776,51 @@ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
|
|
|
451
776
|
bodyParam = buildFormDataBody(resolvedParam, params);
|
|
452
777
|
usesFormData = true;
|
|
453
778
|
}
|
|
454
|
-
} else
|
|
779
|
+
} else {
|
|
780
|
+
const normalised = normaliseSwaggerParameter(resolvedParam, doc, diagnostics, appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"), String(index)));
|
|
781
|
+
if (normalised !== void 0) nonBodyParams.push(normalised);
|
|
782
|
+
}
|
|
455
783
|
}
|
|
456
784
|
if (nonBodyParams.length > 0) result.parameters = nonBodyParams;
|
|
457
|
-
if (bodyParam !== void 0)
|
|
785
|
+
if (bodyParam !== void 0) {
|
|
786
|
+
const bodyContentTypes = usesFormData ? formDataContentTypes(consumes) : consumes;
|
|
787
|
+
if (!usesFormData && consumesResolution.source === "synthesised") emitDiagnostic(diagnostics, {
|
|
788
|
+
code: "swagger-missing-consumes",
|
|
789
|
+
message: "Operation declares a body parameter but neither operation-level nor document-level `consumes` is set; defaulting to application/json",
|
|
790
|
+
pointer: appendPointer(appendPointer(appendPointer("", "paths"), path), method),
|
|
791
|
+
detail: {
|
|
792
|
+
level: "operation",
|
|
793
|
+
method
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
result.requestBody = buildRequestBody(bodyParam, bodyContentTypes);
|
|
797
|
+
}
|
|
458
798
|
}
|
|
459
799
|
const responses = operation.responses;
|
|
460
|
-
if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces);
|
|
800
|
+
if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces, producesResolution.source, diagnostics, path, method);
|
|
461
801
|
return result;
|
|
462
802
|
}
|
|
463
803
|
/**
|
|
804
|
+
* Resolve a Swagger 2.0 `consumes` or `produces` array, recording
|
|
805
|
+
* where the value came from so callers can decide whether to emit a
|
|
806
|
+
* "missing content type" diagnostic. Per the Swagger 2.0 spec, absence
|
|
807
|
+
* at BOTH levels means no body — not an implicit `application/json`.
|
|
808
|
+
*/
|
|
809
|
+
function resolveSwaggerContentTypes(operationLevel, documentLevel) {
|
|
810
|
+
if (Array.isArray(operationLevel)) return {
|
|
811
|
+
types: operationLevel,
|
|
812
|
+
source: "operation"
|
|
813
|
+
};
|
|
814
|
+
if (Array.isArray(documentLevel)) return {
|
|
815
|
+
types: documentLevel,
|
|
816
|
+
source: "document"
|
|
817
|
+
};
|
|
818
|
+
return {
|
|
819
|
+
types: ["application/json"],
|
|
820
|
+
source: "synthesised"
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
464
824
|
* Determine the request body media type for a Swagger 2.0 formData operation.
|
|
465
825
|
*
|
|
466
826
|
* Per the OAS 3 conversion rules, `application/x-www-form-urlencoded` is
|
|
@@ -475,12 +835,77 @@ function formDataContentTypes(consumes) {
|
|
|
475
835
|
return ["multipart/form-data"];
|
|
476
836
|
}
|
|
477
837
|
/**
|
|
478
|
-
*
|
|
838
|
+
* Every JSON-Schema-compatible constraint keyword Swagger 2.0 allows on
|
|
839
|
+
* a Parameter Object or Header Object alongside `type`/`format`. These
|
|
840
|
+
* lift into the synthesised `schema` so consumers see the original
|
|
841
|
+
* validation semantics under OAS 3.x's parameter shape.
|
|
842
|
+
*
|
|
843
|
+
* `allowEmptyValue` is included even though it is a Swagger 2.0
|
|
844
|
+
* parameter-level keyword in the source (not a schema keyword) — OAS
|
|
845
|
+
* 3.x defines it at the Parameter Object root, so the calling function
|
|
846
|
+
* keeps it at the parameter root rather than copying it into `schema`.
|
|
847
|
+
*/
|
|
848
|
+
const SWAGGER_PARAM_SCHEMA_KEYWORDS = [
|
|
849
|
+
"enum",
|
|
850
|
+
"default",
|
|
851
|
+
"minimum",
|
|
852
|
+
"maximum",
|
|
853
|
+
"exclusiveMinimum",
|
|
854
|
+
"exclusiveMaximum",
|
|
855
|
+
"multipleOf",
|
|
856
|
+
"minLength",
|
|
857
|
+
"maxLength",
|
|
858
|
+
"pattern",
|
|
859
|
+
"minItems",
|
|
860
|
+
"maxItems",
|
|
861
|
+
"uniqueItems"
|
|
862
|
+
];
|
|
863
|
+
/**
|
|
864
|
+
* Set of every Swagger 2.0 parameter-root keyword that must be lifted
|
|
865
|
+
* into the synthesised `schema` rather than copied onto the OAS 3.x
|
|
866
|
+
* parameter root. Includes `type`, `format`, `items` (Swagger 2.0
|
|
867
|
+
* parameter-shaped array element descriptor), `collectionFormat`
|
|
868
|
+
* (handled separately by the caller as `style`/`explode`), and every
|
|
869
|
+
* entry from {@link SWAGGER_PARAM_SCHEMA_KEYWORDS}.
|
|
870
|
+
*/
|
|
871
|
+
const PARAM_KEYWORDS_LIFTED_INTO_SCHEMA = new Set([
|
|
872
|
+
"type",
|
|
873
|
+
"format",
|
|
874
|
+
"items",
|
|
875
|
+
"collectionFormat",
|
|
876
|
+
...SWAGGER_PARAM_SCHEMA_KEYWORDS
|
|
877
|
+
]);
|
|
878
|
+
/**
|
|
879
|
+
* Synthesise an OpenAPI 3.x `schema` object from a Swagger 2.0
|
|
880
|
+
* parameter-shaped node (parameter or header). Copies `type`,
|
|
881
|
+
* `format`, and every JSON-Schema-compatible constraint that Swagger
|
|
882
|
+
* 2.0 places at the parameter root. Nested `items` is recursively
|
|
883
|
+
* synthesised the same way so array element constraints survive.
|
|
884
|
+
*/
|
|
885
|
+
function buildSchemaFromSwaggerParameterShape(node) {
|
|
886
|
+
const schema = { type: node.type };
|
|
887
|
+
if (typeof node.format === "string") schema.format = node.format;
|
|
888
|
+
for (const keyword of SWAGGER_PARAM_SCHEMA_KEYWORDS) if (node[keyword] !== void 0) schema[keyword] = node[keyword];
|
|
889
|
+
if (isObject(node.items)) schema.items = buildSchemaFromSwaggerParameterShape(node.items);
|
|
890
|
+
return schema;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Resolve a Swagger parameter that may be a `$ref`. Returns the
|
|
894
|
+
* resolved parameter object, or a cycle marker so the caller can
|
|
895
|
+
* decide how to surface the failure. Non-ref parameters resolve to
|
|
896
|
+
* themselves; ref targets that don't exist also resolve to the input
|
|
897
|
+
* (the caller treats unknown refs the same as bare parameters).
|
|
479
898
|
*/
|
|
480
899
|
function resolveSwaggerParameter(param, doc, visited = /* @__PURE__ */ new Set()) {
|
|
481
900
|
const ref = param.$ref;
|
|
482
|
-
if (typeof ref !== "string" || !ref.startsWith("#/parameters/")) return
|
|
483
|
-
|
|
901
|
+
if (typeof ref !== "string" || !ref.startsWith("#/parameters/")) return {
|
|
902
|
+
kind: "ok",
|
|
903
|
+
param
|
|
904
|
+
};
|
|
905
|
+
if (visited.has(ref)) return {
|
|
906
|
+
kind: "cycle",
|
|
907
|
+
ref
|
|
908
|
+
};
|
|
484
909
|
const nextVisited = new Set(visited);
|
|
485
910
|
nextVisited.add(ref);
|
|
486
911
|
const name = ref.slice(13);
|
|
@@ -489,33 +914,55 @@ function resolveSwaggerParameter(param, doc, visited = /* @__PURE__ */ new Set()
|
|
|
489
914
|
const resolved = globalParams[name];
|
|
490
915
|
if (isObject(resolved)) {
|
|
491
916
|
if (typeof resolved.$ref === "string") return resolveSwaggerParameter(resolved, doc, nextVisited);
|
|
492
|
-
return
|
|
917
|
+
return {
|
|
918
|
+
kind: "ok",
|
|
919
|
+
param: resolved
|
|
920
|
+
};
|
|
493
921
|
}
|
|
494
922
|
}
|
|
495
|
-
return
|
|
923
|
+
return {
|
|
924
|
+
kind: "ok",
|
|
925
|
+
param
|
|
926
|
+
};
|
|
496
927
|
}
|
|
497
928
|
/**
|
|
498
929
|
* Normalise a single Swagger parameter to OpenAPI 3.x form.
|
|
499
930
|
*/
|
|
500
|
-
function normaliseSwaggerParameter(param, doc) {
|
|
931
|
+
function normaliseSwaggerParameter(param, doc, diagnostics, pointer = "") {
|
|
501
932
|
if (typeof param.$ref === "string") {
|
|
502
|
-
const
|
|
503
|
-
if (
|
|
933
|
+
const resolution = resolveSwaggerParameter(param, doc);
|
|
934
|
+
if (resolution.kind === "cycle") {
|
|
935
|
+
emitDiagnostic(diagnostics, {
|
|
936
|
+
code: "swagger-cyclic-parameter-ref",
|
|
937
|
+
message: `Cyclic Swagger 2.0 parameter $ref "${resolution.ref}"; skipping entry`,
|
|
938
|
+
pointer,
|
|
939
|
+
detail: { ref: resolution.ref }
|
|
940
|
+
});
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const resolved = resolution.param;
|
|
944
|
+
if (resolved !== param) return normaliseSwaggerParameter(resolved, doc, diagnostics, pointer);
|
|
504
945
|
}
|
|
505
946
|
const result = {};
|
|
506
947
|
for (const [key, value] of Object.entries(param)) {
|
|
507
|
-
if (key
|
|
948
|
+
if (PARAM_KEYWORDS_LIFTED_INTO_SCHEMA.has(key)) continue;
|
|
508
949
|
result[key] = value;
|
|
509
950
|
}
|
|
510
|
-
if (typeof param.type === "string") {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
951
|
+
if (typeof param.type === "string") if (param.type === "file" && param.in !== "formData") {
|
|
952
|
+
emitDiagnostic(diagnostics, {
|
|
953
|
+
code: "swagger-invalid-file-parameter",
|
|
954
|
+
message: `Swagger 2.0 type: "file" is only valid under in: formData; converting to { type: "string", format: "binary" }`,
|
|
955
|
+
pointer,
|
|
956
|
+
detail: {
|
|
957
|
+
name: param.name,
|
|
958
|
+
in: param.in
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
result.schema = {
|
|
962
|
+
type: "string",
|
|
963
|
+
format: "binary"
|
|
964
|
+
};
|
|
965
|
+
} else result.schema = buildSchemaFromSwaggerParameterShape(param);
|
|
519
966
|
const cf = param.collectionFormat;
|
|
520
967
|
if (typeof cf === "string") switch (cf) {
|
|
521
968
|
case "csv":
|
|
@@ -606,14 +1053,14 @@ function resolveSwaggerResponse(response, doc, visited = /* @__PURE__ */ new Set
|
|
|
606
1053
|
}
|
|
607
1054
|
return response;
|
|
608
1055
|
}
|
|
609
|
-
function normaliseSwaggerResponses(responses, doc, produces) {
|
|
1056
|
+
function normaliseSwaggerResponses(responses, doc, produces, producesSource, diagnostics, path, method) {
|
|
610
1057
|
const result = {};
|
|
611
1058
|
for (const [code, response] of Object.entries(responses)) {
|
|
612
1059
|
if (!isObject(response)) {
|
|
613
1060
|
result[code] = response;
|
|
614
1061
|
continue;
|
|
615
1062
|
}
|
|
616
|
-
result[code] = normaliseSwaggerSingleResponse(response, doc, produces);
|
|
1063
|
+
result[code] = normaliseSwaggerSingleResponse(response, doc, produces, producesSource, diagnostics, path, method, code);
|
|
617
1064
|
}
|
|
618
1065
|
return result;
|
|
619
1066
|
}
|
|
@@ -626,7 +1073,7 @@ function normaliseSwaggerResponses(responses, doc, produces) {
|
|
|
626
1073
|
* operation’s `responses` map or under document-level `responses`
|
|
627
1074
|
* (now `components.responses`).
|
|
628
1075
|
*/
|
|
629
|
-
function normaliseSwaggerSingleResponse(response, doc, produces) {
|
|
1076
|
+
function normaliseSwaggerSingleResponse(response, doc, produces, producesSource = "synthesised", diagnostics, path, method, statusCode) {
|
|
630
1077
|
const resolved = resolveSwaggerResponse(response, doc);
|
|
631
1078
|
const normalised = {};
|
|
632
1079
|
for (const [key, value] of Object.entries(resolved)) if (key !== "schema" && key !== "headers") normalised[key] = value;
|
|
@@ -636,6 +1083,15 @@ function normaliseSwaggerSingleResponse(response, doc, produces) {
|
|
|
636
1083
|
const contentTypes = produces.length > 0 ? produces : ["application/json"];
|
|
637
1084
|
for (const ct of contentTypes) if (typeof ct === "string") content[ct] = { schema };
|
|
638
1085
|
normalised.content = content;
|
|
1086
|
+
if (producesSource === "synthesised") emitDiagnostic(diagnostics, {
|
|
1087
|
+
code: "swagger-missing-consumes",
|
|
1088
|
+
message: "Response declares a schema but neither operation-level nor document-level `produces` is set; defaulting to application/json",
|
|
1089
|
+
pointer: path !== void 0 && method !== void 0 && statusCode !== void 0 ? appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "responses"), statusCode) : "",
|
|
1090
|
+
detail: {
|
|
1091
|
+
level: "response",
|
|
1092
|
+
statusCode
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
639
1095
|
}
|
|
640
1096
|
const headers = resolved.headers;
|
|
641
1097
|
if (isObject(headers)) {
|
|
@@ -661,18 +1117,10 @@ function normaliseSwaggerSingleResponse(response, doc, produces) {
|
|
|
661
1117
|
function normaliseSwaggerHeader(header) {
|
|
662
1118
|
const result = {};
|
|
663
1119
|
for (const [key, value] of Object.entries(header)) {
|
|
664
|
-
if (key
|
|
1120
|
+
if (PARAM_KEYWORDS_LIFTED_INTO_SCHEMA.has(key)) continue;
|
|
665
1121
|
result[key] = value;
|
|
666
1122
|
}
|
|
667
|
-
if (typeof header.type === "string")
|
|
668
|
-
const schema = { type: header.type };
|
|
669
|
-
if (typeof header.format === "string") schema.format = header.format;
|
|
670
|
-
if (header.enum !== void 0) schema.enum = header.enum;
|
|
671
|
-
if (header.default !== void 0) schema.default = header.default;
|
|
672
|
-
if (header.minimum !== void 0) schema.minimum = header.minimum;
|
|
673
|
-
if (header.maximum !== void 0) schema.maximum = header.maximum;
|
|
674
|
-
result.schema = schema;
|
|
675
|
-
}
|
|
1123
|
+
if (typeof header.type === "string") result.schema = buildSchemaFromSwaggerParameterShape(header);
|
|
676
1124
|
const cf = header.collectionFormat;
|
|
677
1125
|
if (typeof cf === "string") switch (cf) {
|
|
678
1126
|
case "csv":
|
|
@@ -722,6 +1170,64 @@ function rewriteSwaggerRefs(node) {
|
|
|
722
1170
|
else if (Array.isArray(value)) for (const item of value) rewriteSwaggerRefs(item);
|
|
723
1171
|
}
|
|
724
1172
|
/**
|
|
1173
|
+
* Map from Swagger 2.0 `oauth2.flow` (singular) to the OAS 3.x flow key
|
|
1174
|
+
* under `flows.<key>`. `application` and `accessCode` were renamed in
|
|
1175
|
+
* OAS 3.x to align with RFC 6749 grant-type names.
|
|
1176
|
+
*/
|
|
1177
|
+
const SWAGGER_OAUTH_FLOW_RENAME = {
|
|
1178
|
+
implicit: "implicit",
|
|
1179
|
+
password: "password",
|
|
1180
|
+
application: "clientCredentials",
|
|
1181
|
+
accessCode: "authorizationCode"
|
|
1182
|
+
};
|
|
1183
|
+
/**
|
|
1184
|
+
* Translate a Swagger 2.0 Security Scheme Object into an OpenAPI 3.x
|
|
1185
|
+
* Security Scheme Object. The Swagger 2.0 spec defines three types:
|
|
1186
|
+
*
|
|
1187
|
+
* - `basic` — has no other fields; OAS 3.x represents this as
|
|
1188
|
+
* `{ type: "http", scheme: "basic" }`.
|
|
1189
|
+
* - `apiKey` — carries `name`/`in`; OAS 3.x uses the same shape.
|
|
1190
|
+
* - `oauth2` — carries `flow`/`authorizationUrl`/`tokenUrl`/`scopes` at
|
|
1191
|
+
* the root. OAS 3.x nests these under `flows.<name>` where the flow
|
|
1192
|
+
* name maps via {@link SWAGGER_OAUTH_FLOW_RENAME}.
|
|
1193
|
+
*
|
|
1194
|
+
* Unknown `type` values pass through verbatim — downstream validation
|
|
1195
|
+
* (`unknown-security-scheme-type` diagnostic in the parser) handles
|
|
1196
|
+
* those cases.
|
|
1197
|
+
*/
|
|
1198
|
+
function translateSwaggerSecurityScheme(scheme) {
|
|
1199
|
+
const type = scheme.type;
|
|
1200
|
+
if (type === "basic") {
|
|
1201
|
+
const result = {
|
|
1202
|
+
type: "http",
|
|
1203
|
+
scheme: "basic"
|
|
1204
|
+
};
|
|
1205
|
+
if (typeof scheme.description === "string") result.description = scheme.description;
|
|
1206
|
+
return result;
|
|
1207
|
+
}
|
|
1208
|
+
if (type === "oauth2") {
|
|
1209
|
+
const flowName = scheme.flow;
|
|
1210
|
+
if (typeof flowName !== "string") return {
|
|
1211
|
+
...scheme,
|
|
1212
|
+
type: "oauth2"
|
|
1213
|
+
};
|
|
1214
|
+
const renamedFlow = SWAGGER_OAUTH_FLOW_RENAME[flowName] ?? flowName;
|
|
1215
|
+
const flowBody = {};
|
|
1216
|
+
if (typeof scheme.authorizationUrl === "string") flowBody.authorizationUrl = scheme.authorizationUrl;
|
|
1217
|
+
if (typeof scheme.tokenUrl === "string") flowBody.tokenUrl = scheme.tokenUrl;
|
|
1218
|
+
if (typeof scheme.refreshUrl === "string") flowBody.refreshUrl = scheme.refreshUrl;
|
|
1219
|
+
const scopes = scheme.scopes;
|
|
1220
|
+
flowBody.scopes = isObject(scopes) ? { ...scopes } : {};
|
|
1221
|
+
const result = {
|
|
1222
|
+
type: "oauth2",
|
|
1223
|
+
flows: { [renamedFlow]: flowBody }
|
|
1224
|
+
};
|
|
1225
|
+
if (typeof scheme.description === "string") result.description = scheme.description;
|
|
1226
|
+
return result;
|
|
1227
|
+
}
|
|
1228
|
+
return { ...scheme };
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
725
1231
|
* Recursively check whether any node in the supplied subtree carries an
|
|
726
1232
|
* `xml` annotation. Walks both objects and arrays so the check works for
|
|
727
1233
|
* schemas (definitions, parameter schemas, response schemas, request body
|
|
@@ -779,58 +1285,76 @@ const SINGLE_SUBSCHEMA_KEYS = new Set([
|
|
|
779
1285
|
* Normalise each element of an unknown array by applying deepNormalise
|
|
780
1286
|
* to object elements and passing others through unchanged.
|
|
781
1287
|
*/
|
|
782
|
-
function normaliseArray(items, transform) {
|
|
1288
|
+
function normaliseArray(items, transform, visited) {
|
|
783
1289
|
const result = [];
|
|
784
|
-
for (const item of items) result.push(isObject(item) ? deepNormalise(item, transform) : item);
|
|
1290
|
+
for (const item of items) result.push(isObject(item) ? deepNormalise(item, transform, visited) : item);
|
|
785
1291
|
return result;
|
|
786
1292
|
}
|
|
787
1293
|
/**
|
|
788
1294
|
* Normalise each value of a sub-schema map (e.g. properties, $defs).
|
|
789
1295
|
*/
|
|
790
|
-
function normaliseSubSchemaMap(map, transform) {
|
|
1296
|
+
function normaliseSubSchemaMap(map, transform, visited) {
|
|
791
1297
|
const result = {};
|
|
792
|
-
for (const [k, v] of Object.entries(map)) result[k] = isObject(v) ? deepNormalise(v, transform) : v;
|
|
1298
|
+
for (const [k, v] of Object.entries(map)) result[k] = isObject(v) ? deepNormalise(v, transform, visited) : v;
|
|
793
1299
|
return result;
|
|
794
1300
|
}
|
|
795
1301
|
/**
|
|
796
1302
|
* Deep-normalise a JSON Schema object by applying a per-node transform
|
|
797
1303
|
* and recursing into every sub-schema location.
|
|
1304
|
+
*
|
|
1305
|
+
* The optional `visited` set guards against shared object references and
|
|
1306
|
+
* cycles introduced upstream (e.g. by the OpenAPI bundler's
|
|
1307
|
+
* `structuredClone`-based inlining of external refs). The walk skips
|
|
1308
|
+
* already-seen nodes by returning the original reference rather than
|
|
1309
|
+
* recursing forever.
|
|
798
1310
|
*/
|
|
799
|
-
function deepNormalise(schema, transform) {
|
|
1311
|
+
function deepNormalise(schema, transform, visited = /* @__PURE__ */ new WeakSet()) {
|
|
1312
|
+
if (visited.has(schema)) return schema;
|
|
1313
|
+
visited.add(schema);
|
|
800
1314
|
const node = transform({ ...schema });
|
|
801
1315
|
const result = {};
|
|
802
|
-
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMap(value, transform);
|
|
803
|
-
else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArray(value, transform);
|
|
804
|
-
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormalise(value, transform);
|
|
805
|
-
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArray(value, transform);
|
|
806
|
-
else if (isObject(value)) result[key] = deepNormalise(value, transform);
|
|
1316
|
+
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMap(value, transform, visited);
|
|
1317
|
+
else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArray(value, transform, visited);
|
|
1318
|
+
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormalise(value, transform, visited);
|
|
1319
|
+
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArray(value, transform, visited);
|
|
1320
|
+
else if (isObject(value)) result[key] = deepNormalise(value, transform, visited);
|
|
807
1321
|
else result[key] = value;
|
|
808
1322
|
else if (key === "dependencies" && isObject(value)) {
|
|
809
1323
|
const normalised = {};
|
|
810
|
-
for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormalise(dv, transform);
|
|
1324
|
+
for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormalise(dv, transform, visited);
|
|
811
1325
|
else normalised[dk] = dv;
|
|
812
1326
|
result[key] = normalised;
|
|
813
1327
|
} else result[key] = value;
|
|
814
1328
|
return result;
|
|
815
1329
|
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Construct a child {@link NodeContext} that descends to `segment`,
|
|
1332
|
+
* preserving document-level flags (`documentHasDynamicAnchor`,
|
|
1333
|
+
* `documentHasRecursiveAnchor`, `declaredDraft`). Centralising the copy
|
|
1334
|
+
* keeps the recursion in `deepNormaliseWithContext` from drifting from
|
|
1335
|
+
* the {@link NodeContext} shape.
|
|
1336
|
+
*/
|
|
1337
|
+
function childContext(ctx, segment) {
|
|
1338
|
+
return {
|
|
1339
|
+
diagnostics: ctx.diagnostics,
|
|
1340
|
+
pointer: appendPointer(ctx.pointer, segment),
|
|
1341
|
+
documentHasDynamicAnchor: ctx.documentHasDynamicAnchor,
|
|
1342
|
+
documentHasRecursiveAnchor: ctx.documentHasRecursiveAnchor,
|
|
1343
|
+
declaredDraft: ctx.declaredDraft
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
816
1346
|
function normaliseArrayWithContext(items, transform, ctx) {
|
|
817
1347
|
const result = [];
|
|
818
1348
|
for (let i = 0; i < items.length; i++) {
|
|
819
1349
|
const item = items[i];
|
|
820
|
-
if (isObject(item)) result.push(deepNormaliseWithContext(item, transform,
|
|
821
|
-
diagnostics: ctx.diagnostics,
|
|
822
|
-
pointer: appendPointer(ctx.pointer, String(i))
|
|
823
|
-
}));
|
|
1350
|
+
if (isObject(item)) result.push(deepNormaliseWithContext(item, transform, childContext(ctx, String(i))));
|
|
824
1351
|
else result.push(item);
|
|
825
1352
|
}
|
|
826
1353
|
return result;
|
|
827
1354
|
}
|
|
828
1355
|
function normaliseSubSchemaMapWithContext(map, transform, ctx) {
|
|
829
1356
|
const result = {};
|
|
830
|
-
for (const [k, v] of Object.entries(map)) if (isObject(v)) result[k] = deepNormaliseWithContext(v, transform,
|
|
831
|
-
diagnostics: ctx.diagnostics,
|
|
832
|
-
pointer: appendPointer(ctx.pointer, k)
|
|
833
|
-
});
|
|
1357
|
+
for (const [k, v] of Object.entries(map)) if (isObject(v)) result[k] = deepNormaliseWithContext(v, transform, childContext(ctx, k));
|
|
834
1358
|
else result[k] = v;
|
|
835
1359
|
return result;
|
|
836
1360
|
}
|
|
@@ -846,34 +1370,16 @@ function normaliseSubSchemaMapWithContext(map, transform, ctx) {
|
|
|
846
1370
|
function deepNormaliseWithContext(schema, transform, ctx) {
|
|
847
1371
|
const node = transform({ ...schema }, ctx);
|
|
848
1372
|
const result = {};
|
|
849
|
-
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMapWithContext(value, transform,
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
else if (
|
|
854
|
-
diagnostics: ctx.diagnostics,
|
|
855
|
-
pointer: appendPointer(ctx.pointer, key)
|
|
856
|
-
});
|
|
857
|
-
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormaliseWithContext(value, transform, {
|
|
858
|
-
diagnostics: ctx.diagnostics,
|
|
859
|
-
pointer: appendPointer(ctx.pointer, key)
|
|
860
|
-
});
|
|
861
|
-
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArrayWithContext(value, transform, {
|
|
862
|
-
diagnostics: ctx.diagnostics,
|
|
863
|
-
pointer: appendPointer(ctx.pointer, key)
|
|
864
|
-
});
|
|
865
|
-
else if (isObject(value)) result[key] = deepNormaliseWithContext(value, transform, {
|
|
866
|
-
diagnostics: ctx.diagnostics,
|
|
867
|
-
pointer: appendPointer(ctx.pointer, key)
|
|
868
|
-
});
|
|
1373
|
+
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMapWithContext(value, transform, childContext(ctx, key));
|
|
1374
|
+
else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArrayWithContext(value, transform, childContext(ctx, key));
|
|
1375
|
+
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormaliseWithContext(value, transform, childContext(ctx, key));
|
|
1376
|
+
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArrayWithContext(value, transform, childContext(ctx, key));
|
|
1377
|
+
else if (isObject(value)) result[key] = deepNormaliseWithContext(value, transform, childContext(ctx, key));
|
|
869
1378
|
else result[key] = value;
|
|
870
1379
|
else if (key === "dependencies" && isObject(value)) {
|
|
871
1380
|
const normalised = {};
|
|
872
|
-
const
|
|
873
|
-
for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormaliseWithContext(dv, transform,
|
|
874
|
-
diagnostics: ctx.diagnostics,
|
|
875
|
-
pointer: appendPointer(depsPointer, dk)
|
|
876
|
-
});
|
|
1381
|
+
const depsContext = childContext(ctx, key);
|
|
1382
|
+
for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormaliseWithContext(dv, transform, childContext(depsContext, dk));
|
|
877
1383
|
else normalised[dk] = dv;
|
|
878
1384
|
result[key] = normalised;
|
|
879
1385
|
} else result[key] = value;
|
|
@@ -976,6 +1482,26 @@ function validateDependentRequired(node, ctx) {
|
|
|
976
1482
|
}
|
|
977
1483
|
}
|
|
978
1484
|
/**
|
|
1485
|
+
* Translate the legacy tuple-form `items: [Schema, Schema]` keyword to
|
|
1486
|
+
* Draft 2020-12's `prefixItems`. `additionalItems` becomes the rest-element
|
|
1487
|
+
* schema and is rewritten to `items`.
|
|
1488
|
+
*
|
|
1489
|
+
* Applies to Drafts 04, 06, and 07 — all of which used the tuple form
|
|
1490
|
+
* before Draft 2020-12 split positional schemas into `prefixItems`.
|
|
1491
|
+
*
|
|
1492
|
+
* No-op when `items` is already a single sub-schema, or when `prefixItems`
|
|
1493
|
+
* is already present (do not overwrite an explicit author choice).
|
|
1494
|
+
*/
|
|
1495
|
+
function translateTupleItems(node) {
|
|
1496
|
+
if (!Array.isArray(node.items) || "prefixItems" in node) return;
|
|
1497
|
+
node.prefixItems = node.items;
|
|
1498
|
+
delete node.items;
|
|
1499
|
+
if ("additionalItems" in node) {
|
|
1500
|
+
node.items = node.additionalItems;
|
|
1501
|
+
delete node.additionalItems;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
979
1505
|
* Apply the version-agnostic Draft 04 keyword translations to a single
|
|
980
1506
|
* node: boolean exclusive-min/max → number form, bare `id` → `$id`, and
|
|
981
1507
|
* tuple-form `items` → `prefixItems`.
|
|
@@ -1036,14 +1562,7 @@ function applyDraft04Translations(node, ctx) {
|
|
|
1036
1562
|
node.$id = node.id;
|
|
1037
1563
|
delete node.id;
|
|
1038
1564
|
}
|
|
1039
|
-
|
|
1040
|
-
node.prefixItems = node.items;
|
|
1041
|
-
delete node.items;
|
|
1042
|
-
if ("additionalItems" in node) {
|
|
1043
|
-
node.items = node.additionalItems;
|
|
1044
|
-
delete node.additionalItems;
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1565
|
+
translateTupleItems(node);
|
|
1047
1566
|
splitDependencies(node, ctx, false);
|
|
1048
1567
|
validateDependentRequired(node, ctx);
|
|
1049
1568
|
}
|
|
@@ -1089,8 +1608,14 @@ function normaliseDraft04NodeWithContext(node, ctx) {
|
|
|
1089
1608
|
* (already in final form) and `const`/`examples`, but still use the
|
|
1090
1609
|
* legacy `dependencies` keyword. Split it into `dependentRequired` /
|
|
1091
1610
|
* `dependentSchemas` so the walker can process them uniformly.
|
|
1611
|
+
*
|
|
1612
|
+
* Drafts 06 and 07 also use the tuple form
|
|
1613
|
+
* `items: [Schema, Schema], additionalItems: Schema` — translate that to
|
|
1614
|
+
* Draft 2020-12's `prefixItems` + rest-`items` so the walker produces a
|
|
1615
|
+
* `TupleField` rather than silently dropping the positional schemas.
|
|
1092
1616
|
*/
|
|
1093
1617
|
function normaliseDraft06Or07NodeWithContext(node, ctx) {
|
|
1618
|
+
translateTupleItems(node);
|
|
1094
1619
|
splitDependencies(node, ctx, false);
|
|
1095
1620
|
validateDependentRequired(node, ctx);
|
|
1096
1621
|
return node;
|
|
@@ -1110,8 +1635,18 @@ function normaliseDraft06Or07NodeWithContext(node, ctx) {
|
|
|
1110
1635
|
* `$recursiveAnchor` names are likewise preserved as `$anchor`.
|
|
1111
1636
|
*/
|
|
1112
1637
|
function normaliseDraft201909NodeWithContext(node, ctx) {
|
|
1113
|
-
|
|
1114
|
-
|
|
1638
|
+
const recursiveRef = node.$recursiveRef;
|
|
1639
|
+
if (typeof recursiveRef === "string") {
|
|
1640
|
+
if (!recursiveRef.startsWith("#")) emitDiagnostic(ctx.diagnostics, {
|
|
1641
|
+
code: "dynamic-ref-degraded",
|
|
1642
|
+
message: `Cross-document \`$recursiveRef\` "${recursiveRef}" rewritten to a static \`$ref\`; dynamic-scope resolution is not preserved`,
|
|
1643
|
+
pointer: appendPointer(ctx.pointer, "$recursiveRef"),
|
|
1644
|
+
detail: {
|
|
1645
|
+
keyword: "$recursiveRef",
|
|
1646
|
+
ref: recursiveRef
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
node.$ref = recursiveRef;
|
|
1115
1650
|
delete node.$recursiveRef;
|
|
1116
1651
|
}
|
|
1117
1652
|
if (node.$recursiveAnchor === true) {
|
|
@@ -1121,12 +1656,24 @@ function normaliseDraft201909NodeWithContext(node, ctx) {
|
|
|
1121
1656
|
if (typeof node.$anchor !== "string") node.$anchor = node.$recursiveAnchor;
|
|
1122
1657
|
delete node.$recursiveAnchor;
|
|
1123
1658
|
}
|
|
1659
|
+
splitDependencies(node, ctx, false);
|
|
1124
1660
|
validateDependentRequired(node, ctx);
|
|
1125
1661
|
return node;
|
|
1126
1662
|
}
|
|
1127
1663
|
function normaliseDynamicRefNodeWithContext(node, ctx) {
|
|
1128
|
-
|
|
1129
|
-
|
|
1664
|
+
const dynamicRef = node.$dynamicRef;
|
|
1665
|
+
if (typeof dynamicRef === "string") {
|
|
1666
|
+
const crossDocument = !dynamicRef.startsWith("#");
|
|
1667
|
+
if (crossDocument || ctx.documentHasDynamicAnchor) emitDiagnostic(ctx.diagnostics, {
|
|
1668
|
+
code: "dynamic-ref-degraded",
|
|
1669
|
+
message: crossDocument ? `Cross-document \`$dynamicRef\` "${dynamicRef}" rewritten to a static \`$ref\`; dynamic-scope resolution is not preserved` : `\`$dynamicRef\` "${dynamicRef}" rewritten to a static \`$ref\` in a document declaring \`$dynamicAnchor\`; dynamic-scope resolution is not preserved`,
|
|
1670
|
+
pointer: appendPointer(ctx.pointer, "$dynamicRef"),
|
|
1671
|
+
detail: {
|
|
1672
|
+
keyword: "$dynamicRef",
|
|
1673
|
+
ref: dynamicRef
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
node.$ref = dynamicRef;
|
|
1130
1677
|
delete node.$dynamicRef;
|
|
1131
1678
|
}
|
|
1132
1679
|
if (typeof node.$dynamicAnchor === "string") {
|
|
@@ -1138,6 +1685,63 @@ function normaliseDynamicRefNodeWithContext(node, ctx) {
|
|
|
1138
1685
|
return node;
|
|
1139
1686
|
}
|
|
1140
1687
|
/**
|
|
1688
|
+
* Pick the per-node transform that normalises a single Schema Object to
|
|
1689
|
+
* canonical Draft 2020-12 form for the supplied draft. Exposed so the
|
|
1690
|
+
* OpenAPI 3.1 path can honour a non-default `jsonSchemaDialect`
|
|
1691
|
+
* declaration by routing each Schema Object through the matching
|
|
1692
|
+
* transform without re-implementing the dispatch.
|
|
1693
|
+
*/
|
|
1694
|
+
function selectDraftTransform(draft) {
|
|
1695
|
+
switch (draft) {
|
|
1696
|
+
case "draft-04": return normaliseDraft04NodeWithContext;
|
|
1697
|
+
case "draft-06":
|
|
1698
|
+
case "draft-07": return normaliseDraft06Or07NodeWithContext;
|
|
1699
|
+
case "draft-2019-09": return normaliseDraft201909NodeWithContext;
|
|
1700
|
+
case "draft-2020-12": return normaliseDynamicRefNodeWithContext;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Scan a JSON document body for the presence of a named keyword
|
|
1705
|
+
* anywhere in the structure. Walks both arrays and objects without
|
|
1706
|
+
* regard to schema-vs-data position — the caller is responsible for
|
|
1707
|
+
* passing a keyword whose presence is meaningful at any depth.
|
|
1708
|
+
*
|
|
1709
|
+
* Cycle-safe: cyclic references introduced by the OpenAPI bundler's
|
|
1710
|
+
* `structuredClone` of external refs are short-circuited via the
|
|
1711
|
+
* `visited` set so the scan terminates.
|
|
1712
|
+
*/
|
|
1713
|
+
function documentContainsKeyword(value, keyword, visited = /* @__PURE__ */ new WeakSet()) {
|
|
1714
|
+
if (Array.isArray(value)) {
|
|
1715
|
+
if (visited.has(value)) return false;
|
|
1716
|
+
visited.add(value);
|
|
1717
|
+
for (const item of value) if (documentContainsKeyword(item, keyword, visited)) return true;
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1720
|
+
if (isObject(value)) {
|
|
1721
|
+
if (visited.has(value)) return false;
|
|
1722
|
+
visited.add(value);
|
|
1723
|
+
if (keyword in value) return true;
|
|
1724
|
+
for (const v of Object.values(value)) if (documentContainsKeyword(v, keyword, visited)) return true;
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Build a root {@link NodeContext} for a document being normalised.
|
|
1731
|
+
* Pre-scans for `$dynamicAnchor` and `$recursiveAnchor` so per-node
|
|
1732
|
+
* transforms can decide whether a `$dynamicRef`/`$recursiveRef`
|
|
1733
|
+
* rewrite needs a `dynamic-ref-degraded` diagnostic.
|
|
1734
|
+
*/
|
|
1735
|
+
function buildRootContext(schema, diagnostics, declaredDraft) {
|
|
1736
|
+
return {
|
|
1737
|
+
diagnostics,
|
|
1738
|
+
pointer: "",
|
|
1739
|
+
documentHasDynamicAnchor: documentContainsKeyword(schema, "$dynamicAnchor"),
|
|
1740
|
+
documentHasRecursiveAnchor: documentContainsKeyword(schema, "$recursiveAnchor"),
|
|
1741
|
+
declaredDraft
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1141
1745
|
* Normalise a JSON Schema to canonical Draft 2020-12 form.
|
|
1142
1746
|
* Deep-clones the input — the original is never mutated.
|
|
1143
1747
|
*
|
|
@@ -1147,27 +1751,8 @@ function normaliseDynamicRefNodeWithContext(node, ctx) {
|
|
|
1147
1751
|
* `dependencies` reaching the 2020-12 path).
|
|
1148
1752
|
*/
|
|
1149
1753
|
function normaliseJsonSchema(schema, draft, diagnostics) {
|
|
1150
|
-
const ctx =
|
|
1151
|
-
|
|
1152
|
-
pointer: ""
|
|
1153
|
-
};
|
|
1154
|
-
let normalised;
|
|
1155
|
-
switch (draft) {
|
|
1156
|
-
case "draft-04":
|
|
1157
|
-
normalised = deepNormaliseWithContext(schema, normaliseDraft04NodeWithContext, ctx);
|
|
1158
|
-
break;
|
|
1159
|
-
case "draft-2019-09":
|
|
1160
|
-
normalised = deepNormaliseWithContext(schema, normaliseDraft201909NodeWithContext, ctx);
|
|
1161
|
-
break;
|
|
1162
|
-
case "draft-2020-12":
|
|
1163
|
-
normalised = deepNormaliseWithContext(schema, normaliseDynamicRefNodeWithContext, ctx);
|
|
1164
|
-
break;
|
|
1165
|
-
case "draft-06":
|
|
1166
|
-
case "draft-07":
|
|
1167
|
-
normalised = deepNormaliseWithContext(schema, normaliseDraft06Or07NodeWithContext, ctx);
|
|
1168
|
-
break;
|
|
1169
|
-
}
|
|
1170
|
-
return resolveRelativeRefs(normalised, diagnostics);
|
|
1754
|
+
const ctx = buildRootContext(schema, diagnostics, draft);
|
|
1755
|
+
return resolveRelativeRefs(deepNormaliseWithContext(schema, selectDraftTransform(draft), ctx), diagnostics);
|
|
1171
1756
|
}
|
|
1172
1757
|
/**
|
|
1173
1758
|
* Parse a string as an absolute URI, returning `undefined` when it has
|
|
@@ -1205,6 +1790,29 @@ function stripFragment(url) {
|
|
|
1205
1790
|
return clone.toString();
|
|
1206
1791
|
}
|
|
1207
1792
|
/**
|
|
1793
|
+
* Emit an `invalid-id-fragment` diagnostic when an `$id` value carries
|
|
1794
|
+
* a fragment that will be stripped during base-URI resolution. Per
|
|
1795
|
+
* JSON Schema 2020-12 §8.2.1, the URI in `$id` MUST NOT contain a
|
|
1796
|
+
* non-empty fragment (an empty `#` fragment is permitted for historical
|
|
1797
|
+
* reasons but conveys nothing). Stripping it silently loses authoring
|
|
1798
|
+
* intent — the caller almost certainly meant to declare an `$anchor`
|
|
1799
|
+
* or sibling identifier instead.
|
|
1800
|
+
*/
|
|
1801
|
+
function reportFragmentInId(value, url, pointer, diagnostics) {
|
|
1802
|
+
if (diagnostics === void 0) return;
|
|
1803
|
+
if (typeof value !== "string") return;
|
|
1804
|
+
if (url.hash.length === 0) return;
|
|
1805
|
+
emitDiagnostic(diagnostics, {
|
|
1806
|
+
code: "invalid-id-fragment",
|
|
1807
|
+
message: `\`$id\` URI "${value}" includes the fragment "${url.hash}", which is not permitted by JSON Schema §8.2.1; the fragment is stripped before use`,
|
|
1808
|
+
pointer: appendPointer(pointer, "$id"),
|
|
1809
|
+
detail: {
|
|
1810
|
+
id: value,
|
|
1811
|
+
fragment: url.hash
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1208
1816
|
* Recursively rewrite relative `$ref`s in a schema so they resolve
|
|
1209
1817
|
* correctly under the JSON Schema base-URI rules (RFC 3986 + JSON
|
|
1210
1818
|
* Schema §8.2). Refs that resolve to the document's own `$id` are
|
|
@@ -1219,14 +1827,29 @@ function resolveRelativeRefs(schema, diagnostics) {
|
|
|
1219
1827
|
const docBaseUrl = parseAbsoluteUri(schema.$id);
|
|
1220
1828
|
if (docBaseUrl === void 0) return schema;
|
|
1221
1829
|
const docBase = stripFragment(docBaseUrl);
|
|
1222
|
-
return rewriteRelativeRefsNode(schema, docBase, docBase, "", diagnostics);
|
|
1830
|
+
return rewriteRelativeRefsNode(schema, docBase, docBase, "", diagnostics, /* @__PURE__ */ new WeakSet());
|
|
1223
1831
|
}
|
|
1224
|
-
|
|
1832
|
+
/**
|
|
1833
|
+
* Rewrite relative `$ref`s in a single node, recursing into sub-schemas.
|
|
1834
|
+
*
|
|
1835
|
+
* The `visited` set guards against shared object references and cycles
|
|
1836
|
+
* introduced upstream (e.g. by the OpenAPI bundler's `structuredClone`
|
|
1837
|
+
* inlining of external refs). When a node is re-encountered the rewrite
|
|
1838
|
+
* is short-circuited — returning the original reference unchanged is
|
|
1839
|
+
* sound because every relative `$ref` in the subtree was already
|
|
1840
|
+
* rewritten on the first visit.
|
|
1841
|
+
*/
|
|
1842
|
+
function rewriteRelativeRefsNode(node, currentBase, docBase, pointer, diagnostics, visited) {
|
|
1843
|
+
if (visited.has(node)) return node;
|
|
1844
|
+
visited.add(node);
|
|
1225
1845
|
let nextBase = currentBase;
|
|
1226
1846
|
const nodeId = node.$id;
|
|
1227
1847
|
if (typeof nodeId === "string" && nodeId.length > 0) {
|
|
1228
1848
|
const resolved = resolveAgainst(nodeId, currentBase);
|
|
1229
|
-
if (resolved !== void 0)
|
|
1849
|
+
if (resolved !== void 0) {
|
|
1850
|
+
reportFragmentInId(nodeId, resolved, pointer, diagnostics);
|
|
1851
|
+
nextBase = stripFragment(resolved);
|
|
1852
|
+
}
|
|
1230
1853
|
}
|
|
1231
1854
|
const result = {};
|
|
1232
1855
|
for (const [key, value] of Object.entries(node)) {
|
|
@@ -1234,13 +1857,17 @@ function rewriteRelativeRefsNode(node, currentBase, docBase, pointer, diagnostic
|
|
|
1234
1857
|
result[key] = rewriteRef(value, nextBase, docBase, appendPointer(pointer, key), diagnostics);
|
|
1235
1858
|
continue;
|
|
1236
1859
|
}
|
|
1237
|
-
result[key] = rewriteRelativeRefsValue(value,
|
|
1860
|
+
result[key] = rewriteRelativeRefsValue(value, nextBase, docBase, appendPointer(pointer, key), diagnostics, visited);
|
|
1238
1861
|
}
|
|
1239
1862
|
return result;
|
|
1240
1863
|
}
|
|
1241
|
-
function rewriteRelativeRefsValue(value,
|
|
1242
|
-
if (Array.isArray(value))
|
|
1243
|
-
|
|
1864
|
+
function rewriteRelativeRefsValue(value, currentBase, docBase, pointer, diagnostics, visited) {
|
|
1865
|
+
if (Array.isArray(value)) {
|
|
1866
|
+
if (visited.has(value)) return value;
|
|
1867
|
+
visited.add(value);
|
|
1868
|
+
return value.map((item, i) => rewriteRelativeRefsValue(item, currentBase, docBase, appendPointer(pointer, String(i)), diagnostics, visited));
|
|
1869
|
+
}
|
|
1870
|
+
if (isObject(value)) return rewriteRelativeRefsNode(value, currentBase, docBase, pointer, diagnostics, visited);
|
|
1244
1871
|
return value;
|
|
1245
1872
|
}
|
|
1246
1873
|
/**
|
|
@@ -1290,7 +1917,25 @@ function rewriteRef(ref, currentBase, docBase, pointer, diagnostics) {
|
|
|
1290
1917
|
*/
|
|
1291
1918
|
function normaliseOpenApiSchemas(doc, version, diagnostics) {
|
|
1292
1919
|
if (isSwagger2(version)) return normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, diagnostics);
|
|
1293
|
-
if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(doc, deepNormalise);
|
|
1920
|
+
if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(applyDiscriminatorAllOfPrepass(doc), deepNormalise);
|
|
1921
|
+
if (!isOpenApi31(version)) {
|
|
1922
|
+
const rawOpenApi = typeof doc.openapi === "string" ? doc.openapi : void 0;
|
|
1923
|
+
const rawSwagger = typeof doc.swagger === "string" ? doc.swagger : void 0;
|
|
1924
|
+
const versionLabel = rawOpenApi ?? rawSwagger ?? `${String(version.major)}.${String(version.minor)}.${String(version.patch)}`;
|
|
1925
|
+
const pointer = rawOpenApi !== void 0 ? "/openapi" : "/swagger";
|
|
1926
|
+
emitDiagnostic(diagnostics, {
|
|
1927
|
+
code: "unknown-openapi-version",
|
|
1928
|
+
message: `Unsupported OpenAPI/Swagger version "${versionLabel}"; falling back to the OpenAPI 3.1 pipeline`,
|
|
1929
|
+
pointer,
|
|
1930
|
+
detail: {
|
|
1931
|
+
version: versionLabel,
|
|
1932
|
+
major: version.major,
|
|
1933
|
+
minor: version.minor,
|
|
1934
|
+
patch: version.patch
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
let dialectDraft;
|
|
1294
1939
|
if (isOpenApi31(version)) {
|
|
1295
1940
|
const dialect = readJsonSchemaDialect(doc);
|
|
1296
1941
|
if (dialect.kind === "unknown") emitDiagnostic(diagnostics, {
|
|
@@ -1299,8 +1944,13 @@ function normaliseOpenApiSchemas(doc, version, diagnostics) {
|
|
|
1299
1944
|
pointer: "/jsonSchemaDialect",
|
|
1300
1945
|
detail: { uri: dialect.uri }
|
|
1301
1946
|
});
|
|
1947
|
+
else if (dialect.kind === "known") dialectDraft = dialect.draft;
|
|
1302
1948
|
}
|
|
1303
|
-
return deepNormaliseOpenApiDoc(doc, (schema) =>
|
|
1949
|
+
return deepNormaliseOpenApiDoc(applyDiscriminatorAllOfPrepass(doc), (schema) => {
|
|
1950
|
+
let intermediate = schema;
|
|
1951
|
+
if (dialectDraft !== void 0 && dialectDraft !== "draft-2020-12") intermediate = deepNormaliseWithContext(intermediate, selectDraftTransform(dialectDraft), buildRootContext(intermediate, diagnostics, dialectDraft));
|
|
1952
|
+
return resolveRelativeRefs(deepNormalise(intermediate, normaliseOpenApi30Discriminator), diagnostics);
|
|
1953
|
+
});
|
|
1304
1954
|
}
|
|
1305
1955
|
//#endregion
|
|
1306
|
-
export { normaliseOpenApiSchemas as a,
|
|
1956
|
+
export { normaliseOpenApiSchemas as a, applyDiscriminatorAllOfPrepass as c, normaliseOpenApi30Combined as d, normaliseOpenApi30Discriminator as f, normaliseJsonSchema as i, deepNormaliseOpenApi30Doc as l, deepNormaliseWithContext as n, selectDraftTransform as o, normaliseOpenApi30Node as p, normaliseDraft04Node as r, normaliseSwagger2Document as s, deepNormalise as t, deepNormaliseOpenApiDoc as u };
|