@kubb/adapter-oas 5.15.3 → 5.16.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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineExtension } from "@kubb/core";
2
- import { Diagnostics, ast, childName, enumPrimitive, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
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.15.3";
22
+ var version = "5.16.0";
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
- return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
471
+ const lower = mimeType.toLowerCase();
472
+ return jsonMimeFragments.some((fragment) => lower.includes(fragment));
453
473
  }
454
474
  // src/refs.ts
455
475
  /**
@@ -615,8 +635,10 @@ async function readSource(sourcePath) {
615
635
  async function resolveSource(sourcePath) {
616
636
  const data = await readSource(sourcePath);
617
637
  if (sourcePath.toLowerCase().endsWith(".md")) return data;
638
+ const pathname = urlRegExp.test(sourcePath) ? new URL(sourcePath).pathname : sourcePath;
618
639
  return parseYaml(data, {
619
640
  origin: sourcePath,
641
+ json: !/\.ya?ml$/i.test(pathname),
620
642
  help: "Fix the syntax at the reported line, or check that `input` points at the spec and not an HTML page."
621
643
  });
622
644
  }
@@ -624,11 +646,19 @@ async function resolveSource(sourcePath) {
624
646
  * Parses a YAML (or JSON) spec. A syntax error throws `KUBB_INVALID_DOCUMENT` naming `origin`,
625
647
  * with the parser message, which keeps the line and column.
626
648
  *
627
- * JSON is valid YAML, so one parser handles both. `js-yaml` parses about ten times faster than
628
- * `yaml`, and unlike `JSON.parse` it rejects duplicate keys instead of keeping the last one. Its
629
- * core schema keeps timestamps as strings and `<<` as a plain key, as `yaml` does for YAML 1.2.
649
+ * With `json` (the default), a document that starts with `{` or `[` goes through `JSON.parse`
650
+ * first, about ten times faster than `js-yaml` on a JSON spec. A duplicate key then keeps the last
651
+ * value, as every JSON parser does. YAML flow syntax also starts with `{`, so a parse failure falls
652
+ * through to YAML. Pass `json: false` for a source known to be YAML.
653
+ *
654
+ * `js-yaml` parses about ten times faster than `yaml`, and rejects duplicate keys instead of
655
+ * keeping the last one. Its core schema keeps timestamps as strings and `<<` as a plain key, as
656
+ * `yaml` does for YAML 1.2.
630
657
  */
