@kubb/adapter-oas 5.14.0 → 5.14.2

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, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
2
+ import { Diagnostics, ast, childName, enumPrimitive, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
3
3
  import { access, readFile } from "node:fs/promises";
4
4
  import { upgrade } from "@scalar/openapi-upgrader";
5
5
  import { bundle } from "api-ref-bundler";
@@ -20,7 +20,7 @@ var compatibility = {
20
20
  };
21
21
  //#endregion
22
22
  //#region package.json
23
- var version = "5.14.0";
23
+ var version = "5.14.2";
24
24
  //#endregion
25
25
  //#region src/constants.ts
26
26
  /**
@@ -371,6 +371,198 @@ async function read(path) {
371
371
  return readFile(path, { encoding: "utf8" });
372
372
  }
373
373
  //#endregion
374
+ //#region src/oas.ts
375
+ /**
376
+ * Returns `true` when a schema should be treated as nullable.
377
+ *
378
+ * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
379
+ * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
380
+ */
381
+ function isNullable(schema) {
382
+ if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
383
+ const schemaType = schema?.type;
384
+ if (schemaType === "null") return true;
385
+ if (Array.isArray(schemaType)) return schemaType.includes("null");
386
+ return false;
387
+ }
388
+ /**
389
+ * Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
390
+ */
391
+ function pickExtensions(source) {
392
+ const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
393
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
394
+ }
395
+ /**
396
+ * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
397
+ */
398
+ function isReference(obj) {
399
+ return !!obj && typeof obj === "object" && "$ref" in obj;
400
+ }
401
+ /**
402
+ * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
403
+ * excluding the Swagger 2 string form.
404
+ */
405
+ function isDiscriminator(obj) {
406
+ const record = obj;
407
+ return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
408
+ }
409
+ /**
410
+ * Returns `true` when a schema is a raw binary payload.
411
+ */
412
+ function isBinary(schema) {
413
+ return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
414
+ }
415
+ /**
416
+ * Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
417
+ * when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
418
+ * `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
419
+ * media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
420
+ * `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
421
+ * instead of a blob.
422
+ */
423
+ function getBinaryFallbackSchema(mediaType, schema) {
424
+ if (!mediaType || isJsonMimeType(mediaType)) return void 0;
425
+ if (schema && Object.keys(schema).length > 0) return void 0;
426
+ return {
427
+ type: "string",
428
+ contentMediaType: "application/octet-stream"
429
+ };
430
+ }
431
+ /**
432
+ * MIME type fragments that mark a media type as JSON-like.
433
+ *
434
+ * A content type is JSON when it contains any of these substrings. The `+json` entry catches
435
+ * structured-syntax suffixes such as `application/vnd.api+json`.
436
+ */
437
+ const jsonMimeFragments = [
438
+ "application/json",
439
+ "application/x-json",
440
+ "text/json",
441
+ "text/x-json",
442
+ "+json"
443
+ ];
444
+ /**
445
+ * Returns `true` when a media type string is JSON-like.
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * isJsonMimeType('application/json') // true
450
+ * isJsonMimeType('application/vnd.api+json') // true
451
+ * isJsonMimeType('multipart/form-data') // false
452
+ * ```
453
+ */
454
+ function isJsonMimeType(mimeType) {
455
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
456
+ }
457
+ //#endregion
458
+ //#region src/refs.ts
459
+ /**
460
+ * Creates the `$ref` resolution service for one document. One pointer walk backs every method.
461
+ *
462
+ * @example
463
+ * ```ts
464
+ * const refs = createRefs(document)
465
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet')
466
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
467
+ * refs.resolveNode('#/components/schemas/Pet', parseSchema)
468
+ * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
469
+ * refs.derefKeepingRef(parameter)
470
+ * ```
471
+ */
472
+ function createRefs(document) {
473
+ const pointerCache = /* @__PURE__ */ new Map();
474
+ const resolvedNodeCache = /* @__PURE__ */ new Map();
475
+ const resolvingRefs = /* @__PURE__ */ new Set();
476
+ function walkPointer($ref) {
477
+ const trimmed = $ref.trim();
478
+ if (trimmed === "" || !trimmed.startsWith("#")) return {
479
+ applicable: false,
480
+ value: null
481
+ };
482
+ const pointer = trimmed.substring(1);
483
+ if (pointerCache.has(pointer)) return {
484
+ applicable: true,
485
+ value: pointerCache.get(pointer)
486
+ };
487
+ const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
488
+ if (current) pointerCache.set(pointer, current);
489
+ return {
490
+ applicable: true,
491
+ value: current ?? null
492
+ };
493
+ }
494
+ /**
495
+ * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
496
+ * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
497
+ * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
498
+ * for a speculative lookup where a missing ref is not an error.
499
+ */
500
+ function resolve(refPath, options) {
501
+ const { applicable, value } = walkPointer(refPath);
502
+ if (!applicable || value || options?.report === false) return value;
503
+ const diagnostic = {
504
+ code: Diagnostics.code.refNotFound,
505
+ severity: "error",
506
+ message: `Could not find a definition for ${refPath}.`,
507
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
508
+ location: {
509
+ kind: "schema",
510
+ pointer: refPath,
511
+ ref: refPath
512
+ }
513
+ };
514
+ if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
515
+ return null;
516
+ }
517
+ /**
518
+ * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
519
+ * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
520
+ * resolved (e.g. a minimal document in a unit test).
521
+ */
522
+ function resolveNode(refPath, parse) {
523
+ if (resolvingRefs.has(refPath)) return null;
524
+ if (!resolvedNodeCache.has(refPath)) {
525
+ let resolved = null;
526
+ try {
527
+ const referenced = resolve(refPath);
528
+ if (referenced) {
529
+ resolvingRefs.add(refPath);
530
+ resolved = parse({ schema: referenced });
531
+ resolvingRefs.delete(refPath);
532
+ }
533
+ } catch {}
534
+ resolvedNodeCache.set(refPath, resolved);
535
+ }
536
+ return resolvedNodeCache.get(refPath) ?? null;
537
+ }
538
+ /**
539
+ * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
540
+ * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
541
+ * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
542
+ * returned as-is.
543
+ */
544
+ function deref(value) {
545
+ if (!isReference(value)) return value ? value : null;
546
+ const resolved = resolve(value.$ref);
547
+ return resolved && !isReference(resolved) ? resolved : null;
548
+ }
549
+ /** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
550
+ function derefKeepingRef(value) {
551
+ if (isReference(value)) return {
552
+ ...value,
553
+ ...resolve(value.$ref),
554
+ $ref: value.$ref
555
+ };
556
+ return value;
557
+ }
558
+ return {
559
+ resolve,
560
+ resolveNode,
561
+ deref,
562
+ derefKeepingRef
563
+ };
564
+ }
565
+ //#endregion
374
566
  //#region src/load/source.ts
375
567
  const urlRegExp = /^https?:\/+/i;
376
568
  /**
@@ -491,6 +683,22 @@ function hasExternalRef(node) {
491
683
  return Object.values(node).some(hasExternalRef);
492
684
  }
493
685
  /**
686
+ * Replaces each `$ref` into `#/paths/...` with a copy of its target, in place.
687
+ *
688
+ * The Swagger 2.0 upgrade moves `responses/200/schema` under `content`, so a ref to the old location
689
+ * would no longer resolve. A ref that points back into itself is kept, since inlining it never ends.
690
+ */
691
+ function inlinePathRefs(node, resolve, visiting = []) {
692
+ if (!node || typeof node !== "object") return node;
693
+ const ref = node.$ref;
694
+ if (typeof ref === "string" && ref.startsWith("#/paths/") && !visiting.includes(ref)) {
695
+ const target = resolve(ref);
696
+ return target && typeof target === "object" ? inlinePathRefs(structuredClone(target), resolve, [...visiting, ref]) : node;
697
+ }
698
+ for (const [key, value] of Object.entries(node)) node[key] = inlinePathRefs(value, resolve, visiting);
699
+ return node;
700
+ }
701
+ /**
494
702
  * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
495
703
  *
496
704
  * External file schemas are hoisted into named `components.schemas` entries, so a property
@@ -548,6 +756,17 @@ async function parseFromConfig(source) {
548
756
  await assertInputExists(source.path);
549
757
  return bundleDocument(source.path);
550
758
  })();
759
+ if (raw && typeof raw === "object") {
760
+ const document = raw;
761
+ for (const key of ["swagger", "openapi"]) {
762
+ const version = document[key];
763
+ if (typeof version === "number") document[key] = Number.isInteger(version) ? version.toFixed(1) : String(version);
764
+ }
765
+ }
766
+ if (raw && typeof raw === "object" && "swagger" in raw) {
767
+ const refs = createRefs(raw);
768
+ inlinePathRefs(raw, (ref) => refs.resolve(ref, { report: false }));
769
+ }
551
770
  try {
552
771
  return upgrade(raw, "3.1");
553
772
  } catch (error) {
@@ -644,90 +863,6 @@ async function validateDocumentInWorker(document) {
644
863
  if (!message.valid) reportValidationErrors(message);
645
864
  }
646
865
  //#endregion
647
- //#region src/oas.ts
648
- /**
649
- * Returns `true` when a schema should be treated as nullable.
650
- *
651
- * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
652
- * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
653
- */
654
- function isNullable(schema) {
655
- if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
656
- const schemaType = schema?.type;
657
- if (schemaType === "null") return true;
658
- if (Array.isArray(schemaType)) return schemaType.includes("null");
659
- return false;
660
- }
661
- /**
662
- * Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
663
- */
664
- function pickExtensions(source) {
665
- const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
666
- return entries.length > 0 ? Object.fromEntries(entries) : void 0;
667
- }
668
- /**
669
- * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
670
- */
671
- function isReference(obj) {
672
- return !!obj && typeof obj === "object" && "$ref" in obj;
673
- }
674
- /**
675
- * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
676
- * excluding the Swagger 2 string form.
677
- */
678
- function isDiscriminator(obj) {
679
- const record = obj;
680
- return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
681
- }
682
- /**
683
- * Returns `true` when a schema is a raw binary payload.
684
- */
685
- function isBinary(schema) {
686
- return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
687
- }
688
- /**
689
- * Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
690
- * when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
691
- * `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
692
- * media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
693
- * `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
694
- * instead of a blob.
695
- */
696
- function getBinaryFallbackSchema(mediaType, schema) {
697
- if (!mediaType || isJsonMimeType(mediaType)) return void 0;
698
- if (schema && Object.keys(schema).length > 0) return void 0;
699
- return {
700
- type: "string",
701
- contentMediaType: "application/octet-stream"
702
- };
703
- }
704
- /**
705
- * MIME type fragments that mark a media type as JSON-like.
706
- *
707
- * A content type is JSON when it contains any of these substrings. The `+json` entry catches
708
- * structured-syntax suffixes such as `application/vnd.api+json`.
709
- */
710
- const jsonMimeFragments = [
711
- "application/json",
712
- "application/x-json",
713
- "text/json",
714
- "text/x-json",
715
- "+json"
716
- ];
717
- /**
718
- * Returns `true` when a media type string is JSON-like.
719
- *
720
- * @example
721
- * ```ts
722
- * isJsonMimeType('application/json') // true
723
- * isJsonMimeType('application/vnd.api+json') // true
724
- * isJsonMimeType('multipart/form-data') // false
725
- * ```
726
- */
727
- function isJsonMimeType(mimeType) {
728
- return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
729
- }
730
- //#endregion
731
866
  //#region src/model/components.ts
732
867
  /**
733
868
  * Extracts the inline schema from a media-type `content` map, with the media type it sits under.
@@ -1156,6 +1291,28 @@ function getParameters({ operation, refs }) {
1156
1291
  for (const entry of [...pathLevelParams, ...operationParams]) if (entry.param.name && entry.param.in) paramMap.set(`${entry.param.in}:${entry.param.name}`, entry);
1157
1292
  return Array.from(paramMap.values());
1158
1293
  }
1294
+ const PARAMETER_STYLES = /* @__PURE__ */ new Set([
1295
+ "matrix",
1296
+ "label",
1297
+ "form",
1298
+ "simple",
1299
+ "spaceDelimited",
1300
+ "pipeDelimited",
1301
+ "deepObject"
1302
+ ]);
1303
+ /** The `encoding` of a media type, reduced to the entries that set a field Kubb carries. A `style` outside the OpenAPI set is dropped. */
1304
+ function getEncoding(encoding) {
1305
+ const entries = Object.entries(encoding ?? {}).flatMap(([property, { contentType, style, explode, allowReserved }]) => {
1306
+ const node = {
1307
+ ...contentType !== void 0 ? { contentType } : {},
1308
+ ...style !== void 0 && PARAMETER_STYLES.has(style) ? { style } : {},
1309
+ ...explode !== void 0 ? { explode } : {},
1310
+ ...allowReserved !== void 0 ? { allowReserved } : {}
1311
+ };
1312
+ return Object.keys(node).length ? [[property, node]] : [];
1313
+ });
1314
+ return entries.length ? Object.fromEntries(entries) : void 0;
1315
+ }
1159
1316
  /** Schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON entry. */
1160
1317
  function getBodySchema({ content, contentType, refs }) {
1161
1318
  const entry = contentType ? content?.[contentType] : void 0;
@@ -1210,7 +1367,7 @@ function parseParameter({ param, pointer, parentName, options, parseSchema }) {
1210
1367
  name: schemaName
1211
1368
  }) : ast.factory.createSchema({ type: options.unknownType });
1212
1369
  if (rawSchema) scanSchema(schema, contentType ? `${pointer}/content/${escapePointerToken(contentType)}/schema` : `${pointer}/schema`);
1213
- const style = param.style;
1370
+ const style = param.style !== void 0 && PARAMETER_STYLES.has(param.style) ? param.style : void 0;
1214
1371
  const explode = param.explode;
1215
1372
  const allowReserved = param.allowReserved;
1216
1373
  return ast.factory.createParameter({
@@ -1263,10 +1420,12 @@ function parseOperation({ operation, ...ctx }) {
1263
1420
  name: requestBodyName
1264
1421
  });
1265
1422
  scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}/schema`);
1423
+ const encoding = getEncoding(body?.content?.[ct]?.encoding);
1266
1424
  return [ast.factory.createContent({
1267
1425
  contentType: ct,
1268
1426
  schema: ast.optionality(node, bodyRequired),
1269
- keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
1427
+ keysToOmit: collectPropertyKeysByFlag(schema, "readOnly"),
1428
+ ...encoding && { encoding }
1270
1429
  })];
1271
1430
  });
1272
1431
  const requestBody = content.length > 0 || body?.description ? {
@@ -1363,6 +1522,42 @@ function createNode({ schema, name, nullable, defaultValue }, extras) {
1363
1522
  //#endregion
1364
1523
  //#region src/emit/discriminator/preserve.ts
1365
1524
  /**
1525
+ * Turns string mapping keys into the discriminator property's type when they parse cleanly.
1526
+ */
1527
+ function typedKeys({ keys, propertyType }) {
1528
+ return keys.map((key) => {
1529
+ if (propertyType === "integer" || propertyType === "number") {
1530
+ const value = Number(key);
1531
+ return key.trim() !== "" && Number.isFinite(value) ? value : key;
1532
+ }
1533
+ if (propertyType === "boolean") {
1534
+ const lower = key.toLowerCase();
1535
+ return lower === "true" ? true : lower === "false" ? false : key;
1536
+ }
1537
+ return key;
1538
+ });
1539
+ }
1540
+ /**
1541
+ * Reads the declared `type` of `propertyName` on a schema, following `$ref`s and `allOf` members.
1542
+ * A nullable array such as `['integer', 'null']` reads as its one non-null type.
1543
+ */
1544
+ function propertyTypeOf({ schema, propertyName, refs, seen = /* @__PURE__ */ new Set() }) {
1545
+ const deref = (value) => value && isReference(value) ? refs.resolve(value.$ref, { report: false }) : value;
1546
+ if (schema && isReference(schema)) {
1547
+ if (seen.has(schema.$ref)) return void 0;
1548
+ seen.add(schema.$ref);
1549
+ }
1550
+ const resolved = deref(schema);
1551
+ const type = deref(resolved?.properties?.[propertyName])?.type ?? resolved?.allOf?.map((member) => propertyTypeOf({
1552
+ schema: member,
1553
+ propertyName,
1554
+ refs,
1555
+ seen
1556
+ })).find(Boolean);
1557
+ const nonNull = Array.isArray(type) ? type.filter((t) => t !== "null") : [];
1558
+ return nonNull.length === 1 ? nonNull[0] : type;
1559
+ }
1560
+ /**
1366
1561
  * Creates a single-property object schema used as a discriminator literal.
1367
1562
  *
1368
1563
  * @example
@@ -1379,7 +1574,7 @@ function createDiscriminantNode({ propertyName, values }) {
1379
1574
  name: propertyName,
1380
1575
  schema: ast.factory.createSchema({
1381
1576
  type: "enum",
1382
- primitive: "string",
1577
+ primitive: enumPrimitive(values),
1383
1578
  enumValues: values
1384
1579
  }),
1385
1580
  required: true
@@ -1394,21 +1589,61 @@ function createDiscriminantNode({ propertyName, values }) {
1394
1589
  *
1395
1590
  * @example
1396
1591
  * ```ts
1397
- * findDiscriminators({ dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // ['dog', 'hound']
1592
+ * findDiscriminators({ mapping: { dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, ref: '#/components/schemas/Dog' }) // ['dog', 'hound']
1398
1593
  * ```
1399
1594
  */
1400
- function findDiscriminators(mapping, ref) {
1595
+ function findDiscriminators({ mapping, ref }) {
1401
1596
  if (!mapping || !ref) return [];
1402
1597
  return Object.entries(mapping).filter(([, value]) => value === ref).map(([key]) => key);
1403
1598
  }
1404
1599
  /**
1600
+ * Finds the discriminator a schema declares itself or through an inline `allOf` member.
1601
+ */
1602
+ function getDiscriminator(schema) {
1603
+ return (isDiscriminator(schema) ? schema : schema.allOf?.find(isDiscriminator))?.discriminator;
1604
+ }
1605
+ /**
1606
+ * Lists the mapping keys of a discriminator plus the keys of the nested discriminators its mapped
1607
+ * schemas declare on the same property, so a root `$type` accepts values from every level.
1608
+ */
1609
+ function getMappingKeys({ discriminator: { propertyName, mapping }, refs, seen = /* @__PURE__ */ new Set() }) {
1610
+ return Object.entries(mapping ?? {}).flatMap(([key, target]) => {
1611
+ if (seen.has(target)) return [key];
1612
+ seen.add(target);
1613
+ const resolved = refs.resolve(target, { report: false });
1614
+ const nested = resolved && getDiscriminator(resolved);
1615
+ return nested?.propertyName === propertyName ? [key, ...getMappingKeys({
1616
+ discriminator: nested,
1617
+ refs,
1618
+ seen
1619
+ })] : [key];
1620
+ });
1621
+ }
1622
+ /**
1623
+ * Drops the discriminator from an inline `allOf` member when a `$ref` sibling already declares one on
1624
+ * the same property. That mapping names the schema's own children, and keeping it pins the property
1625
+ * to those keys next to the parent's, which leaves `never`.
1626
+ */
1627
+ function removeInheritedDiscriminator({ members, refs }) {
1628
+ const inherited = new Set(members.flatMap((member) => {
1629
+ const parent = isReference(member) ? refs.resolve(member.$ref, { report: false }) : null;
1630
+ return parent && getDiscriminator(parent)?.propertyName || [];
1631
+ }));
1632
+ return members.map((member) => {
1633
+ if (isReference(member) || !isDiscriminator(member) || member.oneOf || member.anyOf) return member;
1634
+ if (!inherited.has(member.discriminator.propertyName)) return member;
1635
+ const { discriminator: _discriminator, ...rest } = member;
1636
+ return rest;
1637
+ });
1638
+ }
1639
+ /**
1405
1640
  * Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
1406
1641
  * node with either the shared-properties slice carrying that value, or a synthetic discriminant
1407
1642
  * literal. The referenced child schema's own definition is left untouched — narrowing happens only
1408
1643
  * at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
1409
1644
  * which additionally patches the child schema's own definition in a post-pass).
1410
1645
  */
1411
- function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, name, refs }) {
1646
+ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parentType, parse, name, refs }) {
1412
1647
  function pickDiscriminatorPropertyNode(node, propertyName) {
1413
1648
  const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1414
1649
  if (!discriminatorProperty) return null;
@@ -1444,9 +1679,20 @@ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode,
1444
1679
  }
1445
1680
  return unionMembers.map((s) => {
1446
1681
  const ref = isReference(s) ? s.$ref : void 0;
1447
- const mappedValues = findDiscriminators(discriminator?.mapping, ref);
1682
+ const mappedValues = findDiscriminators({
1683
+ mapping: discriminator?.mapping,
1684
+ ref
1685
+ });
1448
1686
  const implicitValue = mappedValues.length ? null : implicitDiscriminantValue(s);
1449
- const discriminatorValues = mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [];
1687
+ const propertyType = propertyTypeOf({
1688
+ schema: s,
1689
+ propertyName: discriminator?.propertyName ?? "",
1690
+ refs
1691
+ }) ?? parentType;
1692
+ const discriminatorValues = typedKeys({
1693
+ keys: mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [],
1694
+ propertyType
1695
+ });
1450
1696
  const memberNode = parse({
1451
1697
  schema: s,
1452
1698
  name
@@ -1478,21 +1724,34 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
1478
1724
  members: allOfMembers.filter((item) => {
1479
1725
  if (!isReference(item) || !name) return true;
1480
1726
  const deref = refs.resolve(item.$ref);
1481
- if (!deref || !isDiscriminator(deref)) return true;
1482
- const parentUnion = deref.oneOf ?? deref.anyOf;
1483
- if (!parentUnion) return true;
1727
+ const discriminator = deref && getDiscriminator(deref);
1728
+ const parentUnion = deref?.oneOf ?? deref?.anyOf;
1729
+ if (!deref || !discriminator || isDiscriminator(deref) && !parentUnion) return true;
1484
1730
  const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1485
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1486
- const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1487
- if (inOneOf || inMapping) {
1488
- const values = findDiscriminators(deref.discriminator.mapping, childRef);
1489
- if (values.length) discriminantValues.push({
1490
- propertyName: deref.discriminator.propertyName,
1491
- values
1731
+ const values = findDiscriminators({
1732
+ mapping: discriminator.mapping,
1733
+ ref: childRef
1734
+ });
1735
+ if (values.length) {
1736
+ const { propertyName } = discriminator;
1737
+ const propertyType = propertyTypeOf({
1738
+ schema: deref,
1739
+ propertyName,
1740
+ refs
1741
+ }) ?? propertyTypeOf({
1742
+ schema: { $ref: childRef },
1743
+ propertyName,
1744
+ refs
1745
+ });
1746
+ discriminantValues.push({
1747
+ propertyName,
1748
+ values: typedKeys({
1749
+ keys: values,
1750
+ propertyType
1751
+ })
1492
1752
  });
1493
- return false;
1494
1753
  }
1495
- return true;
1754
+ return !(parentUnion && (values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
1496
1755
  }),
1497
1756
  discriminantValues
1498
1757
  };
@@ -1619,10 +1878,9 @@ function resolveUnionMembers(members, siblingProperties) {
1619
1878
  });
1620
1879
  }
1621
1880
  /**
1622
- * Enriches any `oneOf` or `anyOf` branches on a schema object with properties from sibling schemas.
1881
+ * Enriches a schema object, and any `oneOf` or `anyOf` branches on it, with properties from sibling schemas.
1623
1882
  */
1624
1883
  function resolveUnionSchema(schema, siblingProperties) {
1625
- if (!schema.oneOf && !schema.anyOf) return schema;
1626
1884
  const next = { ...resolveUnionMembers([schema], siblingProperties)[0] ?? schema };
1627
1885
  if (schema.oneOf) next.oneOf = resolveUnionMembers(schema.oneOf, siblingProperties);
1628
1886
  if (schema.anyOf) next.anyOf = resolveUnionMembers(schema.anyOf, siblingProperties);
@@ -1646,6 +1904,17 @@ function convertRef({ schema, name, nullable, defaultValue, document, parse, ref
1646
1904
  defaultValue
1647
1905
  };
1648
1906
  if (refPath && document.components && !refs.resolve(refPath, { report: false })) return createNode(ctx, { type: "unknown" });
1907
+ if (refPath && !refPath.startsWith("#/components/schemas/") && resolvedSchema) return ast.factory.createSchema({
1908
+ ...resolvedSchema,
1909
+ title: schema.title ?? resolvedSchema.title,
1910
+ description: schema.description ?? resolvedSchema.description,
1911
+ deprecated: schema.deprecated ?? resolvedSchema.deprecated,
1912
+ readOnly: schema.readOnly ?? resolvedSchema.readOnly,
1913
+ writeOnly: schema.writeOnly ?? resolvedSchema.writeOnly,
1914
+ default: defaultValue ?? resolvedSchema.default,
1915
+ name: name ?? resolvedSchema.name,
1916
+ nullable: nullable ?? resolvedSchema.nullable
1917
+ });
1649
1918
  const targetName = renames?.get(schema.$ref);
1650
1919
  return createNode(ctx, {
1651
1920
  type: "ref",
@@ -1677,9 +1946,41 @@ function convertNot({ schema, name, nullable, defaultValue, parse }) {
1677
1946
  });
1678
1947
  }
1679
1948
  /**
1949
+ * Keywords that describe a schema or conditionally constrain it, but add no structure.
1950
+ */
1951
+ const annotationKeys = /* @__PURE__ */ new Set([
1952
+ "description",
1953
+ "title",
1954
+ "example",
1955
+ "examples",
1956
+ "default",
1957
+ "deprecated",
1958
+ "readOnly",
1959
+ "writeOnly",
1960
+ "if",
1961
+ "then",
1962
+ "else"
1963
+ ]);
1964
+ /**
1965
+ * Moves `allOf` members that hold only annotations or `if`/`then`/`else` onto the parent, so they do
1966
+ * not become `unknown`. The parent's own values win, and the conditional keywords are not enforced.
1967
+ */
1968
+ function mergeAnnotations(schema) {
1969
+ const members = schema.allOf;
1970
+ const isAnnotation = (member) => !isReference(member) && Object.keys(member).every((key) => annotationKeys.has(key));
1971
+ const annotations = members.filter(isAnnotation);
1972
+ const rest = members.filter((member) => !isAnnotation(member));
1973
+ return annotations.length && rest.length ? {
1974
+ ...Object.assign({}, ...annotations),
1975
+ ...schema,
1976
+ allOf: rest
1977
+ } : schema;
1978
+ }
1979
+ /**
1680
1980
  * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
1681
1981
  */
1682
- function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
1982
+ function convertAllOf({ schema: original, name, nullable, defaultValue, parse, refs }) {
1983
+ const schema = mergeAnnotations(original);
1683
1984
  if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
1684
1985
  const [memberSchema] = schema.allOf;
1685
1986
  const memberNode = parse({
@@ -1705,7 +2006,10 @@ function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
1705
2006
  });
1706
2007
  }
1707
2008
  const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
1708
- allOfMembers: schema.allOf,
2009
+ allOfMembers: removeInheritedDiscriminator({
2010
+ members: schema.allOf,
2011
+ refs
2012
+ }),
1709
2013
  name,
1710
2014
  refs
1711
2015
  });
@@ -1785,6 +2089,11 @@ function convertUnion(context) {
1785
2089
  unionMembers,
1786
2090
  discriminator,
1787
2091
  sharedPropertiesNode,
2092
+ parentType: discriminator && propertyTypeOf({
2093
+ schema,
2094
+ propertyName: discriminator.propertyName,
2095
+ refs
2096
+ }),
1788
2097
  parse,
1789
2098
  name,
1790
2099
  refs
@@ -2048,7 +2357,7 @@ function nameEnums(node, options) {
2048
2357
  /**
2049
2358
  * Converts an object-like schema into an `ObjectSchemaNode`.
2050
2359
  */
2051
- function convertObject({ schema, name, nullable, defaultValue, options, parse }) {
2360
+ function convertObject({ schema, name, nullable, defaultValue, options, parse, refs }) {
2052
2361
  const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
2053
2362
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
2054
2363
  const resolvedPropSchema = propSchema;
@@ -2065,7 +2374,7 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
2065
2374
  name: propName,
2066
2375
  schema: {
2067
2376
  ...schemaNode,
2068
- nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
2377
+ nullable: schemaNode.type === "null" ? void 0 : propNullable || schemaNode.nullable || void 0
2069
2378
  },
2070
2379
  required
2071
2380
  });
@@ -2105,7 +2414,17 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
2105
2414
  });
2106
2415
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
2107
2416
  const discPropName = schema.discriminator.propertyName;
2108
- const values = Object.keys(schema.discriminator.mapping);
2417
+ const values = typedKeys({
2418
+ keys: getMappingKeys({
2419
+ discriminator: schema.discriminator,
2420
+ refs
2421
+ }),
2422
+ propertyType: propertyTypeOf({
2423
+ schema,
2424
+ propertyName: discPropName,
2425
+ refs
2426
+ })
2427
+ });
2109
2428
  const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
2110
2429
  return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
2111
2430
  propertyName: discPropName,
@@ -2163,8 +2482,9 @@ function convertArray({ schema, name, nullable, defaultValue, options, parse })
2163
2482
  //#region src/emit/parseSchema.ts
2164
2483
  /**
2165
2484
  * Ordered schema rule table. Order is significant: composition keywords (`not`, `$ref`, `allOf`,
2166
- * `oneOf`/`anyOf`) take precedence over `const`, which takes precedence over a genuine OAS 3.1
2167
- * multi-type array (more than one non-`null` type), which takes precedence over `format`/`type`.
2485
+ * `oneOf`/`anyOf`) take precedence over `const` (except a `$ref` with a sibling `const`, where the
2486
+ * `const` wins), which takes precedence over a genuine OAS 3.1 multi-type array (more than one
2487
+ * non-`null` type), which takes precedence over `format`/`type`.
2168
2488
  * A multi-type array must split into its per-type members before `format` runs, otherwise
2169
2489
  * `format` collapses the whole schema to one type (e.g. `type: ['null', 'integer', 'string'],
2170
2490
  * format: 'int32'` would drop `string` and emit just `integer`); each split-off member still
@@ -2178,7 +2498,7 @@ const schemaRules = [
2178
2498
  convert: convertNot
2179
2499
  },
2180
2500
  {
2181
- match: ({ schema }) => isReference(schema),
2501
+ match: ({ schema }) => isReference(schema) && !("const" in schema && schema.const !== void 0),
2182
2502
  convert: convertRef
2183
2503
  },
2184
2504
  {
@@ -2346,114 +2666,6 @@ function refPromotedEnums(node, promoted) {
2346
2666
  } });
2347
2667
  }
2348
2668
  //#endregion
2349
- //#region src/refs.ts
2350
- /**
2351
- * Creates the `$ref` resolution service for one document. One pointer walk backs every method.
2352
- *
2353
- * @example
2354
- * ```ts
2355
- * const refs = createRefs(document)
2356
- * refs.resolve<SchemaObject>('#/components/schemas/Pet')
2357
- * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
2358
- * refs.resolveNode('#/components/schemas/Pet', parseSchema)
2359
- * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
2360
- * refs.derefKeepingRef(parameter)
2361
- * ```
2362
- */
2363
- function createRefs(document) {
2364
- const pointerCache = /* @__PURE__ */ new Map();
2365
- const resolvedNodeCache = /* @__PURE__ */ new Map();
2366
- const resolvingRefs = /* @__PURE__ */ new Set();
2367
- function walkPointer($ref) {
2368
- const trimmed = $ref.trim();
2369
- if (trimmed === "" || !trimmed.startsWith("#")) return {
2370
- applicable: false,
2371
- value: null
2372
- };
2373
- const pointer = trimmed.substring(1);
2374
- if (pointerCache.has(pointer)) return {
2375
- applicable: true,
2376
- value: pointerCache.get(pointer)
2377
- };
2378
- const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
2379
- if (current) pointerCache.set(pointer, current);
2380
- return {
2381
- applicable: true,
2382
- value: current ?? null
2383
- };
2384
- }
2385
- /**
2386
- * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
2387
- * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
2388
- * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
2389
- * for a speculative lookup where a missing ref is not an error.
2390
- */
2391
- function resolve(refPath, options) {
2392
- const { applicable, value } = walkPointer(refPath);
2393
- if (!applicable || value || options?.report === false) return value;
2394
- const diagnostic = {
2395
- code: Diagnostics.code.refNotFound,
2396
- severity: "error",
2397
- message: `Could not find a definition for ${refPath}.`,
2398
- help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
2399
- location: {
2400
- kind: "schema",
2401
- pointer: refPath,
2402
- ref: refPath
2403
- }
2404
- };
2405
- if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
2406
- return null;
2407
- }
2408
- /**
2409
- * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
2410
- * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
2411
- * resolved (e.g. a minimal document in a unit test).
2412
- */
2413
- function resolveNode(refPath, parse) {
2414
- if (resolvingRefs.has(refPath)) return null;
2415
- if (!resolvedNodeCache.has(refPath)) {
2416
- let resolved = null;
2417
- try {
2418
- const referenced = resolve(refPath);
2419
- if (referenced) {
2420
- resolvingRefs.add(refPath);
2421
- resolved = parse({ schema: referenced });
2422
- resolvingRefs.delete(refPath);
2423
- }
2424
- } catch {}
2425
- resolvedNodeCache.set(refPath, resolved);
2426
- }
2427
- return resolvedNodeCache.get(refPath) ?? null;
2428
- }
2429
- /**
2430
- * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
2431
- * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
2432
- * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
2433
- * returned as-is.
2434
- */
2435
- function deref(value) {
2436
- if (!isReference(value)) return value ? value : null;
2437
- const resolved = resolve(value.$ref);
2438
- return resolved && !isReference(resolved) ? resolved : null;
2439
- }
2440
- /** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
2441
- function derefKeepingRef(value) {
2442
- if (isReference(value)) return {
2443
- ...value,
2444
- ...resolve(value.$ref),
2445
- $ref: value.$ref
2446
- };
2447
- return value;
2448
- }
2449
- return {
2450
- resolve,
2451
- resolveNode,
2452
- deref,
2453
- derefKeepingRef
2454
- };
2455
- }
2456
- //#endregion
2457
2669
  //#region src/sourceLocator.ts
2458
2670
  let cached;
2459
2671
  const toRange = ([start, end]) => ({
@@ -2687,7 +2899,7 @@ const adapterOas = defineExtension({
2687
2899
  };
2688
2900
  parsedByName.set(name, node);
2689
2901
  scanSchema(node);
2690
- if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2902
+ if (node.type === "ref" && node.name) refAliasMap.set(name, node);
2691
2903
  if (ast.narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
2692
2904
  if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2693
2905
  }
@@ -2702,6 +2914,36 @@ const adapterOas = defineExtension({
2702
2914
  }));
2703
2915
  const promotedEnums = enums === "root" ? collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas))) : null;
2704
2916
  if (promotedEnums) for (const name of promotedEnums.keys()) enumNames.push(name);
2917
+ const aliasTargetOf = (name) => {
2918
+ const node = refAliasMap.get(name);
2919
+ return node?.ref ? ast.refName(node.ref) : node?.name;
2920
+ };
2921
+ function isAliasCycle(name) {
2922
+ const seen = /* @__PURE__ */ new Set([name]);
2923
+ for (let next = aliasTargetOf(name); next; next = aliasTargetOf(next)) {
2924
+ if (next === name) return true;
2925
+ if (seen.has(next)) return false;
2926
+ seen.add(next);
2927
+ }
2928
+ return false;
2929
+ }
2930
+ function createCycleNode(name, pointer) {
2931
+ Diagnostics.report({
2932
+ code: Diagnostics.code.aliasCycle,
2933
+ severity: "warning",
2934
+ message: `${name} only refers to other schemas that lead back to it, so it is resolved to unknown.`,
2935
+ help: "Give one of the schemas in the loop a real definition, or remove the `$ref` that closes it.",
2936
+ location: {
2937
+ kind: "schema",
2938
+ pointer: pointer ?? `#/components/schemas/${name}`
2939
+ }
2940
+ });
2941
+ return ast.factory.createSchema({
2942
+ type: "unknown",
2943
+ name,
2944
+ pointer
2945
+ });
2946
+ }
2705
2947
  const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
2706
2948
  for (const name of Object.keys(schemas)) {
2707
2949
  const aliasTarget = refAliasMap.get(name)?.name;
@@ -2709,11 +2951,12 @@ const adapterOas = defineExtension({
2709
2951
  const parsed = parsedByName.get(name);
2710
2952
  const child = discriminatorChildMap?.get(name);
2711
2953
  const base = child ? patchDiscriminatorNode(parsed, child) : parsed;
2712
- const node = aliased ? {
2954
+ const resolved = aliased ? {
2713
2955
  ...aliased,
2714
2956
  name,
2715
2957
  pointer: parsed.pointer
2716
2958
  } : base;
2959
+ const node = isAliasCycle(name) ? createCycleNode(name, parsed.pointer) : resolved;
2717
2960
  schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
2718
2961
  }
2719
2962
  const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;