schema-components 1.19.0 → 1.21.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/dist/core/adapter.d.mts +10 -3
- package/dist/core/adapter.mjs +237 -31
- 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 +10 -8
- package/dist/core/fieldOrder.d.mts +1 -1
- package/dist/core/formats.d.mts +21 -14
- package/dist/core/formats.mjs +88 -4
- package/dist/core/merge.d.mts +11 -2
- package/dist/core/merge.mjs +11 -0
- package/dist/core/normalise.d.mts +9 -3
- package/dist/core/normalise.mjs +1 -1
- 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 +34 -9
- package/dist/core/renderer.d.mts +1 -1
- 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 +1 -1
- package/dist/core/uri.d.mts +41 -0
- package/dist/core/uri.mjs +76 -0
- package/dist/core/version.d.mts +2 -2
- package/dist/core/version.mjs +25 -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 +80 -26
- package/dist/{diagnostics-VgEKI_Ct.d.mts → diagnostics-CbBPsxSt.d.mts} +1 -1
- package/dist/{errors-CnGjT1cg.d.mts → errors-QEwOtQAA.d.mts} +8 -5
- package/dist/html/a11y.d.mts +2 -2
- package/dist/html/renderToHtml.d.mts +2 -2
- package/dist/html/renderToHtmlStream.d.mts +2 -2
- package/dist/html/renderers.d.mts +2 -2
- package/dist/html/renderers.mjs +9 -2
- package/dist/html/streamRenderers.d.mts +2 -2
- package/dist/{normalise-C0ofw3W6.mjs → normalise-DaSrnr8g.mjs} +574 -40
- 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 +113 -7
- package/dist/openapi/bundle.mjs +2 -0
- package/dist/openapi/components.d.mts +32 -10
- package/dist/openapi/components.mjs +37 -16
- package/dist/openapi/parser.d.mts +1 -1
- package/dist/openapi/parser.mjs +41 -4
- package/dist/openapi/resolve.d.mts +70 -9
- package/dist/openapi/resolve.mjs +124 -24
- package/dist/react/SchemaComponent.d.mts +21 -9
- package/dist/react/SchemaComponent.mjs +32 -4
- package/dist/react/SchemaView.d.mts +3 -3
- package/dist/react/fieldPath.d.mts +1 -1
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headlessRenderers.d.mts +2 -2
- package/dist/react/headlessRenderers.mjs +18 -6
- package/dist/{ref-Bb43ZURY.d.mts → ref-si8ViYun.d.mts} +7 -2
- package/dist/{renderer-BQqiXUYP.d.mts → renderer-DI6ZYf7a.d.mts} +1 -1
- 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-Bxw3NOG1.d.mts +647 -0
- package/dist/{types-D_5ST7SS.d.mts → types-BnxPEElk.d.mts} +18 -2
- package/dist/{version-XNH7PRGP.d.mts → version-D-u7aMfy.d.mts} +36 -1
- package/package.json +1 -1
- package/dist/typeInference-5JiqIZ8t.d.mts +0 -388
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isObject } from "./core/guards.mjs";
|
|
2
2
|
import { appendPointer, emitDiagnostic } from "./core/diagnostics.mjs";
|
|
3
|
-
import { isOpenApi30, isSwagger2 } from "./core/version.mjs";
|
|
3
|
+
import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./core/version.mjs";
|
|
4
4
|
//#region src/core/openapi30.ts
|
|
5
5
|
/**
|
|
6
6
|
* OpenAPI 3.0.x schema normalisation.
|
|
@@ -117,6 +117,260 @@ function normaliseOpenApi30Discriminator(node) {
|
|
|
117
117
|
return node;
|
|
118
118
|
}
|
|
119
119
|
/**
|
|
120
|
+
* Returns the schema name a `$ref` points at when it targets
|
|
121
|
+
* `#/components/schemas/<Name>`, or `undefined` otherwise.
|
|
122
|
+
*
|
|
123
|
+
* The walker only resolves intra-document refs and other allOf-base
|
|
124
|
+
* patterns; refs into `definitions` (Swagger 2.0) are already rewritten
|
|
125
|
+
* before this stage.
|
|
126
|
+
*/
|
|
127
|
+
function componentSchemaName(ref) {
|
|
128
|
+
if (typeof ref !== "string") return void 0;
|
|
129
|
+
if (!ref.startsWith("#/components/schemas/")) return void 0;
|
|
130
|
+
const name = ref.slice(21);
|
|
131
|
+
return name.length > 0 ? name : void 0;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Find every immediate `$ref` that an `allOf` array contains pointing
|
|
135
|
+
* back at a `components/schemas/<Name>` entry. Used to discover
|
|
136
|
+
* "Cat extends Pet"-style inheritance — the subtype's `allOf` lists
|
|
137
|
+
* the base by `$ref` alongside its own local fields.
|
|
138
|
+
*/
|
|
139
|
+
function listAllOfBaseRefs(schema) {
|
|
140
|
+
const allOf = schema.allOf;
|
|
141
|
+
if (!Array.isArray(allOf)) return [];
|
|
142
|
+
const result = [];
|
|
143
|
+
for (const entry of allOf) {
|
|
144
|
+
if (!isObject(entry)) continue;
|
|
145
|
+
const name = componentSchemaName(entry.$ref);
|
|
146
|
+
if (name !== void 0) result.push(name);
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Collect discriminator subtypes for a base schema. Entries come from:
|
|
152
|
+
*
|
|
153
|
+
* 1. The base's `discriminator.mapping` (explicit author intent — the
|
|
154
|
+
* mapping key supplies the `const` value, the ref names the subtype).
|
|
155
|
+
* 2. Component schemas whose `allOf` lists this base by `$ref` and
|
|
156
|
+
* were not already named in the mapping. The `const` value defaults
|
|
157
|
+
* to the subtype's component name.
|
|
158
|
+
*
|
|
159
|
+
* Returned in deterministic order: mapping entries first (preserving
|
|
160
|
+
* authored order), then implicit subtypes alphabetically.
|
|
161
|
+
*/
|
|
162
|
+
function collectDiscriminatorSubtypes(baseName, discriminator, componentSchemas) {
|
|
163
|
+
const result = [];
|
|
164
|
+
const seen = /* @__PURE__ */ new Set();
|
|
165
|
+
const mapping = isObject(discriminator.mapping) ? discriminator.mapping : void 0;
|
|
166
|
+
if (mapping !== void 0) for (const [constValue, ref] of Object.entries(mapping)) {
|
|
167
|
+
const name = componentSchemaName(ref);
|
|
168
|
+
if (name === void 0) continue;
|
|
169
|
+
if (!isObject(componentSchemas[name])) continue;
|
|
170
|
+
if (seen.has(name)) continue;
|
|
171
|
+
seen.add(name);
|
|
172
|
+
result.push({
|
|
173
|
+
name,
|
|
174
|
+
constValue
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const implicitNames = [];
|
|
178
|
+
for (const [name, schema] of Object.entries(componentSchemas)) {
|
|
179
|
+
if (!isObject(schema)) continue;
|
|
180
|
+
if (seen.has(name)) continue;
|
|
181
|
+
if (!listAllOfBaseRefs(schema).includes(baseName)) continue;
|
|
182
|
+
implicitNames.push(name);
|
|
183
|
+
}
|
|
184
|
+
implicitNames.sort();
|
|
185
|
+
for (const name of implicitNames) {
|
|
186
|
+
result.push({
|
|
187
|
+
name,
|
|
188
|
+
constValue: name
|
|
189
|
+
});
|
|
190
|
+
seen.add(name);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Inject the discriminator `const` on a subtype schema in-place.
|
|
196
|
+
*
|
|
197
|
+
* When the subtype already declares a matching const we leave it
|
|
198
|
+
* alone. Otherwise the const is added in whichever location the walker
|
|
199
|
+
* will actually observe:
|
|
200
|
+
*
|
|
201
|
+
* - Subtype declares `allOf`: append a new `allOf` entry carrying just
|
|
202
|
+
* `{ properties: { [propertyName]: { const } } }`. The walker's
|
|
203
|
+
* `mergeAllOf` merges every entry's `properties` into the resolved
|
|
204
|
+
* schema, so the const propagates through to the merged result. A
|
|
205
|
+
* top-level `properties` sibling of `allOf` would be ignored by the
|
|
206
|
+
* merge.
|
|
207
|
+
* - Subtype does not declare `allOf`: extend the top-level `properties`
|
|
208
|
+
* block — the walker reads this directly.
|
|
209
|
+
*/
|
|
210
|
+
function injectSubtypeConst(subtype, propertyName, constValue) {
|
|
211
|
+
if (subtypeAlreadyDeclaresConst(subtype, propertyName)) return;
|
|
212
|
+
const constEntry = { properties: { [propertyName]: { const: constValue } } };
|
|
213
|
+
if (Array.isArray(subtype.allOf)) {
|
|
214
|
+
subtype.allOf = [...subtype.allOf, constEntry];
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const existingProps = isObject(subtype.properties) ? { ...subtype.properties } : {};
|
|
218
|
+
const existingDisc = existingProps[propertyName];
|
|
219
|
+
existingProps[propertyName] = {
|
|
220
|
+
...isObject(existingDisc) ? existingDisc : {},
|
|
221
|
+
const: constValue
|
|
222
|
+
};
|
|
223
|
+
subtype.properties = existingProps;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Check whether a subtype (or any of its `allOf` entries) already
|
|
227
|
+
* carries a `const` for the discriminator property. Used to avoid
|
|
228
|
+
* overwriting an author-supplied const.
|
|
229
|
+
*/
|
|
230
|
+
function subtypeAlreadyDeclaresConst(subtype, propertyName) {
|
|
231
|
+
if (hasConstProp(subtype.properties, propertyName)) return true;
|
|
232
|
+
if (Array.isArray(subtype.allOf)) for (const entry of subtype.allOf) {
|
|
233
|
+
if (!isObject(entry)) continue;
|
|
234
|
+
if (hasConstProp(entry.properties, propertyName)) return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
function hasConstProp(properties, propertyName) {
|
|
239
|
+
if (!isObject(properties)) return false;
|
|
240
|
+
const prop = properties[propertyName];
|
|
241
|
+
return isObject(prop) && "const" in prop;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Strip every `$ref` entry in a subtype's `allOf` that targets the
|
|
245
|
+
* discriminator base. The base's own schema content (properties,
|
|
246
|
+
* required, type) is replicated into a synthesised `allOf` entry so
|
|
247
|
+
* the subtype remains structurally complete — without this the base's
|
|
248
|
+
* synthesised `oneOf` would cycle through the subtype's `$ref`s on
|
|
249
|
+
* every walk (Dog → Pet.oneOf → Dog → ...).
|
|
250
|
+
*/
|
|
251
|
+
function rewriteSubtypeAllOf(subtype, baseName, baseInherited) {
|
|
252
|
+
const allOf = subtype.allOf;
|
|
253
|
+
if (!Array.isArray(allOf)) return;
|
|
254
|
+
const baseRefPrefix = `#/components/schemas/${baseName}`;
|
|
255
|
+
const rewritten = [];
|
|
256
|
+
let removedBaseRef = false;
|
|
257
|
+
for (const entry of allOf) {
|
|
258
|
+
if (isObject(entry) && typeof entry.$ref === "string" && entry.$ref === baseRefPrefix) {
|
|
259
|
+
removedBaseRef = true;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
rewritten.push(entry);
|
|
263
|
+
}
|
|
264
|
+
if (!removedBaseRef) return;
|
|
265
|
+
subtype.allOf = [baseInherited, ...rewritten];
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Capture the "inheritable" portion of a base schema before rewriting:
|
|
269
|
+
* `properties`, `required`, `type`, and any other constraint that
|
|
270
|
+
* subtypes used to inherit through `$ref`. The discriminator keyword
|
|
271
|
+
* and the synthesised `oneOf` are intentionally excluded — subtypes
|
|
272
|
+
* never inherited those and including them would re-introduce the
|
|
273
|
+
* Pet → Dog → Pet cycle.
|
|
274
|
+
*/
|
|
275
|
+
function captureBaseInherited(base) {
|
|
276
|
+
const inherited = {};
|
|
277
|
+
for (const [key, value] of Object.entries(base)) {
|
|
278
|
+
if (key === "discriminator") continue;
|
|
279
|
+
if (key === "oneOf") continue;
|
|
280
|
+
if (key === "anyOf") continue;
|
|
281
|
+
inherited[key] = value;
|
|
282
|
+
}
|
|
283
|
+
return inherited;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Build an inline `oneOf` option that targets a subtype via `$ref` and
|
|
287
|
+
* carries the discriminator property's `const` at the option's top
|
|
288
|
+
* level. The const sibling is what makes `detectDiscriminated` classify
|
|
289
|
+
* the parent `oneOf` as a discriminated union — it inspects each
|
|
290
|
+
* option's literal `properties`, not the resolved schema.
|
|
291
|
+
*/
|
|
292
|
+
function buildDiscriminatorOption(subtype, propertyName) {
|
|
293
|
+
return {
|
|
294
|
+
$ref: `#/components/schemas/${subtype.name}`,
|
|
295
|
+
properties: { [propertyName]: { const: subtype.constValue } }
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Document-level pre-pass for OpenAPI discriminators that are declared
|
|
300
|
+
* on a base schema and inherited by subtypes via `allOf`.
|
|
301
|
+
*
|
|
302
|
+
* The per-node {@link normaliseOpenApi30Discriminator} only handles
|
|
303
|
+
* discriminators that already sit alongside `oneOf`/`anyOf`. For the
|
|
304
|
+
* canonical "Cat extends Pet" pattern — where `Pet` carries the
|
|
305
|
+
* discriminator and `Cat`/`Dog` reference `Pet` via `allOf` — the
|
|
306
|
+
* discriminator is silently lost. This pre-pass:
|
|
307
|
+
*
|
|
308
|
+
* 1. Injects the discriminator `const` on each subtype's local
|
|
309
|
+
* `properties` (so a direct render of the subtype validates the
|
|
310
|
+
* discriminator value correctly).
|
|
311
|
+
* 2. Synthesises a `oneOf` on the base whenever it lacks one, listing
|
|
312
|
+
* each subtype as `{ $ref, properties: { propertyName: { const } } }`.
|
|
313
|
+
* The per-node discriminator transform then sees `oneOf` and clears
|
|
314
|
+
* the `discriminator` keyword, and the walker's
|
|
315
|
+
* `detectDiscriminated` finds the per-option `const`s.
|
|
316
|
+
*
|
|
317
|
+
* Mutates a shallow clone of `components/schemas` — the input document
|
|
318
|
+
* is never modified.
|
|
319
|
+
*/
|
|
320
|
+
function applyDiscriminatorAllOfPrepass(doc) {
|
|
321
|
+
const components = doc.components;
|
|
322
|
+
if (!isObject(components)) return doc;
|
|
323
|
+
const schemas = components.schemas;
|
|
324
|
+
if (!isObject(schemas)) return doc;
|
|
325
|
+
const plans = [];
|
|
326
|
+
for (const [baseName, base] of Object.entries(schemas)) {
|
|
327
|
+
if (!isObject(base)) continue;
|
|
328
|
+
const discriminator = base.discriminator;
|
|
329
|
+
if (!isObject(discriminator)) continue;
|
|
330
|
+
const propertyName = discriminator.propertyName;
|
|
331
|
+
if (typeof propertyName !== "string") continue;
|
|
332
|
+
const subtypes = collectDiscriminatorSubtypes(baseName, discriminator, schemas);
|
|
333
|
+
if (subtypes.length === 0) continue;
|
|
334
|
+
plans.push({
|
|
335
|
+
baseName,
|
|
336
|
+
propertyName,
|
|
337
|
+
subtypes,
|
|
338
|
+
baseHasOneOfOrAnyOf: Array.isArray(base.oneOf) || Array.isArray(base.anyOf),
|
|
339
|
+
baseInherited: captureBaseInherited(base)
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (plans.length === 0) return doc;
|
|
343
|
+
const newSchemas = { ...schemas };
|
|
344
|
+
const cloneSchema = (name) => {
|
|
345
|
+
const existing = newSchemas[name];
|
|
346
|
+
if (!isObject(existing)) throw new Error(`applyDiscriminatorAllOfPrepass: schema "${name}" disappeared between planning and rewrite`);
|
|
347
|
+
const clone = { ...existing };
|
|
348
|
+
newSchemas[name] = clone;
|
|
349
|
+
return clone;
|
|
350
|
+
};
|
|
351
|
+
for (const plan of plans) {
|
|
352
|
+
for (const subtype of plan.subtypes) {
|
|
353
|
+
const clone = cloneSchema(subtype.name);
|
|
354
|
+
rewriteSubtypeAllOf(clone, plan.baseName, plan.baseInherited);
|
|
355
|
+
injectSubtypeConst(clone, plan.propertyName, subtype.constValue);
|
|
356
|
+
}
|
|
357
|
+
if (!plan.baseHasOneOfOrAnyOf) {
|
|
358
|
+
const baseClone = cloneSchema(plan.baseName);
|
|
359
|
+
baseClone.oneOf = plan.subtypes.map((subtype) => buildDiscriminatorOption(subtype, plan.propertyName));
|
|
360
|
+
delete baseClone.properties;
|
|
361
|
+
delete baseClone.required;
|
|
362
|
+
delete baseClone.type;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
...doc,
|
|
367
|
+
components: {
|
|
368
|
+
...components,
|
|
369
|
+
schemas: newSchemas
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
120
374
|
* Combined OpenAPI 3.0.x node transform: Draft 04 + nullable + discriminator.
|
|
121
375
|
* Applied to every schema node in an OpenAPI 3.0 document.
|
|
122
376
|
*
|
|
@@ -278,7 +532,7 @@ function normaliseContentMap(content, normaliseSchema) {
|
|
|
278
532
|
const encoding = mediaObj.encoding;
|
|
279
533
|
if (isObject(encoding)) normalised.encoding = mapObjectValues(encoding, (enc) => isObject(enc) ? normaliseEncoding(enc, normaliseSchema) : enc);
|
|
280
534
|
if ("example" in normalised && !("examples" in normalised)) {
|
|
281
|
-
normalised.examples = { value: normalised.example };
|
|
535
|
+
normalised.examples = { default: { value: normalised.example } };
|
|
282
536
|
delete normalised.example;
|
|
283
537
|
} else if ("example" in normalised) delete normalised.example;
|
|
284
538
|
result[mediaType] = normalised;
|
|
@@ -332,7 +586,7 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
332
586
|
result.servers = [{ url: `${typeof schemes[0] === "string" ? schemes[0] : "https"}://${host}${basePath}` }];
|
|
333
587
|
}
|
|
334
588
|
const paths = doc.paths;
|
|
335
|
-
if (isObject(paths)) result.paths = normaliseSwaggerPaths(paths, doc);
|
|
589
|
+
if (isObject(paths)) result.paths = normaliseSwaggerPaths(paths, doc, diagnostics);
|
|
336
590
|
const components = {};
|
|
337
591
|
const definitions = doc.definitions;
|
|
338
592
|
if (isObject(definitions)) {
|
|
@@ -353,7 +607,7 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
353
607
|
const resolved = resolveSwaggerParameter(param, doc);
|
|
354
608
|
const location = resolved.in;
|
|
355
609
|
if (location === "body") requestBodies[name] = buildRequestBody(resolved, globalConsumes);
|
|
356
|
-
else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]),
|
|
610
|
+
else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
|
|
357
611
|
else convertedParameters[name] = normaliseSwaggerParameter(resolved, doc);
|
|
358
612
|
}
|
|
359
613
|
if (Object.keys(convertedParameters).length > 0) components.parameters = convertedParameters;
|
|
@@ -381,7 +635,7 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
|
|
|
381
635
|
});
|
|
382
636
|
return result;
|
|
383
637
|
}
|
|
384
|
-
function normaliseSwaggerPaths(paths, doc) {
|
|
638
|
+
function normaliseSwaggerPaths(paths, doc, diagnostics) {
|
|
385
639
|
const result = {};
|
|
386
640
|
const METHODS = [
|
|
387
641
|
"get",
|
|
@@ -401,7 +655,7 @@ function normaliseSwaggerPaths(paths, doc) {
|
|
|
401
655
|
for (const method of METHODS) {
|
|
402
656
|
const operation = pathItem[method];
|
|
403
657
|
if (!isObject(operation)) continue;
|
|
404
|
-
normalisedPath[method] = normaliseSwaggerOperation(operation, doc);
|
|
658
|
+
normalisedPath[method] = normaliseSwaggerOperation(operation, doc, path, method, diagnostics);
|
|
405
659
|
}
|
|
406
660
|
const pathParams = pathItem.parameters;
|
|
407
661
|
if (Array.isArray(pathParams)) normalisedPath.parameters = pathParams.map((p) => isObject(p) ? normaliseSwaggerParameter(p, doc) : p);
|
|
@@ -409,7 +663,7 @@ function normaliseSwaggerPaths(paths, doc) {
|
|
|
409
663
|
}
|
|
410
664
|
return result;
|
|
411
665
|
}
|
|
412
|
-
function normaliseSwaggerOperation(operation, doc) {
|
|
666
|
+
function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
|
|
413
667
|
const result = {};
|
|
414
668
|
const globalProduces = Array.isArray(doc.produces) ? doc.produces : ["application/json"];
|
|
415
669
|
const globalConsumes = Array.isArray(doc.consumes) ? doc.consumes : ["application/json"];
|
|
@@ -420,28 +674,61 @@ function normaliseSwaggerOperation(operation, doc) {
|
|
|
420
674
|
if (Array.isArray(params)) {
|
|
421
675
|
const nonBodyParams = [];
|
|
422
676
|
let bodyParam;
|
|
677
|
+
let firstBodyName;
|
|
423
678
|
let usesFormData = false;
|
|
424
|
-
for (const param of params) {
|
|
679
|
+
for (const [index, param] of params.entries()) {
|
|
425
680
|
if (!isObject(param)) {
|
|
426
681
|
nonBodyParams.push(param);
|
|
427
682
|
continue;
|
|
428
683
|
}
|
|
429
684
|
const resolvedParam = resolveSwaggerParameter(param, doc);
|
|
430
685
|
const location = resolvedParam.in;
|
|
431
|
-
if (location === "body")
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
686
|
+
if (location === "body") {
|
|
687
|
+
if (bodyParam !== void 0) {
|
|
688
|
+
const duplicateName = typeof resolvedParam.name === "string" ? resolvedParam.name : `parameters[${String(index)}]`;
|
|
689
|
+
emitDiagnostic(diagnostics, {
|
|
690
|
+
code: "duplicate-body-parameter",
|
|
691
|
+
message: `Operation defines more than one "in: body" parameter; keeping the first ("${firstBodyName ?? "(unnamed)"}") and discarding "${duplicateName}"`,
|
|
692
|
+
pointer: appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"),
|
|
693
|
+
detail: {
|
|
694
|
+
kept: firstBodyName,
|
|
695
|
+
discarded: duplicateName,
|
|
696
|
+
location: "operation"
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
bodyParam = resolvedParam;
|
|
702
|
+
firstBodyName = typeof resolvedParam.name === "string" ? resolvedParam.name : void 0;
|
|
703
|
+
} else if (location === "formData") {
|
|
704
|
+
if (!usesFormData) {
|
|
705
|
+
bodyParam = buildFormDataBody(resolvedParam, params);
|
|
706
|
+
usesFormData = true;
|
|
707
|
+
}
|
|
435
708
|
} else nonBodyParams.push(normaliseSwaggerParameter(resolvedParam, doc));
|
|
436
709
|
}
|
|
437
710
|
if (nonBodyParams.length > 0) result.parameters = nonBodyParams;
|
|
438
|
-
if (bodyParam !== void 0) result.requestBody = buildRequestBody(bodyParam, usesFormData ?
|
|
711
|
+
if (bodyParam !== void 0) result.requestBody = buildRequestBody(bodyParam, usesFormData ? formDataContentTypes(consumes) : consumes);
|
|
439
712
|
}
|
|
440
713
|
const responses = operation.responses;
|
|
441
714
|
if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces);
|
|
442
715
|
return result;
|
|
443
716
|
}
|
|
444
717
|
/**
|
|
718
|
+
* Determine the request body media type for a Swagger 2.0 formData operation.
|
|
719
|
+
*
|
|
720
|
+
* Per the OAS 3 conversion rules, `application/x-www-form-urlencoded` is
|
|
721
|
+
* preferred when the operation- or document-level `consumes` includes it;
|
|
722
|
+
* otherwise `multipart/form-data` is the default. File uploads (Swagger 2.0
|
|
723
|
+
* `type: file`) still require `multipart/form-data`, but the formData body
|
|
724
|
+
* schema-builder normalises them to `string` + `format: binary` either way
|
|
725
|
+
* and the choice of media type is left to the source document.
|
|
726
|
+
*/
|
|
727
|
+
function formDataContentTypes(consumes) {
|
|
728
|
+
if (consumes.includes("application/x-www-form-urlencoded")) return ["application/x-www-form-urlencoded"];
|
|
729
|
+
return ["multipart/form-data"];
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
445
732
|
* Resolve a Swagger parameter that may be a `$ref`.
|
|
446
733
|
*/
|
|
447
734
|
function resolveSwaggerParameter(param, doc, visited = /* @__PURE__ */ new Set()) {
|
|
@@ -596,7 +883,7 @@ function normaliseSwaggerResponses(responses, doc, produces) {
|
|
|
596
883
|
function normaliseSwaggerSingleResponse(response, doc, produces) {
|
|
597
884
|
const resolved = resolveSwaggerResponse(response, doc);
|
|
598
885
|
const normalised = {};
|
|
599
|
-
for (const [key, value] of Object.entries(resolved)) if (key !== "schema") normalised[key] = value;
|
|
886
|
+
for (const [key, value] of Object.entries(resolved)) if (key !== "schema" && key !== "headers") normalised[key] = value;
|
|
600
887
|
const schema = resolved.schema;
|
|
601
888
|
if (isObject(schema)) {
|
|
602
889
|
const content = {};
|
|
@@ -604,9 +891,64 @@ function normaliseSwaggerSingleResponse(response, doc, produces) {
|
|
|
604
891
|
for (const ct of contentTypes) if (typeof ct === "string") content[ct] = { schema };
|
|
605
892
|
normalised.content = content;
|
|
606
893
|
}
|
|
894
|
+
const headers = resolved.headers;
|
|
895
|
+
if (isObject(headers)) {
|
|
896
|
+
const convertedHeaders = {};
|
|
897
|
+
for (const [name, header] of Object.entries(headers)) convertedHeaders[name] = isObject(header) ? normaliseSwaggerHeader(header) : header;
|
|
898
|
+
normalised.headers = convertedHeaders;
|
|
899
|
+
}
|
|
607
900
|
return normalised;
|
|
608
901
|
}
|
|
609
902
|
/**
|
|
903
|
+
* Normalise a single Swagger 2.0 response header to OpenAPI 3.x form.
|
|
904
|
+
*
|
|
905
|
+
* Swagger 2.0 headers mirror parameter shape: `type`/`format`/
|
|
906
|
+
* `collectionFormat` live at the root. OpenAPI 3.x requires the type
|
|
907
|
+
* descriptor under `schema`, with collection serialisation expressed via
|
|
908
|
+
* `style`/`explode`. Headers do not carry `name` or `in` — those are not
|
|
909
|
+
* part of either spec at this level — so this is a thin sibling to
|
|
910
|
+
* `normaliseSwaggerParameter` rather than a full reuse. The OpenAPI 3.x
|
|
911
|
+
* default header style is `simple`, so CSV-encoded headers map to
|
|
912
|
+
* `simple`/`explode: false` rather than the `form` style used for query
|
|
913
|
+
* parameters.
|
|
914
|
+
*/
|
|
915
|
+
function normaliseSwaggerHeader(header) {
|
|
916
|
+
const result = {};
|
|
917
|
+
for (const [key, value] of Object.entries(header)) {
|
|
918
|
+
if (key === "type" || key === "format" || key === "collectionFormat") continue;
|
|
919
|
+
result[key] = value;
|
|
920
|
+
}
|
|
921
|
+
if (typeof header.type === "string") {
|
|
922
|
+
const schema = { type: header.type };
|
|
923
|
+
if (typeof header.format === "string") schema.format = header.format;
|
|
924
|
+
if (header.enum !== void 0) schema.enum = header.enum;
|
|
925
|
+
if (header.default !== void 0) schema.default = header.default;
|
|
926
|
+
if (header.minimum !== void 0) schema.minimum = header.minimum;
|
|
927
|
+
if (header.maximum !== void 0) schema.maximum = header.maximum;
|
|
928
|
+
result.schema = schema;
|
|
929
|
+
}
|
|
930
|
+
const cf = header.collectionFormat;
|
|
931
|
+
if (typeof cf === "string") switch (cf) {
|
|
932
|
+
case "csv":
|
|
933
|
+
result.style = "simple";
|
|
934
|
+
result.explode = false;
|
|
935
|
+
break;
|
|
936
|
+
case "ssv":
|
|
937
|
+
result.style = "spaceDelimited";
|
|
938
|
+
result.explode = false;
|
|
939
|
+
break;
|
|
940
|
+
case "tsv":
|
|
941
|
+
result.style = "tabDelimited";
|
|
942
|
+
result.explode = false;
|
|
943
|
+
break;
|
|
944
|
+
case "pipes":
|
|
945
|
+
result.style = "pipeDelimited";
|
|
946
|
+
result.explode = false;
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
return result;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
610
952
|
* Mapping of Swagger 2.0 $ref prefixes to OpenAPI 3.x equivalents.
|
|
611
953
|
* Applied after document restructuring so all $ref strings point
|
|
612
954
|
* to the correct locations in the normalised document.
|
|
@@ -691,35 +1033,43 @@ const SINGLE_SUBSCHEMA_KEYS = new Set([
|
|
|
691
1033
|
* Normalise each element of an unknown array by applying deepNormalise
|
|
692
1034
|
* to object elements and passing others through unchanged.
|
|
693
1035
|
*/
|
|
694
|
-
function normaliseArray(items, transform) {
|
|
1036
|
+
function normaliseArray(items, transform, visited) {
|
|
695
1037
|
const result = [];
|
|
696
|
-
for (const item of items) result.push(isObject(item) ? deepNormalise(item, transform) : item);
|
|
1038
|
+
for (const item of items) result.push(isObject(item) ? deepNormalise(item, transform, visited) : item);
|
|
697
1039
|
return result;
|
|
698
1040
|
}
|
|
699
1041
|
/**
|
|
700
1042
|
* Normalise each value of a sub-schema map (e.g. properties, $defs).
|
|
701
1043
|
*/
|
|
702
|
-
function normaliseSubSchemaMap(map, transform) {
|
|
1044
|
+
function normaliseSubSchemaMap(map, transform, visited) {
|
|
703
1045
|
const result = {};
|
|
704
|
-
for (const [k, v] of Object.entries(map)) result[k] = isObject(v) ? deepNormalise(v, transform) : v;
|
|
1046
|
+
for (const [k, v] of Object.entries(map)) result[k] = isObject(v) ? deepNormalise(v, transform, visited) : v;
|
|
705
1047
|
return result;
|
|
706
1048
|
}
|
|
707
1049
|
/**
|
|
708
1050
|
* Deep-normalise a JSON Schema object by applying a per-node transform
|
|
709
1051
|
* and recursing into every sub-schema location.
|
|
1052
|
+
*
|
|
1053
|
+
* The optional `visited` set guards against shared object references and
|
|
1054
|
+
* cycles introduced upstream (e.g. by the OpenAPI bundler's
|
|
1055
|
+
* `structuredClone`-based inlining of external refs). The walk skips
|
|
1056
|
+
* already-seen nodes by returning the original reference rather than
|
|
1057
|
+
* recursing forever.
|
|
710
1058
|
*/
|
|
711
|
-
function deepNormalise(schema, transform) {
|
|
1059
|
+
function deepNormalise(schema, transform, visited = /* @__PURE__ */ new WeakSet()) {
|
|
1060
|
+
if (visited.has(schema)) return schema;
|
|
1061
|
+
visited.add(schema);
|
|
712
1062
|
const node = transform({ ...schema });
|
|
713
1063
|
const result = {};
|
|
714
|
-
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMap(value, transform);
|
|
715
|
-
else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArray(value, transform);
|
|
716
|
-
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormalise(value, transform);
|
|
717
|
-
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArray(value, transform);
|
|
718
|
-
else if (isObject(value)) result[key] = deepNormalise(value, transform);
|
|
1064
|
+
for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMap(value, transform, visited);
|
|
1065
|
+
else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArray(value, transform, visited);
|
|
1066
|
+
else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormalise(value, transform, visited);
|
|
1067
|
+
else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArray(value, transform, visited);
|
|
1068
|
+
else if (isObject(value)) result[key] = deepNormalise(value, transform, visited);
|
|
719
1069
|
else result[key] = value;
|
|
720
1070
|
else if (key === "dependencies" && isObject(value)) {
|
|
721
1071
|
const normalised = {};
|
|
722
|
-
for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormalise(dv, transform);
|
|
1072
|
+
for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormalise(dv, transform, visited);
|
|
723
1073
|
else normalised[dk] = dv;
|
|
724
1074
|
result[key] = normalised;
|
|
725
1075
|
} else result[key] = value;
|
|
@@ -888,6 +1238,26 @@ function validateDependentRequired(node, ctx) {
|
|
|
888
1238
|
}
|
|
889
1239
|
}
|
|
890
1240
|
/**
|
|
1241
|
+
* Translate the legacy tuple-form `items: [Schema, Schema]` keyword to
|
|
1242
|
+
* Draft 2020-12's `prefixItems`. `additionalItems` becomes the rest-element
|
|
1243
|
+
* schema and is rewritten to `items`.
|
|
1244
|
+
*
|
|
1245
|
+
* Applies to Drafts 04, 06, and 07 — all of which used the tuple form
|
|
1246
|
+
* before Draft 2020-12 split positional schemas into `prefixItems`.
|
|
1247
|
+
*
|
|
1248
|
+
* No-op when `items` is already a single sub-schema, or when `prefixItems`
|
|
1249
|
+
* is already present (do not overwrite an explicit author choice).
|
|
1250
|
+
*/
|
|
1251
|
+
function translateTupleItems(node) {
|
|
1252
|
+
if (!Array.isArray(node.items) || "prefixItems" in node) return;
|
|
1253
|
+
node.prefixItems = node.items;
|
|
1254
|
+
delete node.items;
|
|
1255
|
+
if ("additionalItems" in node) {
|
|
1256
|
+
node.items = node.additionalItems;
|
|
1257
|
+
delete node.additionalItems;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
891
1261
|
* Apply the version-agnostic Draft 04 keyword translations to a single
|
|
892
1262
|
* node: boolean exclusive-min/max → number form, bare `id` → `$id`, and
|
|
893
1263
|
* tuple-form `items` → `prefixItems`.
|
|
@@ -906,10 +1276,28 @@ function applyDraft04Translations(node, ctx) {
|
|
|
906
1276
|
node.exclusiveMinimum = node.minimum;
|
|
907
1277
|
delete node.minimum;
|
|
908
1278
|
} else if (node.exclusiveMinimum === false) delete node.exclusiveMinimum;
|
|
1279
|
+
else if (node.exclusiveMinimum === true) {
|
|
1280
|
+
if (ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
|
|
1281
|
+
code: "bare-exclusive-bound",
|
|
1282
|
+
message: "`exclusiveMinimum: true` requires a sibling numeric `minimum` in Draft 04; dropping the keyword",
|
|
1283
|
+
pointer: appendPointer(ctx.pointer, "exclusiveMinimum"),
|
|
1284
|
+
detail: { keyword: "exclusiveMinimum" }
|
|
1285
|
+
});
|
|
1286
|
+
delete node.exclusiveMinimum;
|
|
1287
|
+
}
|
|
909
1288
|
if (node.exclusiveMaximum === true && typeof node.maximum === "number") {
|
|
910
1289
|
node.exclusiveMaximum = node.maximum;
|
|
911
1290
|
delete node.maximum;
|
|
912
1291
|
} else if (node.exclusiveMaximum === false) delete node.exclusiveMaximum;
|
|
1292
|
+
else if (node.exclusiveMaximum === true) {
|
|
1293
|
+
if (ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
|
|
1294
|
+
code: "bare-exclusive-bound",
|
|
1295
|
+
message: "`exclusiveMaximum: true` requires a sibling numeric `maximum` in Draft 04; dropping the keyword",
|
|
1296
|
+
pointer: appendPointer(ctx.pointer, "exclusiveMaximum"),
|
|
1297
|
+
detail: { keyword: "exclusiveMaximum" }
|
|
1298
|
+
});
|
|
1299
|
+
delete node.exclusiveMaximum;
|
|
1300
|
+
}
|
|
913
1301
|
const divisibleBy = node.divisibleBy;
|
|
914
1302
|
if (typeof divisibleBy === "number") {
|
|
915
1303
|
const multipleOf = node.multipleOf;
|
|
@@ -930,14 +1318,7 @@ function applyDraft04Translations(node, ctx) {
|
|
|
930
1318
|
node.$id = node.id;
|
|
931
1319
|
delete node.id;
|
|
932
1320
|
}
|
|
933
|
-
|
|
934
|
-
node.prefixItems = node.items;
|
|
935
|
-
delete node.items;
|
|
936
|
-
if ("additionalItems" in node) {
|
|
937
|
-
node.items = node.additionalItems;
|
|
938
|
-
delete node.additionalItems;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
1321
|
+
translateTupleItems(node);
|
|
941
1322
|
splitDependencies(node, ctx, false);
|
|
942
1323
|
validateDependentRequired(node, ctx);
|
|
943
1324
|
}
|
|
@@ -983,8 +1364,14 @@ function normaliseDraft04NodeWithContext(node, ctx) {
|
|
|
983
1364
|
* (already in final form) and `const`/`examples`, but still use the
|
|
984
1365
|
* legacy `dependencies` keyword. Split it into `dependentRequired` /
|
|
985
1366
|
* `dependentSchemas` so the walker can process them uniformly.
|
|
1367
|
+
*
|
|
1368
|
+
* Drafts 06 and 07 also use the tuple form
|
|
1369
|
+
* `items: [Schema, Schema], additionalItems: Schema` — translate that to
|
|
1370
|
+
* Draft 2020-12's `prefixItems` + rest-`items` so the walker produces a
|
|
1371
|
+
* `TupleField` rather than silently dropping the positional schemas.
|
|
986
1372
|
*/
|
|
987
1373
|
function normaliseDraft06Or07NodeWithContext(node, ctx) {
|
|
1374
|
+
translateTupleItems(node);
|
|
988
1375
|
splitDependencies(node, ctx, false);
|
|
989
1376
|
validateDependentRequired(node, ctx);
|
|
990
1377
|
return node;
|
|
@@ -1045,15 +1432,153 @@ function normaliseJsonSchema(schema, draft, diagnostics) {
|
|
|
1045
1432
|
diagnostics,
|
|
1046
1433
|
pointer: ""
|
|
1047
1434
|
};
|
|
1435
|
+
let normalised;
|
|
1048
1436
|
switch (draft) {
|
|
1049
|
-
case "draft-04":
|
|
1050
|
-
|
|
1051
|
-
|
|
1437
|
+
case "draft-04":
|
|
1438
|
+
normalised = deepNormaliseWithContext(schema, normaliseDraft04NodeWithContext, ctx);
|
|
1439
|
+
break;
|
|
1440
|
+
case "draft-2019-09":
|
|
1441
|
+
normalised = deepNormaliseWithContext(schema, normaliseDraft201909NodeWithContext, ctx);
|
|
1442
|
+
break;
|
|
1443
|
+
case "draft-2020-12":
|
|
1444
|
+
normalised = deepNormaliseWithContext(schema, normaliseDynamicRefNodeWithContext, ctx);
|
|
1445
|
+
break;
|
|
1052
1446
|
case "draft-06":
|
|
1053
|
-
case "draft-07":
|
|
1447
|
+
case "draft-07":
|
|
1448
|
+
normalised = deepNormaliseWithContext(schema, normaliseDraft06Or07NodeWithContext, ctx);
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
return resolveRelativeRefs(normalised, diagnostics);
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Parse a string as an absolute URI, returning `undefined` when it has
|
|
1455
|
+
* no scheme. Used to detect whether an `$id` value defines a base URI.
|
|
1456
|
+
*/
|
|
1457
|
+
function parseAbsoluteUri(value) {
|
|
1458
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
1459
|
+
try {
|
|
1460
|
+
const url = new URL(value);
|
|
1461
|
+
if (url.protocol.length === 0) return void 0;
|
|
1462
|
+
return url;
|
|
1463
|
+
} catch {
|
|
1464
|
+
return;
|
|
1054
1465
|
}
|
|
1055
1466
|
}
|
|
1056
1467
|
/**
|
|
1468
|
+
* Resolve a relative reference against a base URI. Returns `undefined`
|
|
1469
|
+
* when the reference cannot be resolved (e.g. malformed input).
|
|
1470
|
+
*/
|
|
1471
|
+
function resolveAgainst(ref, base) {
|
|
1472
|
+
try {
|
|
1473
|
+
return new URL(ref, base);
|
|
1474
|
+
} catch {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Strip the fragment portion from a URL, returning the canonical
|
|
1480
|
+
* `scheme://authority/path?query` form. Used to compare a resolved
|
|
1481
|
+
* `$ref` URI against the document's `$id` base.
|
|
1482
|
+
*/
|
|
1483
|
+
function stripFragment(url) {
|
|
1484
|
+
const clone = new URL(url.toString());
|
|
1485
|
+
clone.hash = "";
|
|
1486
|
+
return clone.toString();
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Recursively rewrite relative `$ref`s in a schema so they resolve
|
|
1490
|
+
* correctly under the JSON Schema base-URI rules (RFC 3986 + JSON
|
|
1491
|
+
* Schema §8.2). Refs that resolve to the document's own `$id` are
|
|
1492
|
+
* rewritten to fragment-only form so the existing dereferencer can
|
|
1493
|
+
* handle them; refs that resolve outside the document are left as
|
|
1494
|
+
* absolute URIs (handled by the external resolver path).
|
|
1495
|
+
*
|
|
1496
|
+
* Returns the input unchanged when the document has no base URI or
|
|
1497
|
+
* no relative refs.
|
|
1498
|
+
*/
|
|
1499
|
+
function resolveRelativeRefs(schema, diagnostics) {
|
|
1500
|
+
const docBaseUrl = parseAbsoluteUri(schema.$id);
|
|
1501
|
+
if (docBaseUrl === void 0) return schema;
|
|
1502
|
+
const docBase = stripFragment(docBaseUrl);
|
|
1503
|
+
return rewriteRelativeRefsNode(schema, docBase, docBase, "", diagnostics, /* @__PURE__ */ new WeakSet());
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Rewrite relative `$ref`s in a single node, recursing into sub-schemas.
|
|
1507
|
+
*
|
|
1508
|
+
* The `visited` set guards against shared object references and cycles
|
|
1509
|
+
* introduced upstream (e.g. by the OpenAPI bundler's `structuredClone`
|
|
1510
|
+
* inlining of external refs). When a node is re-encountered the rewrite
|
|
1511
|
+
* is short-circuited — returning the original reference unchanged is
|
|
1512
|
+
* sound because every relative `$ref` in the subtree was already
|
|
1513
|
+
* rewritten on the first visit.
|
|
1514
|
+
*/
|
|
1515
|
+
function rewriteRelativeRefsNode(node, currentBase, docBase, pointer, diagnostics, visited) {
|
|
1516
|
+
if (visited.has(node)) return node;
|
|
1517
|
+
visited.add(node);
|
|
1518
|
+
let nextBase = currentBase;
|
|
1519
|
+
const nodeId = node.$id;
|
|
1520
|
+
if (typeof nodeId === "string" && nodeId.length > 0) {
|
|
1521
|
+
const resolved = resolveAgainst(nodeId, currentBase);
|
|
1522
|
+
if (resolved !== void 0) nextBase = stripFragment(resolved);
|
|
1523
|
+
}
|
|
1524
|
+
const result = {};
|
|
1525
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1526
|
+
if (key === "$ref" && typeof value === "string") {
|
|
1527
|
+
result[key] = rewriteRef(value, nextBase, docBase, appendPointer(pointer, key), diagnostics);
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
result[key] = rewriteRelativeRefsValue(value, nextBase, docBase, appendPointer(pointer, key), diagnostics, visited);
|
|
1531
|
+
}
|
|
1532
|
+
return result;
|
|
1533
|
+
}
|
|
1534
|
+
function rewriteRelativeRefsValue(value, currentBase, docBase, pointer, diagnostics, visited) {
|
|
1535
|
+
if (Array.isArray(value)) {
|
|
1536
|
+
if (visited.has(value)) return value;
|
|
1537
|
+
visited.add(value);
|
|
1538
|
+
return value.map((item, i) => rewriteRelativeRefsValue(item, currentBase, docBase, appendPointer(pointer, String(i)), diagnostics, visited));
|
|
1539
|
+
}
|
|
1540
|
+
if (isObject(value)) return rewriteRelativeRefsNode(value, currentBase, docBase, pointer, diagnostics, visited);
|
|
1541
|
+
return value;
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Rewrite a single `$ref` string. Fragment-only refs and refs that
|
|
1545
|
+
* already include a scheme are returned unchanged. Relative refs are
|
|
1546
|
+
* resolved against `currentBase`; if the result lives in the same
|
|
1547
|
+
* document as `docBase`, the ref is rewritten to fragment form.
|
|
1548
|
+
*/
|
|
1549
|
+
function rewriteRef(ref, currentBase, docBase, pointer, diagnostics) {
|
|
1550
|
+
if (ref.startsWith("#")) return ref;
|
|
1551
|
+
if (/^[a-z][a-z0-9+\-.]*:/i.test(ref)) return ref;
|
|
1552
|
+
const resolved = resolveAgainst(ref, currentBase);
|
|
1553
|
+
if (resolved === void 0) return ref;
|
|
1554
|
+
if (stripFragment(resolved) === docBase) {
|
|
1555
|
+
const fragment = resolved.hash === "" ? "#" : resolved.hash;
|
|
1556
|
+
emitDiagnostic(diagnostics, {
|
|
1557
|
+
code: "relative-ref-resolved",
|
|
1558
|
+
message: `Relative $ref "${ref}" resolved to "${fragment}" against base "${currentBase}"`,
|
|
1559
|
+
pointer,
|
|
1560
|
+
detail: {
|
|
1561
|
+
ref,
|
|
1562
|
+
base: currentBase,
|
|
1563
|
+
resolved: fragment
|
|
1564
|
+
}
|
|
1565
|
+
});
|
|
1566
|
+
return fragment;
|
|
1567
|
+
}
|
|
1568
|
+
const absolute = resolved.toString();
|
|
1569
|
+
emitDiagnostic(diagnostics, {
|
|
1570
|
+
code: "relative-ref-resolved",
|
|
1571
|
+
message: `Relative $ref "${ref}" resolved to "${absolute}" against base "${currentBase}"`,
|
|
1572
|
+
pointer,
|
|
1573
|
+
detail: {
|
|
1574
|
+
ref,
|
|
1575
|
+
base: currentBase,
|
|
1576
|
+
resolved: absolute
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
return absolute;
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1057
1582
|
* Normalise an OpenAPI document's schemas for walker consumption.
|
|
1058
1583
|
* Handles version-specific keyword transformations.
|
|
1059
1584
|
*
|
|
@@ -1062,8 +1587,17 @@ function normaliseJsonSchema(schema, draft, diagnostics) {
|
|
|
1062
1587
|
*/
|
|
1063
1588
|
function normaliseOpenApiSchemas(doc, version, diagnostics) {
|
|
1064
1589
|
if (isSwagger2(version)) return normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, diagnostics);
|
|
1065
|
-
if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(doc, deepNormalise);
|
|
1066
|
-
|
|
1590
|
+
if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(applyDiscriminatorAllOfPrepass(doc), deepNormalise);
|
|
1591
|
+
if (isOpenApi31(version)) {
|
|
1592
|
+
const dialect = readJsonSchemaDialect(doc);
|
|
1593
|
+
if (dialect.kind === "unknown") emitDiagnostic(diagnostics, {
|
|
1594
|
+
code: "unknown-json-schema-dialect",
|
|
1595
|
+
message: `OpenAPI 3.1 \`jsonSchemaDialect\` URI "${dialect.uri}" does not match a supported JSON Schema draft; falling back to Draft 2020-12`,
|
|
1596
|
+
pointer: "/jsonSchemaDialect",
|
|
1597
|
+
detail: { uri: dialect.uri }
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
return deepNormaliseOpenApiDoc(applyDiscriminatorAllOfPrepass(doc), (schema) => resolveRelativeRefs(deepNormalise(schema, normaliseOpenApi30Discriminator), diagnostics));
|
|
1067
1601
|
}
|
|
1068
1602
|
//#endregion
|
|
1069
|
-
export { normaliseOpenApiSchemas as a,
|
|
1603
|
+
export { normaliseOpenApiSchemas as a, deepNormaliseOpenApi30Doc as c, normaliseOpenApi30Discriminator as d, normaliseOpenApi30Node as f, normaliseJsonSchema as i, deepNormaliseOpenApiDoc as l, deepNormaliseWithContext as n, normaliseSwagger2Document as o, normaliseDraft04Node as r, applyDiscriminatorAllOfPrepass as s, deepNormalise as t, normaliseOpenApi30Combined as u };
|