631
- function parseYaml(data, { origin, help }) {
658
+ function parseYaml(data, { origin, help, json = true }) {
659
+ if (json && /^\s*[[{]/.test(data)) try {
660
+ return JSON.parse(data);
661
+ } catch {}
632
662
  try {
633
663
  const document = load(data, { schema: CORE_SCHEMA });
634
664
  if (document !== void 0) return document;
@@ -863,6 +893,17 @@ function reportInvalidSpec(message) {
863
893
  * ```
864
894
  */
865
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
+ }
866
907
  const { compileErrors, validate } = await import("@readme/openapi-parser");
867
908
  let result;
868
909
  try {
@@ -892,6 +933,7 @@ function reportValidationErrors({ errors, additionalErrors }) {
892
933
  * validating in place where worker threads are missing, as in edge runtimes.
893
934
  */
894
935
  async function validateDocumentInWorker(document) {
936
+ if (isOpenApi32(document)) return validateDocument(document);
895
937
  let message;
896
938
  try {
897
939
  const { Worker } = await import("node:worker_threads");
@@ -1162,6 +1204,123 @@ function resolveServerUrl(server, overrides) {
1162
1204
  }
1163
1205
  return url;
1164
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
+ }
1165
1324
  // src/emit/schemaShape.ts
1166
1325
  /**
1167
1326
  * Whether `format` maps to a dedicated type; `false` is what `KUBB_UNSUPPORTED_FORMAT` flags.
@@ -1274,6 +1433,13 @@ function slugify(value) {
1274
1433
  return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
1275
1434
  }
1276
1435
  /**
1436
+ * Returns the operation's JSON pointer, under `additionalOperations` for an OpenAPI 3.2 method outside the fixed fields.
1437
+ */
1438
+ function getOperationPointer({ path, method }) {
1439
+ const pathPointer = `#/paths/${escapePointerToken(path)}`;
1440
+ return SUPPORTED_METHODS.has(method.toLowerCase()) ? `${pathPointer}/${method.toLowerCase()}` : `${pathPointer}/additionalOperations/${escapePointerToken(method)}`;
1441
+ }
1442
+ /**
1277
1443
  * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
1278
1444
  */
1279
1445
  function getOperationId({ path, method, schema }) {
@@ -1326,7 +1492,7 @@ function getParameters({ operation, refs }) {
1326
1492
  }] : [];
1327
1493
  });
1328
1494
  const pathPointer = `#/paths/${escapePointerToken(operation.path)}`;
1329
- const operationParams = resolveParams(operation.schema?.parameters || [], `${pathPointer}/${operation.method.toLowerCase()}`);
1495
+ const operationParams = resolveParams(operation.schema?.parameters || [], getOperationPointer(operation));
1330
1496
  const pathLevelParams = resolveParams(operation.pathItem.parameters ?? [], pathPointer);
1331
1497
  const paramMap = /* @__PURE__ */ new Map();
1332
1498
  for (const entry of [...pathLevelParams, ...operationParams]) if (entry.param.name && entry.param.in) paramMap.set(`${entry.param.in}:${entry.param.name}`, entry);
@@ -1359,17 +1525,59 @@ function getEncoding(encoding) {
1359
1525
  });
1360
1526
  return entries.length ? Object.fromEntries(entries) : void 0;
1361
1527
  }
1362
- /** Schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON entry. */
1363
- function getBodySchema({ content, contentType, refs }) {
1364
- const entry = contentType ? content?.[contentType] : void 0;
1365
- if (!entry) return null;
1366
- const binary = getBinaryFallbackSchema(contentType, entry.schema);
1528
+ /**
1529
+ * Returns the media type entry for `contentType`, resolving a `$ref` into `components.mediaTypes`. `null` when absent.
1530
+ */
1531
+ function getMediaType({ content, contentType, refs }) {
1532
+ return contentType ? refs.deref(content?.[contentType]) : null;
1533
+ }
1534
+ /**
1535
+ * Returns the schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON
1536
+ * entry. For a stream this is the item schema, so `schema: string` next to `itemSchema` types one item.
1537
+ */
1538
+ function getBodySchema({ content, contentType, refs, document }) {
1539
+ const entry = getMediaType({
1540
+ content,
1541
+ contentType,
1542
+ refs
1543
+ });
1544
+ if (!contentType || !entry) return null;
1545
+ const schema = getItemSchema({
1546
+ contentType,
1547
+ mediaType: entry,
1548
+ refs,
1549
+ document
1550
+ }) ?? entry.schema;
1551
+ const binary = entry.itemSchema ? void 0 : getBinaryFallbackSchema(contentType, schema);
1367
1552
  if (binary) return binary;
1368
- return entry.schema ? refs.derefKeepingRef(entry.schema) : null;
1553
+ return schema ? refs.derefKeepingRef(schema) : null;
1554
+ }
1555
+ /** The `stream` field of a content node, set when the media type is a stream. `node` is the parsed item schema. */
1556
+ function getStreamField({ contentType, mediaType, refs, document, node }) {
1557
+ if (!isStream(contentType, mediaType)) return {};
1558
+ const raw = getItemSchema({
1559
+ contentType,
1560
+ mediaType,
1561
+ refs,
1562
+ document
1563
+ });
1564
+ return { stream: {
1565
+ ...raw ? { schema: node } : {},
1566
+ ...raw && isEventStream(contentType) ? { shape: getItemShape(raw, refs) } : {}
1567
+ } };
1568
+ }
1569
+ /** The media type keyword the body schema was read from, for the diagnostic pointer. */
1570
+ function schemaKeyword(mediaType) {
1571
+ return mediaType?.itemSchema ? "itemSchema" : "schema";
1572
+ }
1573
+ /** Pointer of a media type entry, following a `$ref` into `components.mediaTypes` so a diagnostic lands on the source. */
1574
+ function mediaTypePointer(content, contentType, fallback) {
1575
+ const entry = content?.[contentType];
1576
+ return isReference(entry) ? entry.$ref : fallback;
1369
1577
  }
1370
1578
  /**
1371
- * Builds an `Operation` for every supported HTTP method on every path, in document order.
1372
- * `x-` path keys and unresolvable path-item `$ref`s are skipped.
1579
+ * Builds an `Operation` for every supported HTTP method and every OpenAPI 3.2 `additionalOperations` entry on
1580
+ * every path, in document order. `x-` path keys and unresolvable path-item `$ref`s are skipped.
1373
1581
  */
1374
1582
  function getOperations(document, refs) {
1375
1583
  const operations = [];
@@ -1391,6 +1599,12 @@ function getOperations(document, refs) {
1391
1599
  pathItem
1392
1600
  });
1393
1601
  }
1602
+ for (const [method, schema] of Object.entries(pathItem.additionalOperations ?? {})) if (schema && typeof schema === "object" && !SUPPORTED_METHODS.has(method.toLowerCase())) operations.push({
1603
+ path,
1604
+ method,
1605
+ schema,
1606
+ pathItem
1607
+ });
1394
1608
  }
