@kubb/adapter-oas 5.15.4 → 5.16.1
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/index.cjs +333 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -8
- package/dist/index.d.ts +20 -8
- package/dist/index.js +334 -36
- package/dist/index.js.map +1 -1
- package/docs/options.md +1 -1
- package/kubb-extension.json +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineExtension } from "@kubb/core";
|
|
2
|
-
import { Diagnostics, ast, childName, enumPrimitive, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName,
|
|
2
|
+
import { Diagnostics, ast, childName, enumPrimitive, enumPropName, extractRefName, macroDedupeUnion, macroDiscriminatorEnum, macroEnumName, mergeAdjacentObjectsLazy } from "@kubb/kit";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { access, readFile } from "node:fs/promises";
|
|
5
5
|
import { upgrade } from "@scalar/openapi-upgrader";
|
|
@@ -19,7 +19,7 @@ var compatibility = {
|
|
|
19
19
|
"node": ">=22.22"
|
|
20
20
|
};
|
|
21
21
|
// package.json
|
|
22
|
-
var version = "5.
|
|
22
|
+
var version = "5.16.1";
|
|
23
23
|
// src/constants.ts
|
|
24
24
|
/**
|
|
25
25
|
* Default parser options applied when no explicit options are provided.
|
|
@@ -59,6 +59,17 @@ const SUPPORTED_METHODS = /* @__PURE__ */ new Set([
|
|
|
59
59
|
"query"
|
|
60
60
|
]);
|
|
61
61
|
/**
|
|
62
|
+
* Media types OpenAPI 3.2 reads as a stream of items (its "sequential media types"), each described by `itemSchema`.
|
|
63
|
+
*/
|
|
64
|
+
const STREAM_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
65
|
+
"application/jsonl",
|
|
66
|
+
"application/x-ndjson",
|
|
67
|
+
"application/json-seq",
|
|
68
|
+
"application/geo+json-seq",
|
|
69
|
+
"text/event-stream",
|
|
70
|
+
"multipart/mixed"
|
|
71
|
+
]);
|
|
72
|
+
/**
|
|
62
73
|
* Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
|
|
63
74
|
*
|
|
64
75
|
* A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
|
|
@@ -383,6 +394,14 @@ function isNullable(schema) {
|
|
|
383
394
|
return false;
|
|
384
395
|
}
|
|
385
396
|
/**
|
|
397
|
+
* Returns `true` for an OpenAPI 3.2 or later document. Swagger 2.0 and OpenAPI 3.0 documents are
|
|
398
|
+
* upgraded to 3.1 before parsing, so `openapi` is always set here.
|
|
399
|
+
*/
|
|
400
|
+
function isOpenApi32(document) {
|
|
401
|
+
const [major = 0, minor = 0] = String(document.openapi).split(".").map(Number);
|
|
402
|
+
return major > 3 || major === 3 && minor >= 2;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
386
405
|
* Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
|
|
387
406
|
*/
|
|
388
407
|
function pickExtensions(source) {
|
|
@@ -449,7 +468,8 @@ const jsonMimeFragments = [
|
|
|
449
468
|
* ```
|
|
450
469
|
*/
|
|
451
470
|
function isJsonMimeType(mimeType) {
|
|
452
|
-
|
|
471
|
+
const lower = mimeType.toLowerCase();
|
|
472
|
+
return jsonMimeFragments.some((fragment) => lower.includes(fragment));
|
|
453
473
|
}
|
|
454
474
|
// src/refs.ts
|
|
455
475
|
/**
|
|
@@ -873,6 +893,17 @@ function reportInvalidSpec(message) {
|
|
|
873
893
|
* ```
|
|
874
894
|
*/
|
|
875
895
|
async function validateDocument(document, { throwOnError = false } = {}) {
|
|
896
|
+
if (isOpenApi32(document)) {
|
|
897
|
+
const diagnostic = {
|
|
898
|
+
code: Diagnostics.code.invalidSpec,
|
|
899
|
+
severity: "warning",
|
|
900
|
+
message: "The document was not validated: `@readme/openapi-parser` does not support OpenAPI 3.2 yet.",
|
|
901
|
+
help: "Set `validate: false` on the adapter to skip this check until the validator supports OpenAPI 3.2."
|
|
902
|
+
};
|
|
903
|
+
if (throwOnError) throw new Diagnostics.Error(diagnostic);
|
|
904
|
+
Diagnostics.report(diagnostic);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
876
907
|
const { compileErrors, validate } = await import("@readme/openapi-parser");
|
|
877
908
|
let result;
|
|
878
909
|
try {
|
|
@@ -902,6 +933,7 @@ function reportValidationErrors({ errors, additionalErrors }) {
|
|
|
902
933
|
* validating in place where worker threads are missing, as in edge runtimes.
|
|
903
934
|
*/
|
|
904
935
|
async function validateDocumentInWorker(document) {
|
|
936
|
+
if (isOpenApi32(document)) return validateDocument(document);
|
|
905
937
|
let message;
|
|
906
938
|
try {
|
|
907
939
|
const { Worker } = await import("node:worker_threads");
|
|
@@ -1172,6 +1204,123 @@ function resolveServerUrl(server, overrides) {
|
|
|
1172
1204
|
}
|
|
1173
1205
|
return url;
|
|
1174
1206
|
}
|
|
1207
|
+
// src/model/mediaType.ts
|
|
1208
|
+
/**
|
|
1209
|
+
* The media type without its parameters, so `text/event-stream; charset=utf-8` matches the bare name.
|
|
1210
|
+
*/
|
|
1211
|
+
function baseMediaType(contentType) {
|
|
1212
|
+
return contentType.split(";")[0].trim().toLowerCase();
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Returns `true` for a media type read as a stream of items: one that OpenAPI 3.2 lists, or any that declares `itemSchema`.
|
|
1216
|
+
*/
|
|
1217
|
+
function isStream(contentType, mediaType) {
|
|
1218
|
+
return STREAM_MEDIA_TYPES.has(baseMediaType(contentType)) || mediaType?.itemSchema !== void 0;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Whether the media type is `text/event-stream`, with or without parameters.
|
|
1222
|
+
*/
|
|
1223
|
+
function isEventStream(contentType) {
|
|
1224
|
+
return baseMediaType(contentType) === "text/event-stream";
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Replaces a `data` string that holds JSON with its `contentSchema`, so the event reads as parsed.
|
|
1228
|
+
*/
|
|
1229
|
+
function withParsedData(schema, refs) {
|
|
1230
|
+
const data = schema.properties?.data;
|
|
1231
|
+
const resolved = data && isReference(data) ? refs.resolve(data.$ref, { report: false }) : data;
|
|
1232
|
+
if (!resolved?.contentSchema || !resolved.contentMediaType || !isJsonMimeType(resolved.contentMediaType)) return schema;
|
|
1233
|
+
return {
|
|
1234
|
+
...schema,
|
|
1235
|
+
properties: {
|
|
1236
|
+
...schema.properties,
|
|
1237
|
+
data: resolved.contentSchema
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Keywords next to `$ref` that only annotate it, so a reference that carries nothing else stays a reference.
|
|
1243
|
+
*/
|
|
1244
|
+
const REF_ANNOTATIONS = /* @__PURE__ */ new Set([
|
|
1245
|
+
"$ref",
|
|
1246
|
+
"summary",
|
|
1247
|
+
"description",
|
|
1248
|
+
"$comment"
|
|
1249
|
+
]);
|
|
1250
|
+
/**
|
|
1251
|
+
* Applies `extra` on top of `base` the way JSON Schema does: `properties` and `required` add up, other keywords of `extra` win.
|
|
1252
|
+
*/
|
|
1253
|
+
function mergeSchemas(base, extra) {
|
|
1254
|
+
const required = [.../* @__PURE__ */ new Set([...Array.isArray(base.required) ? base.required : [], ...Array.isArray(extra.required) ? extra.required : []])];
|
|
1255
|
+
return {
|
|
1256
|
+
type: "object",
|
|
1257
|
+
...base,
|
|
1258
|
+
...extra,
|
|
1259
|
+
properties: {
|
|
1260
|
+
...base.properties,
|
|
1261
|
+
...extra.properties
|
|
1262
|
+
},
|
|
1263
|
+
...required.length ? { required } : {}
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Normalizes the OpenAPI 3.2 `text/event-stream` item schema into one schema per event.
|
|
1268
|
+
* - Keywords next to a `$ref` apply to the referenced schema.
|
|
1269
|
+
* - Shared `properties` merge into each inline `oneOf`/`anyOf` member.
|
|
1270
|
+
* - A JSON `data` string becomes its `contentSchema`.
|
|
1271
|
+
*/
|
|
1272
|
+
function toEventSchema(schema, refs) {
|
|
1273
|
+
if (isReference(schema)) {
|
|
1274
|
+
const siblings = Object.fromEntries(Object.entries(schema).filter(([key]) => !REF_ANNOTATIONS.has(key)));
|
|
1275
|
+
const resolved = Object.keys(siblings).length ? refs.resolve(schema.$ref, { report: false }) : null;
|
|
1276
|
+
return resolved ? toEventSchema(mergeSchemas(resolved, siblings), refs) : schema;
|
|
1277
|
+
}
|
|
1278
|
+
const key = schema.oneOf?.length ? "oneOf" : schema.anyOf?.length ? "anyOf" : void 0;
|
|
1279
|
+
if (!key) return withParsedData(schema, refs);
|
|
1280
|
+
const members = schema[key];
|
|
1281
|
+
const { properties, required, ...rest } = schema;
|
|
1282
|
+
const merge = !!properties && !members.some(isReference);
|
|
1283
|
+
const shared = {
|
|
1284
|
+
properties,
|
|
1285
|
+
required
|
|
1286
|
+
};
|
|
1287
|
+
return {
|
|
1288
|
+
...merge ? rest : schema,
|
|
1289
|
+
[key]: members.map((member) => isReference(member) ? member : withParsedData(merge ? mergeSchemas(shared, member) : member, refs))
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Whether the schema, following `$ref`s, or a composition member of it declares an `event` or `data` property.
|
|
1294
|
+
*/
|
|
1295
|
+
function declaresEventFields(schema, refs, seen) {
|
|
1296
|
+
if (isReference(schema)) {
|
|
1297
|
+
if (seen.has(schema.$ref)) return false;
|
|
1298
|
+
seen.add(schema.$ref);
|
|
1299
|
+
const resolved = refs.resolve(schema.$ref, { report: false });
|
|
1300
|
+
return !!resolved && declaresEventFields(resolved, refs, seen);
|
|
1301
|
+
}
|
|
1302
|
+
if (schema.properties && (Object.hasOwn(schema.properties, "event") || Object.hasOwn(schema.properties, "data"))) return true;
|
|
1303
|
+
return [
|
|
1304
|
+
...schema.oneOf ?? [],
|
|
1305
|
+
...schema.anyOf ?? [],
|
|
1306
|
+
...schema.allOf ?? []
|
|
1307
|
+
].some((member) => declaresEventFields(member, refs, seen));
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Returns `'event'` when the item declares `event` or `data`, directly or in a composition member, else `'data'`.
|
|
1311
|
+
*/
|
|
1312
|
+
function getItemShape(schema, refs) {
|
|
1313
|
+
return declaresEventFields(schema, refs, /* @__PURE__ */ new Set()) ? "event" : "data";
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Returns the raw item schema: `itemSchema`, or `schema` on `text/event-stream` as 3.0 and 3.1 documents write one event.
|
|
1317
|
+
*/
|
|
1318
|
+
function getItemSchema({ contentType, mediaType, refs, document }) {
|
|
1319
|
+
if (!mediaType || !isStream(contentType, mediaType)) return null;
|
|
1320
|
+
const raw = mediaType.itemSchema ?? (isEventStream(contentType) && !isOpenApi32(document) ? mediaType.schema : void 0);
|
|
1321
|
+
if (!raw) return null;
|
|
1322
|
+
return isEventStream(contentType) ? toEventSchema(raw, refs) : raw;
|
|
1323
|
+
}
|
|
1175
1324
|
// src/emit/schemaShape.ts
|
|
1176
1325
|
/**
|
|
1177
1326
|
* Whether `format` maps to a dedicated type; `false` is what `KUBB_UNSUPPORTED_FORMAT` flags.
|
|
@@ -1229,7 +1378,7 @@ function flattenSchema(schema) {
|
|
|
1229
1378
|
/**
|
|
1230
1379
|
* Links one freshly converted schema and its nested schemas to their RFC 6901 pointers
|
|
1231
1380
|
* (`#/components/schemas/Pet/properties/owner/properties/name`), and reports the advisory
|
|
1232
|
-
* diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) against them.
|
|
1381
|
+
* diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`, `KUBB_DUPLICATE_DISCRIMINATOR`) against them.
|
|
1233
1382
|
* Refs are not followed, so the resolved schema is handled under its own walk.
|
|
1234
1383
|
* Reports land in the active build run, are a no-op outside one, and repeats are deduped by the build.
|
|
1235
1384
|
*/
|
|
@@ -1274,16 +1423,67 @@ function visit(node, pointer) {
|
|
|
1274
1423
|
for (const member of node.members ?? []) visit(member, `${pointer}/not`);
|
|
1275
1424
|
return;
|
|
1276
1425
|
}
|
|
1426
|
+
if (node.type === "union" && node.discriminatorPropertyName) reportDuplicateDiscriminators(node, node.discriminatorPropertyName, pointer);
|
|
1277
1427
|
if (node.type === "union" || node.type === "intersection") {
|
|
1278
1428
|
const keyword = node.type === "intersection" ? "allOf" : node.strategy === "any" ? "anyOf" : "oneOf";
|
|
1279
1429
|
for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/${keyword}/${index}`);
|
|
1280
1430
|
}
|
|
1281
1431
|
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Reports a declared discriminator whose value pins more than one member. The spec cannot tell
|
|
1434
|
+
* those members apart, and Zod's `z.discriminatedUnion` throws on the first parse.
|
|
1435
|
+
* One diagnostic per union, since the build dedupes by code and pointer.
|
|
1436
|
+
*/
|
|
1437
|
+
function reportDuplicateDiscriminators(node, propertyName, pointer) {
|
|
1438
|
+
const keyword = node.strategy === "any" ? "anyOf" : "oneOf";
|
|
1439
|
+
const membersByValue = /* @__PURE__ */ new Map();
|
|
1440
|
+
for (const [index, member] of (node.members ?? []).entries()) {
|
|
1441
|
+
const label = memberName(member) ?? `${keyword}/${index}`;
|
|
1442
|
+
for (const value of memberValues(member, propertyName)) membersByValue.set(value, [...membersByValue.get(value) ?? [], label]);
|
|
1443
|
+
}
|
|
1444
|
+
const duplicates = [...membersByValue].filter(([, members]) => members.length > 1);
|
|
1445
|
+
if (!duplicates.length) return;
|
|
1446
|
+
Diagnostics.report({
|
|
1447
|
+
code: Diagnostics.code.duplicateDiscriminator,
|
|
1448
|
+
severity: "warning",
|
|
1449
|
+
message: duplicates.map(([value, members]) => `Discriminator "${propertyName}" value ${JSON.stringify(value)} matches more than one member (${members.join(", ")}).`).join(" "),
|
|
1450
|
+
help: `Give each member its own "${propertyName}" value, or drop the discriminator. Zod's z.discriminatedUnion throws on the first parse of this union.`,
|
|
1451
|
+
location: {
|
|
1452
|
+
kind: "schema",
|
|
1453
|
+
pointer
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
function memberValues(node, propertyName, seen = /* @__PURE__ */ new Set()) {
|
|
1458
|
+
if (seen.has(node)) return [];
|
|
1459
|
+
seen.add(node);
|
|
1460
|
+
const target = node.type === "ref" && node.schema ? node.schema : node;
|
|
1461
|
+
if (target.type === "union") return [...new Set(target.members?.flatMap((member) => memberValues(member, propertyName, seen)))];
|
|
1462
|
+
return intersect(ast.resolveSchemaProperties({
|
|
1463
|
+
node,
|
|
1464
|
+
propertyName
|
|
1465
|
+
}).map((property) => [...ast.getSchemaLiteralValues(property.schema)]));
|
|
1466
|
+
}
|
|
1467
|
+
function intersect(slices) {
|
|
1468
|
+
const pinned = slices.filter((values) => values.length > 0);
|
|
1469
|
+
return pinned.reduce((acc, values) => acc.filter((value) => values.includes(value)), pinned[0] ?? []);
|
|
1470
|
+
}
|
|
1471
|
+
function memberName(member) {
|
|
1472
|
+
if (member.type === "ref") return member.name ?? void 0;
|
|
1473
|
+
if (member.type === "intersection") return member.members?.map(memberName).find(Boolean);
|
|
1474
|
+
}
|
|
1282
1475
|
// src/operation.ts
|
|
1283
1476
|
function slugify(value) {
|
|
1284
1477
|
return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
1285
1478
|
}
|
|
1286
1479
|
/**
|
|
1480
|
+
* Returns the operation's JSON pointer, under `additionalOperations` for an OpenAPI 3.2 method outside the fixed fields.
|
|
1481
|
+
*/
|
|
1482
|
+
function getOperationPointer({ path, method }) {
|
|
1483
|
+
const pathPointer = `#/paths/${escapePointerToken(path)}`;
|
|
1484
|
+
return SUPPORTED_METHODS.has(method.toLowerCase()) ? `${pathPointer}/${method.toLowerCase()}` : `${pathPointer}/additionalOperations/${escapePointerToken(method)}`;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1287
1487
|
* Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
|
|
1288
1488
|
*/
|
|
1289
1489
|
function getOperationId({ path, method, schema }) {
|
|
@@ -1336,7 +1536,7 @@ function getParameters({ operation, refs }) {
|
|
|
1336
1536
|
}] : [];
|
|
1337
1537
|
});
|
|
1338
1538
|
const pathPointer = `#/paths/${escapePointerToken(operation.path)}`;
|
|
1339
|
-
const operationParams = resolveParams(operation.schema?.parameters || [],
|
|
1539
|
+
const operationParams = resolveParams(operation.schema?.parameters || [], getOperationPointer(operation));
|
|
1340
1540
|
const pathLevelParams = resolveParams(operation.pathItem.parameters ?? [], pathPointer);
|
|
1341
1541
|
const paramMap = /* @__PURE__ */ new Map();
|
|
1342
1542
|
for (const entry of [...pathLevelParams, ...operationParams]) if (entry.param.name && entry.param.in) paramMap.set(`${entry.param.in}:${entry.param.name}`, entry);
|
|
@@ -1369,17 +1569,59 @@ function getEncoding(encoding) {
|
|
|
1369
1569
|
});
|
|
1370
1570
|
return entries.length ? Object.fromEntries(entries) : void 0;
|
|
1371
1571
|
}
|
|
1372
|
-
/**
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1572
|
+
/**
|
|
1573
|
+
* Returns the media type entry for `contentType`, resolving a `$ref` into `components.mediaTypes`. `null` when absent.
|
|
1574
|
+
*/
|
|
1575
|
+
function getMediaType({ content, contentType, refs }) {
|
|
1576
|
+
return contentType ? refs.deref(content?.[contentType]) : null;
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Returns the schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON
|
|
1580
|
+
* entry. For a stream this is the item schema, so `schema: string` next to `itemSchema` types one item.
|
|
1581
|
+
*/
|
|
1582
|
+
function getBodySchema({ content, contentType, refs, document }) {
|
|
1583
|
+
const entry = getMediaType({
|
|
1584
|
+
content,
|
|
1585
|
+
contentType,
|
|
1586
|
+
refs
|
|
1587
|
+
});
|
|
1588
|
+
if (!contentType || !entry) return null;
|
|
1589
|
+
const schema = getItemSchema({
|
|
1590
|
+
contentType,
|
|
1591
|
+
mediaType: entry,
|
|
1592
|
+
refs,
|
|
1593
|
+
document
|
|
1594
|
+
}) ?? entry.schema;
|
|
1595
|
+
const binary = entry.itemSchema ? void 0 : getBinaryFallbackSchema(contentType, schema);
|
|
1377
1596
|
if (binary) return binary;
|
|
1378
|
-
return
|
|
1597
|
+
return schema ? refs.derefKeepingRef(schema) : null;
|
|
1598
|
+
}
|
|
1599
|
+
/** The `stream` field of a content node, set when the media type is a stream. `node` is the parsed item schema. */
|
|
1600
|
+
function getStreamField({ contentType, mediaType, refs, document, node }) {
|
|
1601
|
+
if (!isStream(contentType, mediaType)) return {};
|
|
1602
|
+
const raw = getItemSchema({
|
|
1603
|
+
contentType,
|
|
1604
|
+
mediaType,
|
|
1605
|
+
refs,
|
|
1606
|
+
document
|
|
1607
|
+
});
|
|
1608
|
+
return { stream: {
|
|
1609
|
+
...raw ? { schema: node } : {},
|
|
1610
|
+
...raw && isEventStream(contentType) ? { shape: getItemShape(raw, refs) } : {}
|
|
1611
|
+
} };
|
|
1612
|
+
}
|
|
1613
|
+
/** The media type keyword the body schema was read from, for the diagnostic pointer. */
|
|
1614
|
+
function schemaKeyword(mediaType) {
|
|
1615
|
+
return mediaType?.itemSchema ? "itemSchema" : "schema";
|
|
1616
|
+
}
|
|
1617
|
+
/** Pointer of a media type entry, following a `$ref` into `components.mediaTypes` so a diagnostic lands on the source. */
|
|
1618
|
+
function mediaTypePointer(content, contentType, fallback) {
|
|
1619
|
+
const entry = content?.[contentType];
|
|
1620
|
+
return isReference(entry) ? entry.$ref : fallback;
|
|
1379
1621
|
}
|
|
1380
1622
|
/**
|
|
1381
|
-
* Builds an `Operation` for every supported HTTP method
|
|
1382
|
-
* `x-` path keys and unresolvable path-item `$ref`s are skipped.
|
|
1623
|
+
* Builds an `Operation` for every supported HTTP method and every OpenAPI 3.2 `additionalOperations` entry on
|
|
1624
|
+
* every path, in document order. `x-` path keys and unresolvable path-item `$ref`s are skipped.
|
|
1383
1625
|
*/
|
|
1384
1626
|
function getOperations(document, refs) {
|
|
1385
1627
|
const operations = [];
|
|
@@ -1401,6 +1643,12 @@ function getOperations(document, refs) {
|
|
|
1401
1643
|
pathItem
|
|
1402
1644
|
});
|
|
1403
1645
|
}
|
|
1646
|
+
for (const [method, schema] of Object.entries(pathItem.additionalOperations ?? {})) if (schema && typeof schema === "object" && !SUPPORTED_METHODS.has(method.toLowerCase())) operations.push({
|
|
1647
|
+
path,
|
|
1648
|
+
method,
|
|
1649
|
+
schema,
|
|
1650
|
+
pathItem
|
|
1651
|
+
});
|
|
1404
1652
|
}
|
|
1405
1653
|
return operations;
|
|
1406
1654
|
}
|
|
@@ -1414,10 +1662,15 @@ function collectPropertyKeysByFlag(schema, flag) {
|
|
|
1414
1662
|
}
|
|
1415
1663
|
return keys.length ? keys : null;
|
|
1416
1664
|
}
|
|
1417
|
-
|
|
1665
|
+
/** Converts one parameter into a `ParameterNode`, naming an inline schema after the operation and the parameter. */
|
|
1666
|
+
function parseParameter({ param, pointer, parentName, options, parseSchema, refs }) {
|
|
1418
1667
|
const schemaName = parentName && param.name ? pascalCase(`${parentName} ${param.name}`) : void 0;
|
|
1419
1668
|
const contentType = param.schema ? void 0 : Object.keys(param.content ?? {})[0];
|
|
1420
|
-
const rawSchema = param.schema ?? (
|
|
1669
|
+
const rawSchema = param.schema ?? getMediaType({
|
|
1670
|
+
content: param.content,
|
|
1671
|
+
contentType,
|
|
1672
|
+
refs
|
|
1673
|
+
})?.schema;
|
|
1421
1674
|
const schema = rawSchema ? parseSchema({
|
|
1422
1675
|
schema: rawSchema,
|
|
1423
1676
|
name: schemaName
|
|
@@ -1440,16 +1693,17 @@ function parseParameter({ param, pointer, parentName, options, parseSchema }) {
|
|
|
1440
1693
|
* Converts an OAS `Operation` into an `OperationNode`.
|
|
1441
1694
|
*/
|
|
1442
1695
|
function parseOperation({ operation, ...ctx }) {
|
|
1443
|
-
const { refs, contentType, options, parseSchema } = ctx;
|
|
1696
|
+
const { document, refs, contentType, options, parseSchema } = ctx;
|
|
1444
1697
|
const operationId = getOperationId(operation);
|
|
1445
1698
|
const operationName = operationId ? pascalCase(operationId) : void 0;
|
|
1446
|
-
const pointer =
|
|
1699
|
+
const pointer = getOperationPointer(operation);
|
|
1447
1700
|
const parameters = getParameters({
|
|
1448
1701
|
operation,
|
|
1449
1702
|
refs
|
|
1450
1703
|
}).map((entry) => parseParameter({
|
|
1451
1704
|
options,
|
|
1452
1705
|
parseSchema,
|
|
1706
|
+
refs,
|
|
1453
1707
|
...entry,
|
|
1454
1708
|
parentName: operationName
|
|
1455
1709
|
}));
|
|
@@ -1463,20 +1717,33 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1463
1717
|
const schema = getBodySchema({
|
|
1464
1718
|
content: body?.content,
|
|
1465
1719
|
contentType: ct,
|
|
1466
|
-
refs
|
|
1720
|
+
refs,
|
|
1721
|
+
document
|
|
1467
1722
|
});
|
|
1468
1723
|
if (!schema) return [];
|
|
1469
|
-
const
|
|
1724
|
+
const mediaType = getMediaType({
|
|
1725
|
+
content: body?.content,
|
|
1726
|
+
contentType: ct,
|
|
1727
|
+
refs
|
|
1728
|
+
});
|
|
1729
|
+
const node = ast.optionality(parseSchema({
|
|
1470
1730
|
schema,
|
|
1471
1731
|
name: requestBodyName
|
|
1472
|
-
});
|
|
1473
|
-
scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}
|
|
1474
|
-
const encoding = getEncoding(
|
|
1732
|
+
}), bodyRequired);
|
|
1733
|
+
scanSchema(node, `${mediaTypePointer(body?.content, ct, `${pointer}/requestBody/content/${escapePointerToken(ct)}`)}/${schemaKeyword(mediaType)}`);
|
|
1734
|
+
const encoding = getEncoding(mediaType?.encoding);
|
|
1475
1735
|
return [ast.factory.createContent({
|
|
1476
1736
|
contentType: ct,
|
|
1477
|
-
schema:
|
|
1737
|
+
schema: node,
|
|
1478
1738
|
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly"),
|
|
1479
|
-
...encoding && { encoding }
|
|
1739
|
+
...encoding && { encoding },
|
|
1740
|
+
...getStreamField({
|
|
1741
|
+
contentType: ct,
|
|
1742
|
+
mediaType,
|
|
1743
|
+
refs,
|
|
1744
|
+
document,
|
|
1745
|
+
node
|
|
1746
|
+
})
|
|
1480
1747
|
})];
|
|
1481
1748
|
});
|
|
1482
1749
|
const requestBody = content.length > 0 || body?.description ? {
|
|
@@ -1494,6 +1761,12 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1494
1761
|
const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
|
|
1495
1762
|
const parseEntrySchema = (ct) => {
|
|
1496
1763
|
const raw = getBodySchema({
|
|
1764
|
+
content: response?.content,
|
|
1765
|
+
contentType: ct,
|
|
1766
|
+
refs,
|
|
1767
|
+
document
|
|
1768
|
+
});
|
|
1769
|
+
const mediaType = getMediaType({
|
|
1497
1770
|
content: response?.content,
|
|
1498
1771
|
contentType: ct,
|
|
1499
1772
|
refs
|
|
@@ -1502,10 +1775,17 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1502
1775
|
schema: raw,
|
|
1503
1776
|
name: responseName
|
|
1504
1777
|
}) : ast.factory.createSchema({ type: options.emptySchemaType });
|
|
1505
|
-
if (raw && ct) scanSchema(node, `${pointer}/responses/${statusCode}/content/${escapePointerToken(ct)}
|
|
1778
|
+
if (raw && ct) scanSchema(node, `${mediaTypePointer(response?.content, ct, `${pointer}/responses/${statusCode}/content/${escapePointerToken(ct)}`)}/${schemaKeyword(mediaType)}`);
|
|
1506
1779
|
return {
|
|
1507
1780
|
schema: node,
|
|
1508
|
-
keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
|
|
1781
|
+
keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly"),
|
|
1782
|
+
...ct ? getStreamField({
|
|
1783
|
+
contentType: ct,
|
|
1784
|
+
mediaType,
|
|
1785
|
+
refs,
|
|
1786
|
+
document,
|
|
1787
|
+
node
|
|
1788
|
+
}) : {}
|
|
1509
1789
|
};
|
|
1510
1790
|
};
|
|
1511
1791
|
const responseContent = (contentType ? [contentType] : Object.keys(response?.content ?? {})).map((ct) => ast.factory.createContent({
|
|
@@ -1536,7 +1816,7 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1536
1816
|
pointer,
|
|
1537
1817
|
operationId,
|
|
1538
1818
|
protocol: "http",
|
|
1539
|
-
method: operation.method.toUpperCase(),
|
|
1819
|
+
method: SUPPORTED_METHODS.has(operation.method) ? operation.method.toUpperCase() : operation.method,
|
|
1540
1820
|
path: operation.path,
|
|
1541
1821
|
tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
|
|
1542
1822
|
summary: pickDoc("summary") || void 0,
|
|
@@ -1631,6 +1911,14 @@ function createDiscriminantNode({ propertyName, values }) {
|
|
|
1631
1911
|
});
|
|
1632
1912
|
}
|
|
1633
1913
|
/**
|
|
1914
|
+
* Returns the `$ref` of an OpenAPI 3.2 `defaultMapping`, written as a schema name or a `$ref`; `undefined` when none.
|
|
1915
|
+
*/
|
|
1916
|
+
function getDefaultMappingRef(discriminator) {
|
|
1917
|
+
const value = discriminator?.defaultMapping;
|
|
1918
|
+
if (!value) return void 0;
|
|
1919
|
+
return value.includes("/") ? value : `${SCHEMA_REF_PREFIX}${value}`;
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1634
1922
|
* Returns every discriminator key whose mapping value matches `ref`, in mapping order.
|
|
1635
1923
|
*
|
|
1636
1924
|
* A mapping may point several keys at the same schema, in which case the member has to be
|
|
@@ -1726,13 +2014,15 @@ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode,
|
|
|
1726
2014
|
}
|
|
1727
2015
|
return constrains(variant) ? null : value;
|
|
1728
2016
|
}
|
|
2017
|
+
const defaultRef = getDefaultMappingRef(discriminator);
|
|
1729
2018
|
return unionMembers.map((s) => {
|
|
1730
2019
|
const ref = isReference(s) ? s.$ref : void 0;
|
|
1731
|
-
const
|
|
2020
|
+
const isDefault = ref !== void 0 && ref === defaultRef;
|
|
2021
|
+
const mappedValues = isDefault ? [] : findDiscriminators({
|
|
1732
2022
|
mapping: discriminator?.mapping,
|
|
1733
2023
|
ref
|
|
1734
2024
|
});
|
|
1735
|
-
const implicitValue = mappedValues.length ? null : implicitDiscriminantValue(s);
|
|
2025
|
+
const implicitValue = mappedValues.length || isDefault ? null : implicitDiscriminantValue(s);
|
|
1736
2026
|
const propertyType = propertyTypeOf({
|
|
1737
2027
|
schema: s,
|
|
1738
2028
|
propertyName: discriminator?.propertyName ?? "",
|
|
@@ -1777,7 +2067,8 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
|
|
|
1777
2067
|
const parentUnion = deref?.oneOf ?? deref?.anyOf;
|
|
1778
2068
|
if (!deref || !discriminator || isDiscriminator(deref) && !parentUnion) return true;
|
|
1779
2069
|
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1780
|
-
const
|
|
2070
|
+
const isDefault = childRef === getDefaultMappingRef(discriminator);
|
|
2071
|
+
const values = isDefault ? [] : findDiscriminators({
|
|
1781
2072
|
mapping: discriminator.mapping,
|
|
1782
2073
|
ref: childRef
|
|
1783
2074
|
});
|
|
@@ -1800,7 +2091,7 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
|
|
|
1800
2091
|
})
|
|
1801
2092
|
});
|
|
1802
2093
|
}
|
|
1803
|
-
return !(parentUnion && (values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
|
|
2094
|
+
return !(parentUnion && (isDefault || values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
|
|
1804
2095
|
}),
|
|
1805
2096
|
discriminantValues
|
|
1806
2097
|
};
|
|
@@ -2122,10 +2413,12 @@ function convertUnion(context) {
|
|
|
2122
2413
|
nullable,
|
|
2123
2414
|
defaultValue
|
|
2124
2415
|
};
|
|
2125
|
-
const unionMembers = resolveUnionMembers([...schema.oneOf ?? [], ...schema.anyOf ?? []], schema.properties ?? {});
|
|
2126
|
-
const strategy = schema.oneOf ? "one" : "any";
|
|
2127
|
-
const explicitDiscriminatorPropertyName = isDiscriminator(schema) ? schema.discriminator.propertyName : void 0;
|
|
2128
2416
|
const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
|
|
2417
|
+
const declaredMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
|
|
2418
|
+
const defaultRef = getDefaultMappingRef(discriminator);
|
|
2419
|
+
const unionMembers = resolveUnionMembers(defaultRef && !declaredMembers.some((member) => isReference(member) && member.$ref === defaultRef) ? [...declaredMembers, { $ref: defaultRef }] : declaredMembers, schema.properties ?? {});
|
|
2420
|
+
const strategy = schema.oneOf ? "one" : "any";
|
|
2421
|
+
const explicitDiscriminatorPropertyName = discriminator?.propertyName;
|
|
2129
2422
|
const { oneOf: _o, anyOf: _a, discriminator: _d, ...memberBaseSchema } = schema;
|
|
2130
2423
|
const sharedPropertiesNode = schema.properties ? parse({
|
|
2131
2424
|
schema: memberBaseSchema,
|
|
@@ -2178,7 +2471,7 @@ function convertUnion(context) {
|
|
|
2178
2471
|
members,
|
|
2179
2472
|
discriminatorPropertyName: explicitDiscriminatorPropertyName ?? ast.inferDiscriminatorPropertyName(members)
|
|
2180
2473
|
});
|
|
2181
|
-
return ast.applyMacros(unionNode, [
|
|
2474
|
+
return ast.applyMacros(unionNode, [macroDedupeUnion], { depth: "shallow" });
|
|
2182
2475
|
}
|
|
2183
2476
|
/**
|
|
2184
2477
|
* Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
|
|
@@ -2655,6 +2948,10 @@ const schemaRules = [
|
|
|
2655
2948
|
primitive: "string"
|
|
2656
2949
|
})
|
|
2657
2950
|
},
|
|
2951
|
+
{
|
|
2952
|
+
match: ({ schema, type }) => !type && !schema.enum?.length && (schema.contentMediaType !== void 0 || schema.contentSchema !== void 0),
|
|
2953
|
+
convert: convertString
|
|
2954
|
+
},
|
|
2658
2955
|
{
|
|
2659
2956
|
match: ({ schema, type }) => !type && (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0),
|
|
2660
2957
|
convert: convertString
|
|
@@ -2949,7 +3246,7 @@ const locate = (source, location) => {
|
|
|
2949
3246
|
*/
|
|
2950
3247
|
const adapterOasName = "oas";
|
|
2951
3248
|
/**
|
|
2952
|
-
* Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.
|
|
3249
|
+
* Default Kubb adapter for OpenAPI 2.0, 3.0, 3.1, and 3.2 specifications. Reads the
|
|
2953
3250
|
* spec from `input` (a file path, URL, inline content, or parsed object), validates
|
|
2954
3251
|
* it, resolves the base URL, and converts every schema and operation into the
|
|
2955
3252
|
* universal AST that every downstream plugin consumes.
|
|
@@ -3023,6 +3320,7 @@ const adapterOas = defineExtension({
|
|
|
3023
3320
|
const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
|
|
3024
3321
|
const operationNodes = getOperations(document, refs).map((operation) => parseOperation({
|
|
3025
3322
|
operation,
|
|
3323
|
+
document,
|
|
3026
3324
|
refs,
|
|
3027
3325
|
contentType,
|
|
3028
3326
|
options: parserOptions,
|