1395
1609
  return operations;
1396
1610
  }
@@ -1404,10 +1618,15 @@ function collectPropertyKeysByFlag(schema, flag) {
1404
1618
  }
1405
1619
  return keys.length ? keys : null;
1406
1620
  }
1407
- function parseParameter({ param, pointer, parentName, options, parseSchema }) {
1621
+ /** Converts one parameter into a `ParameterNode`, naming an inline schema after the operation and the parameter. */
1622
+ function parseParameter({ param, pointer, parentName, options, parseSchema, refs }) {
1408
1623
  const schemaName = parentName && param.name ? pascalCase(`${parentName} ${param.name}`) : void 0;
1409
1624
  const contentType = param.schema ? void 0 : Object.keys(param.content ?? {})[0];
1410
- const rawSchema = param.schema ?? (contentType ? param.content?.[contentType]?.schema : void 0);
1625
+ const rawSchema = param.schema ?? getMediaType({
1626
+ content: param.content,
1627
+ contentType,
1628
+ refs
1629
+ })?.schema;
1411
1630
  const schema = rawSchema ? parseSchema({
1412
1631
  schema: rawSchema,
1413
1632
  name: schemaName
@@ -1430,16 +1649,17 @@ function parseParameter({ param, pointer, parentName, options, parseSchema }) {
1430
1649
  * Converts an OAS `Operation` into an `OperationNode`.
1431
1650
  */
1432
1651
  function parseOperation({ operation, ...ctx }) {
1433
- const { refs, contentType, options, parseSchema } = ctx;
1652
+ const { document, refs, contentType, options, parseSchema } = ctx;
1434
1653
  const operationId = getOperationId(operation);
1435
1654
  const operationName = operationId ? pascalCase(operationId) : void 0;
1436
- const pointer = `#/paths/${escapePointerToken(operation.path)}/${operation.method.toLowerCase()}`;
1655
+ const pointer = getOperationPointer(operation);
1437
1656
  const parameters = getParameters({
1438
1657
  operation,
1439
1658
  refs
1440
1659
  }).map((entry) => parseParameter({
1441
1660
  options,
1442
1661
  parseSchema,
1662
+ refs,
1443
1663
  ...entry,
1444
1664
  parentName: operationName
1445
1665
  }));
@@ -1453,20 +1673,33 @@ function parseOperation({ operation, ...ctx }) {
1453
1673
  const schema = getBodySchema({
1454
1674
  content: body?.content,
1455
1675
  contentType: ct,
1456
- refs
1676
+ refs,
1677
+ document
1457
1678
  });
1458
1679
  if (!schema) return [];
1459
- const node = parseSchema({
1680
+ const mediaType = getMediaType({
1681
+ content: body?.content,
1682
+ contentType: ct,
1683
+ refs
1684
+ });
1685
+ const node = ast.optionality(parseSchema({
1460
1686
  schema,
1461
1687
  name: requestBodyName
1462
- });
1463
- scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}/schema`);
1464
- const encoding = getEncoding(body?.content?.[ct]?.encoding);
1688
+ }), bodyRequired);
1689
+ scanSchema(node, `${mediaTypePointer(body?.content, ct, `${pointer}/requestBody/content/${escapePointerToken(ct)}`)}/${schemaKeyword(mediaType)}`);
1690
+ const encoding = getEncoding(mediaType?.encoding);
1465
1691
  return [ast.factory.createContent({
1466
1692
  contentType: ct,
1467
- schema: ast.optionality(node, bodyRequired),
1693
+ schema: node,
1468
1694
  keysToOmit: collectPropertyKeysByFlag(schema, "readOnly"),
1469
- ...encoding && { encoding }
1695
+ ...encoding && { encoding },
1696
+ ...getStreamField({
1697
+ contentType: ct,
1698
+ mediaType,
1699
+ refs,
1700
+ document,
1701
+ node
1702
+ })
1470
1703
  })];
1471
1704
  });
1472
1705
  const requestBody = content.length > 0 || body?.description ? {
@@ -1484,6 +1717,12 @@ function parseOperation({ operation, ...ctx }) {
1484
1717
  const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
1485
1718
  const parseEntrySchema = (ct) => {
1486
1719
  const raw = getBodySchema({
1720
+ content: response?.content,
1721
+ contentType: ct,
1722
+ refs,
1723
+ document
1724
+ });
1725
+ const mediaType = getMediaType({
1487
1726
  content: response?.content,
1488
1727
  contentType: ct,
1489
1728
  refs
@@ -1492,10 +1731,17 @@ function parseOperation({ operation, ...ctx }) {
1492
1731
  schema: raw,
1493
1732
  name: responseName
1494
1733
  }) : ast.factory.createSchema({ type: options.emptySchemaType });
1495
- if (raw && ct) scanSchema(node, `${pointer}/responses/${statusCode}/content/${escapePointerToken(ct)}/schema`);
1734
+ if (raw && ct) scanSchema(node, `${mediaTypePointer(response?.content, ct, `${pointer}/responses/${statusCode}/content/${escapePointerToken(ct)}`)}/${schemaKeyword(mediaType)}`);
1496
1735
  return {
1497
1736
  schema: node,
1498
- keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
1737
+ keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly"),
1738
+ ...ct ? getStreamField({
1739
+ contentType: ct,
1740
+ mediaType,
1741
+ refs,
1742
+ document,
1743
+ node
1744
+ }) : {}
1499
1745
  };
1500
1746
  };
1501
1747
  const responseContent = (contentType ? [contentType] : Object.keys(response?.content ?? {})).map((ct) => ast.factory.createContent({
@@ -1526,7 +1772,7 @@ function parseOperation({ operation, ...ctx }) {
1526
1772
  pointer,
1527
1773
  operationId,
1528
1774
  protocol: "http",
1529
- method: operation.method.toUpperCase(),
1775
+ method: SUPPORTED_METHODS.has(operation.method) ? operation.method.toUpperCase() : operation.method,
1530
1776
  path: operation.path,
1531
1777
  tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
1532
1778
  summary: pickDoc("summary") || void 0,
@@ -1621,6 +1867,14 @@ function createDiscriminantNode({ propertyName, values }) {
1621
1867
  });
1622
1868
  }
1623
1869
  /**
1870
+ * Returns the `$ref` of an OpenAPI 3.2 `defaultMapping`, written as a schema name or a `$ref`; `undefined` when none.
1871
+ */
1872
+ function getDefaultMappingRef(discriminator) {
1873
+ const value = discriminator?.defaultMapping;
1874
+ if (!value) return void 0;
1875
+ return value.includes("/") ? value : `${SCHEMA_REF_PREFIX}${value}`;
1876
+ }
1877
+ /**
1624
1878
  * Returns every discriminator key whose mapping value matches `ref`, in mapping order.
1625
1879
  *
1626
1880
  * A mapping may point several keys at the same schema, in which case the member has to be
@@ -1716,13 +1970,15 @@ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode,
1716
1970
  }
1717
1971
  return constrains(variant) ? null : value;
1718
1972
  }
1973
+ const defaultRef = getDefaultMappingRef(discriminator);
1719
1974
  return unionMembers.map((s) => {
1720
1975
  const ref = isReference(s) ? s.$ref : void 0;
1721
- const mappedValues = findDiscriminators({
1976
+ const isDefault = ref !== void 0 && ref === defaultRef;
1977
+ const mappedValues = isDefault ? [] : findDiscriminators({
1722
1978
  mapping: discriminator?.mapping,
1723
1979
  ref
1724
1980
  });
1725
- const implicitValue = mappedValues.length ? null : implicitDiscriminantValue(s);
1981
+ const implicitValue = mappedValues.length || isDefault ? null : implicitDiscriminantValue(s);
1726
1982
  const propertyType = propertyTypeOf({
1727
1983
  schema: s,
1728
1984
  propertyName: discriminator?.propertyName ?? "",
@@ -1767,7 +2023,8 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
1767
2023
  const parentUnion = deref?.oneOf ?? deref?.anyOf;
1768
2024
  if (!deref || !discriminator || isDiscriminator(deref) && !parentUnion) return true;
1769
2025
  const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1770
- const values = findDiscriminators({
2026
+ const isDefault = childRef === getDefaultMappingRef(discriminator);
2027
+ const values = isDefault ? [] : findDiscriminators({
1771
2028
  mapping: discriminator.mapping,
1772
2029
  ref: childRef
1773
2030
  });
@@ -1790,7 +2047,7 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
1790
2047
  })
1791
2048
  });
1792
2049
  }
1793
- return !(parentUnion && (values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
2050
+ return !(parentUnion && (isDefault || values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
1794
2051
  }),
1795
2052
  discriminantValues
1796
2053
  };
@@ -2112,10 +2369,12 @@ function convertUnion(context) {
2112
2369
  nullable,
2113
2370
  defaultValue
2114
2371
  };
2115
- const unionMembers = resolveUnionMembers([...schema.oneOf ?? [], ...schema.anyOf ?? []], schema.properties ?? {});
2116
- const strategy = schema.oneOf ? "one" : "any";
2117
- const explicitDiscriminatorPropertyName = isDiscriminator(schema) ? schema.discriminator.propertyName : void 0;
2118
2372
  const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
2373
+ const declaredMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
2374
+ const defaultRef = getDefaultMappingRef(discriminator);
2375
+ const unionMembers = resolveUnionMembers(defaultRef && !declaredMembers.some((member) => isReference(member) && member.$ref === defaultRef) ? [...declaredMembers, { $ref: defaultRef }] : declaredMembers, schema.properties ?? {});
2376
+ const strategy = schema.oneOf ? "one" : "any";
2377
+ const explicitDiscriminatorPropertyName = discriminator?.propertyName;
2119
2378
  const { oneOf: _o, anyOf: _a, discriminator: _d, ...memberBaseSchema } = schema;
2120
2379
  const sharedPropertiesNode = schema.properties ? parse({
2121
2380
  schema: memberBaseSchema,
@@ -2168,7 +2427,7 @@ function convertUnion(context) {
2168
2427
  members,
2169
2428
  discriminatorPropertyName: explicitDiscriminatorPropertyName ?? ast.inferDiscriminatorPropertyName(members)
2170
2429
  });
2171
- return ast.applyMacros(unionNode, [macroSimplifyUnion], { depth: "shallow" });
2430
+ return ast.applyMacros(unionNode, [macroDedupeUnion], { depth: "shallow" });
2172
2431
  }
2173
2432
  /**
2174
2433
  * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
@@ -2233,6 +2492,34 @@ function getNumericConstraints(schema) {
2233
2492
  };
2234
2493
  }
2235
2494
  /**
2495
+ * Whether an enum value fits the schema's numeric bounds.
2496
+ */
2497
+ function isWithinBounds({ value, schema }) {
2498
+ if (typeof value !== "number") return true;
2499
+ const { min, max, exclusiveMinimum, exclusiveMaximum, multipleOf } = getNumericConstraints(schema);
2500
+ if (min !== void 0 && value < min) return false;
2501
+ if (max !== void 0 && value > max) return false;
2502
+ if (exclusiveMinimum !== void 0 && value <= exclusiveMinimum) return false;
2503
+ if (exclusiveMaximum !== void 0 && value >= exclusiveMaximum) return false;
2504
+ if (multipleOf !== void 0 && Math.abs(value / multipleOf - Math.round(value / multipleOf)) > 1e-9) return false;
2505
+ return true;
2506
+ }
2507
+ /**
2508
+ * Builds the test for which `enum` entries stay: not `null`, and within the numeric bounds. `enum` and the bounds
2509
+ * apply together, so a value outside them is never valid. Bounds that exclude every value are a spec error, so
2510
+ * they are ignored rather than leaving an empty enum.
2511
+ */
2512
+ function createEnumMemberFilter(schema) {
2513
+ const boundsApply = schema.enum.some((value) => value !== null && isWithinBounds({
2514
+ value,
2515
+ schema
2516
+ }));
2517
+ return (value) => value !== null && (!boundsApply || isWithinBounds({
2518
+ value,
2519
+ schema
2520
+ }));
2521
+ }
2522
+ /**
2236
2523
  * Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
2237
2524
  * and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
2238
2525
  */
@@ -2313,7 +2600,8 @@ function convertEnum({ schema, name, nullable, type, parse }) {
2313
2600
  name
2314
2601
  });
2315
2602
  const nullInEnum = schema.enum.includes(null);
2316
- const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
2603
+ const isMember = createEnumMemberFilter(schema);
2604
+ const filteredValues = schema.enum.filter(isMember);
2317
2605
  if (nullInEnum && filteredValues.length === 0) return createNullNode(schema, name);
2318
2606
  const enumNullable = nullable || nullInEnum || void 0;
2319
2607
  const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
@@ -2332,8 +2620,9 @@ function convertEnum({ schema, name, nullable, type, parse }) {
2332
2620
  primitive,
2333
2621
  enumValues: uniqueValues
2334
2622
  });
2335
- const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
2336
- const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
2623
+ const memberSlots = (list) => Array.isArray(list) ? list.filter((_, index) => isMember(schema.enum[index])) : void 0;
2624
+ const rawEnumNames = extensionKey ? memberSlots(schema[extensionKey]) : void 0;
2625
+ const rawEnumDescriptions = descriptionKey ? memberSlots(schema[descriptionKey]) : void 0;
2337
2626
  const seenNames = /* @__PURE__ */ new Set();
2338
2627
  return createNode(ctx, {
2339
2628
  type: "enum",
@@ -2351,6 +2640,25 @@ function convertEnum({ schema, name, nullable, type, parse }) {
2351
2640
  });
2352
2641
  }
2353
2642
  /**
2643
+ * Converts an `enum` with a handled `format` into a union of its literal values. A named enum node would rename
2644
+ * the exported type (`Status` becomes `StatusKey`), so the union keeps the name the format type had.
2645
+ */
2646
+ function convertFormattedEnum(context) {
2647
+ const { schema, nullable } = context;
2648
+ const values = [...new Set(schema.enum.filter(createEnumMemberFilter(schema)))];
2649
+ return createNode({
2650
+ ...context,
2651
+ nullable: nullable || schema.enum.includes(null) || void 0
2652
+ }, {
2653
+ type: "union",
2654
+ members: values.map((value) => ast.factory.createSchema({
2655
+ type: "enum",
2656
+ primitive: getPrimitiveType(typeof value),
2657
+ enumValues: [value]
2658
+ }))
2659
+ });
2660
+ }
2661
+ /**
2354
2662
  * Converts a `type: 'string'` schema into a `StringSchemaNode`.
2355
2663
  */
2356
2664
  function convertString(context) {
@@ -2527,6 +2835,24 @@ function convertArray({ schema, name, nullable, defaultValue, options, parse })
2527
2835
  }
2528
2836
  // src/emit/parseSchema.ts
2529
2837
  /**
2838
+ * Whether the `format` rule owns the schema: a handled format, and for a date format a `dateType` that is not `false`.
2839
+ */
2840
+ function matchesFormat({ schema, options }) {
2841
+ if (!schema.format) return false;
2842
+ if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") return resolveDateTypeValue(options.dateType, schema.format) !== false;
2843
+ return isHandledFormat(schema.format);
2844
+ }
2845
+ /**
2846
+ * Whether the format prints as a plain `string` or `number`, so narrowing it to literal values cannot break callers.
2847
+ * `Date`, `bigint` and `Blob` outputs keep the format.
2848
+ */
2849
+ function formatPrintsPlainType({ schema, options, type }) {
2850
+ const format = schema.format;
2851
+ if (format === "date-time" || format === "date" || format === "time") return resolveDateTypeValue(options.dateType, format) !== "date";
2852
+ if ((format === "int64" || format === "uint64") && type !== "string") return options.integerType !== "bigint";
2853
+ return formatMap[format] !== "blob";
2854
+ }
2855
+ /**
2530
2856
  * Ordered schema rule table. Order is significant: composition keywords (`not`, `$ref`, `allOf`,
2531
2857
  * `oneOf`/`anyOf`) take precedence over `const` (except a `$ref` with a sibling `const`, where the
2532
2858
  * `const` wins), which takes precedence over a genuine OAS 3.1 multi-type array (more than one
@@ -2564,11 +2890,11 @@ const schemaRules = [
2564
2890
  convert: convertMultiType
2565
2891
  },
2566
2892
  {
2567
- match: ({ schema, options }) => {
2568
- if (!schema.format) return false;
2569
- if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") return resolveDateTypeValue(options.dateType, schema.format) !== false;
2570
- return isHandledFormat(schema.format);
2571
- },
2893
+ match: (ctx) => ctx.type !== "array" && !!ctx.schema.enum?.some((value) => value !== null) && matchesFormat(ctx) && formatPrintsPlainType(ctx),
2894
+ convert: convertFormattedEnum
2895
+ },
2896
+ {
2897
+ match: matchesFormat,
2572
2898
  convert: convertFormat
2573
2899
  },
2574
2900
  {
@@ -2578,6 +2904,10 @@ const schemaRules = [
2578
2904
  primitive: "string"
2579
2905
  })
2580
2906
  },
2907
+ {
2908
+ match: ({ schema, type }) => !type && !schema.enum?.length && (schema.contentMediaType !== void 0 || schema.contentSchema !== void 0),
2909
+ convert: convertString
2910
+ },
2581
2911
  {
2582
2912
  match: ({ schema, type }) => !type && (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0),
2583
2913
  convert: convertString
@@ -2872,7 +3202,7 @@ const locate = (source, location) => {
2872
3202
  */
2873
3203
  const adapterOasName = "oas";
2874
3204
  /**
2875
- * Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.1 specifications. Reads the
3205
+ * Default Kubb adapter for OpenAPI 2.0, 3.0, 3.1, and 3.2 specifications. Reads the
2876
3206
  * spec from `input` (a file path, URL, inline content, or parsed object), validates
2877
3207
  * it, resolves the base URL, and converts every schema and operation into the
2878
3208
  * universal AST that every downstream plugin consumes.
@@ -2946,6 +3276,7 @@ const adapterOas = defineExtension({
2946
3276
  const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
2947
3277
  const operationNodes = getOperations(document, refs).map((operation) => parseOperation({
2948
3278
  operation,
3279
+ document,
2949
3280
  refs,
2950
3281
  contentType,
2951
3282
  options: parserOptions,