@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.cjs CHANGED
@@ -36,7 +36,7 @@ var compatibility = {
36
36
  };
37
37
  //#endregion
38
38
  //#region package.json
39
- var version = "5.14.0";
39
+ var version = "5.14.2";
40
40
  //#endregion
41
41
  //#region src/constants.ts
42
42
  /**
@@ -387,6 +387,198 @@ async function read(path) {
387
387
  return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
388
388
  }
389
389
  //#endregion
390
+ //#region src/oas.ts
391
+ /**
392
+ * Returns `true` when a schema should be treated as nullable.
393
+ *
394
+ * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
395
+ * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
396
+ */
397
+ function isNullable(schema) {
398
+ if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
399
+ const schemaType = schema?.type;
400
+ if (schemaType === "null") return true;
401
+ if (Array.isArray(schemaType)) return schemaType.includes("null");
402
+ return false;
403
+ }
404
+ /**
405
+ * Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
406
+ */
407
+ function pickExtensions(source) {
408
+ const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
409
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
410
+ }
411
+ /**
412
+ * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
413
+ */
414
+ function isReference(obj) {
415
+ return !!obj && typeof obj === "object" && "$ref" in obj;
416
+ }
417
+ /**
418
+ * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
419
+ * excluding the Swagger 2 string form.
420
+ */
421
+ function isDiscriminator(obj) {
422
+ const record = obj;
423
+ return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
424
+ }
425
+ /**
426
+ * Returns `true` when a schema is a raw binary payload.
427
+ */
428
+ function isBinary(schema) {
429
+ return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
430
+ }
431
+ /**
432
+ * Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
433
+ * when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
434
+ * `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
435
+ * media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
436
+ * `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
437
+ * instead of a blob.
438
+ */
439
+ function getBinaryFallbackSchema(mediaType, schema) {
440
+ if (!mediaType || isJsonMimeType(mediaType)) return void 0;
441
+ if (schema && Object.keys(schema).length > 0) return void 0;
442
+ return {
443
+ type: "string",
444
+ contentMediaType: "application/octet-stream"
445
+ };
446
+ }
447
+ /**
448
+ * MIME type fragments that mark a media type as JSON-like.
449
+ *
450
+ * A content type is JSON when it contains any of these substrings. The `+json` entry catches
451
+ * structured-syntax suffixes such as `application/vnd.api+json`.
452
+ */
453
+ const jsonMimeFragments = [
454
+ "application/json",
455
+ "application/x-json",
456
+ "text/json",
457
+ "text/x-json",
458
+ "+json"
459
+ ];
460
+ /**
461
+ * Returns `true` when a media type string is JSON-like.
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * isJsonMimeType('application/json') // true
466
+ * isJsonMimeType('application/vnd.api+json') // true
467
+ * isJsonMimeType('multipart/form-data') // false
468
+ * ```
469
+ */
470
+ function isJsonMimeType(mimeType) {
471
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
472
+ }
473
+ //#endregion
474
+ //#region src/refs.ts
475
+ /**
476
+ * Creates the `$ref` resolution service for one document. One pointer walk backs every method.
477
+ *
478
+ * @example
479
+ * ```ts
480
+ * const refs = createRefs(document)
481
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet')
482
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
483
+ * refs.resolveNode('#/components/schemas/Pet', parseSchema)
484
+ * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
485
+ * refs.derefKeepingRef(parameter)
486
+ * ```
487
+ */
488
+ function createRefs(document) {
489
+ const pointerCache = /* @__PURE__ */ new Map();
490
+ const resolvedNodeCache = /* @__PURE__ */ new Map();
491
+ const resolvingRefs = /* @__PURE__ */ new Set();
492
+ function walkPointer($ref) {
493
+ const trimmed = $ref.trim();
494
+ if (trimmed === "" || !trimmed.startsWith("#")) return {
495
+ applicable: false,
496
+ value: null
497
+ };
498
+ const pointer = trimmed.substring(1);
499
+ if (pointerCache.has(pointer)) return {
500
+ applicable: true,
501
+ value: pointerCache.get(pointer)
502
+ };
503
+ const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
504
+ if (current) pointerCache.set(pointer, current);
505
+ return {
506
+ applicable: true,
507
+ value: current ?? null
508
+ };
509
+ }
510
+ /**
511
+ * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
512
+ * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
513
+ * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
514
+ * for a speculative lookup where a missing ref is not an error.
515
+ */
516
+ function resolve(refPath, options) {
517
+ const { applicable, value } = walkPointer(refPath);
518
+ if (!applicable || value || options?.report === false) return value;
519
+ const diagnostic = {
520
+ code: _kubb_kit.Diagnostics.code.refNotFound,
521
+ severity: "error",
522
+ message: `Could not find a definition for ${refPath}.`,
523
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
524
+ location: {
525
+ kind: "schema",
526
+ pointer: refPath,
527
+ ref: refPath
528
+ }
529
+ };
530
+ if (!_kubb_kit.Diagnostics.report(diagnostic)) throw new _kubb_kit.Diagnostics.Error(diagnostic);
531
+ return null;
532
+ }
533
+ /**
534
+ * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
535
+ * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
536
+ * resolved (e.g. a minimal document in a unit test).
537
+ */
538
+ function resolveNode(refPath, parse) {
539
+ if (resolvingRefs.has(refPath)) return null;
540
+ if (!resolvedNodeCache.has(refPath)) {
541
+ let resolved = null;
542
+ try {
543
+ const referenced = resolve(refPath);
544
+ if (referenced) {
545
+ resolvingRefs.add(refPath);
546
+ resolved = parse({ schema: referenced });
547
+ resolvingRefs.delete(refPath);
548
+ }
549
+ } catch {}
550
+ resolvedNodeCache.set(refPath, resolved);
551
+ }
552
+ return resolvedNodeCache.get(refPath) ?? null;
553
+ }
554
+ /**
555
+ * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
556
+ * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
557
+ * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
558
+ * returned as-is.
559
+ */
560
+ function deref(value) {
561
+ if (!isReference(value)) return value ? value : null;
562
+ const resolved = resolve(value.$ref);
563
+ return resolved && !isReference(resolved) ? resolved : null;
564
+ }
565
+ /** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
566
+ function derefKeepingRef(value) {
567
+ if (isReference(value)) return {
568
+ ...value,
569
+ ...resolve(value.$ref),
570
+ $ref: value.$ref
571
+ };
572
+ return value;
573
+ }
574
+ return {
575
+ resolve,
576
+ resolveNode,
577
+ deref,
578
+ derefKeepingRef
579
+ };
580
+ }
581
+ //#endregion
390
582
  //#region src/load/source.ts
391
583
  const urlRegExp = /^https?:\/+/i;
392
584
  /**
@@ -507,6 +699,22 @@ function hasExternalRef(node) {
507
699
  return Object.values(node).some(hasExternalRef);
508
700
  }
509
701
  /**
702
+ * Replaces each `$ref` into `#/paths/...` with a copy of its target, in place.
703
+ *
704
+ * The Swagger 2.0 upgrade moves `responses/200/schema` under `content`, so a ref to the old location
705
+ * would no longer resolve. A ref that points back into itself is kept, since inlining it never ends.
706
+ */
707
+ function inlinePathRefs(node, resolve, visiting = []) {
708
+ if (!node || typeof node !== "object") return node;
709
+ const ref = node.$ref;
710
+ if (typeof ref === "string" && ref.startsWith("#/paths/") && !visiting.includes(ref)) {
711
+ const target = resolve(ref);
712
+ return target && typeof target === "object" ? inlinePathRefs(structuredClone(target), resolve, [...visiting, ref]) : node;
713
+ }
714
+ for (const [key, value] of Object.entries(node)) node[key] = inlinePathRefs(value, resolve, visiting);
715
+ return node;
716
+ }
717
+ /**
510
718
  * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
511
719
  *
512
720
  * External file schemas are hoisted into named `components.schemas` entries, so a property
@@ -564,6 +772,17 @@ async function parseFromConfig(source) {
564
772
  await assertInputExists(source.path);
565
773
  return bundleDocument(source.path);
566
774
  })();
775
+ if (raw && typeof raw === "object") {
776
+ const document = raw;
777
+ for (const key of ["swagger", "openapi"]) {
778
+ const version = document[key];
779
+ if (typeof version === "number") document[key] = Number.isInteger(version) ? version.toFixed(1) : String(version);
780
+ }
781
+ }
782
+ if (raw && typeof raw === "object" && "swagger" in raw) {
783
+ const refs = createRefs(raw);
784
+ inlinePathRefs(raw, (ref) => refs.resolve(ref, { report: false }));
785
+ }
567
786
  try {
568
787
  return (0, _scalar_openapi_upgrader.upgrade)(raw, "3.1");
569
788
  } catch (error) {
@@ -660,90 +879,6 @@ async function validateDocumentInWorker(document) {
660
879
  if (!message.valid) reportValidationErrors(message);
661
880
  }
662
881
  //#endregion
663
- //#region src/oas.ts
664
- /**
665
- * Returns `true` when a schema should be treated as nullable.
666
- *
667
- * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
668
- * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
669
- */
670
- function isNullable(schema) {
671
- if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
672
- const schemaType = schema?.type;
673
- if (schemaType === "null") return true;
674
- if (Array.isArray(schemaType)) return schemaType.includes("null");
675
- return false;
676
- }
677
- /**
678
- * Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
679
- */
680
- function pickExtensions(source) {
681
- const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
682
- return entries.length > 0 ? Object.fromEntries(entries) : void 0;
683
- }
684
- /**
685
- * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
686
- */
687
- function isReference(obj) {
688
- return !!obj && typeof obj === "object" && "$ref" in obj;
689
- }
690
- /**
691
- * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
692
- * excluding the Swagger 2 string form.
693
- */
694
- function isDiscriminator(obj) {
695
- const record = obj;
696
- return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
697
- }
698
- /**
699
- * Returns `true` when a schema is a raw binary payload.
700
- */
701
- function isBinary(schema) {
702
- return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
703
- }
704
- /**
705
- * Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
706
- * when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
707
- * `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
708
- * media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
709
- * `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
710
- * instead of a blob.
711
- */
712
- function getBinaryFallbackSchema(mediaType, schema) {
713
- if (!mediaType || isJsonMimeType(mediaType)) return void 0;
714
- if (schema && Object.keys(schema).length > 0) return void 0;
715
- return {
716
- type: "string",
717
- contentMediaType: "application/octet-stream"
718
- };
719
- }
720
- /**
721
- * MIME type fragments that mark a media type as JSON-like.
722
- *
723
- * A content type is JSON when it contains any of these substrings. The `+json` entry catches
724
- * structured-syntax suffixes such as `application/vnd.api+json`.
725
- */
726
- const jsonMimeFragments = [
727
- "application/json",
728
- "application/x-json",
729
- "text/json",
730
- "text/x-json",
731
- "+json"
732
- ];
733
- /**
734
- * Returns `true` when a media type string is JSON-like.
735
- *
736
- * @example
737
- * ```ts
738
- * isJsonMimeType('application/json') // true
739
- * isJsonMimeType('application/vnd.api+json') // true
740
- * isJsonMimeType('multipart/form-data') // false
741
- * ```
742
- */
743
- function isJsonMimeType(mimeType) {
744
- return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
745
- }
746
- //#endregion
747
882
  //#region src/model/components.ts
748
883
  /**
749
884
  * Extracts the inline schema from a media-type `content` map, with the media type it sits under.
@@ -1172,6 +1307,28 @@ function getParameters({ operation, refs }) {
1172
1307
  for (const entry of [...pathLevelParams, ...operationParams]) if (entry.param.name && entry.param.in) paramMap.set(`${entry.param.in}:${entry.param.name}`, entry);
1173
1308
  return Array.from(paramMap.values());
1174
1309
  }
1310
+ const PARAMETER_STYLES = /* @__PURE__ */ new Set([
1311
+ "matrix",
1312
+ "label",
1313
+ "form",
1314
+ "simple",
1315
+ "spaceDelimited",
1316
+ "pipeDelimited",
1317
+ "deepObject"
1318
+ ]);
1319
+ /** The `encoding` of a media type, reduced to the entries that set a field Kubb carries. A `style` outside the OpenAPI set is dropped. */
1320
+ function getEncoding(encoding) {
1321
+ const entries = Object.entries(encoding ?? {}).flatMap(([property, { contentType, style, explode, allowReserved }]) => {
1322
+ const node = {
1323
+ ...contentType !== void 0 ? { contentType } : {},
1324
+ ...style !== void 0 && PARAMETER_STYLES.has(style) ? { style } : {},
1325
+ ...explode !== void 0 ? { explode } : {},
1326
+ ...allowReserved !== void 0 ? { allowReserved } : {}
1327
+ };
1328
+ return Object.keys(node).length ? [[property, node]] : [];
1329
+ });
1330
+ return entries.length ? Object.fromEntries(entries) : void 0;
1331
+ }
1175
1332
  /** Schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON entry. */
1176
1333
  function getBodySchema({ content, contentType, refs }) {
1177
1334
  const entry = contentType ? content?.[contentType] : void 0;
@@ -1226,7 +1383,7 @@ function parseParameter({ param, pointer, parentName, options, parseSchema }) {
1226
1383
  name: schemaName
1227
1384
  }) : _kubb_kit.ast.factory.createSchema({ type: options.unknownType });
1228
1385
  if (rawSchema) scanSchema(schema, contentType ? `${pointer}/content/${escapePointerToken(contentType)}/schema` : `${pointer}/schema`);
1229
- const style = param.style;
1386
+ const style = param.style !== void 0 && PARAMETER_STYLES.has(param.style) ? param.style : void 0;
1230
1387
  const explode = param.explode;
1231
1388
  const allowReserved = param.allowReserved;
1232
1389
  return _kubb_kit.ast.factory.createParameter({
@@ -1279,10 +1436,12 @@ function parseOperation({ operation, ...ctx }) {
1279
1436
  name: requestBodyName
1280
1437
  });
1281
1438
  scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}/schema`);
1439
+ const encoding = getEncoding(body?.content?.[ct]?.encoding);
1282
1440
  return [_kubb_kit.ast.factory.createContent({
1283
1441
  contentType: ct,
1284
1442
  schema: _kubb_kit.ast.optionality(node, bodyRequired),
1285
- keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
1443
+ keysToOmit: collectPropertyKeysByFlag(schema, "readOnly"),
1444
+ ...encoding && { encoding }
1286
1445
  })];
1287
1446
  });
1288
1447
  const requestBody = content.length > 0 || body?.description ? {
@@ -1379,6 +1538,42 @@ function createNode({ schema, name, nullable, defaultValue }, extras) {
1379
1538
  //#endregion
1380
1539
  //#region src/emit/discriminator/preserve.ts
1381
1540
  /**
1541
+ * Turns string mapping keys into the discriminator property's type when they parse cleanly.
1542
+ */
1543
+ function typedKeys({ keys, propertyType }) {
1544
+ return keys.map((key) => {
1545
+ if (propertyType === "integer" || propertyType === "number") {
1546
+ const value = Number(key);
1547
+ return key.trim() !== "" && Number.isFinite(value) ? value : key;
1548
+ }
1549
+ if (propertyType === "boolean") {
1550
+ const lower = key.toLowerCase();
1551
+ return lower === "true" ? true : lower === "false" ? false : key;
1552
+ }
1553
+ return key;
1554
+ });
1555
+ }
1556
+ /**
1557
+ * Reads the declared `type` of `propertyName` on a schema, following `$ref`s and `allOf` members.
1558
+ * A nullable array such as `['integer', 'null']` reads as its one non-null type.
1559
+ */
1560
+ function propertyTypeOf({ schema, propertyName, refs, seen = /* @__PURE__ */ new Set() }) {
1561
+ const deref = (value) => value && isReference(value) ? refs.resolve(value.$ref, { report: false }) : value;
1562
+ if (schema && isReference(schema)) {
1563
+ if (seen.has(schema.$ref)) return void 0;
1564
+ seen.add(schema.$ref);
1565
+ }
1566
+ const resolved = deref(schema);
1567
+ const type = deref(resolved?.properties?.[propertyName])?.type ?? resolved?.allOf?.map((member) => propertyTypeOf({
1568
+ schema: member,
1569
+ propertyName,
1570
+ refs,
1571
+ seen
1572
+ })).find(Boolean);
1573
+ const nonNull = Array.isArray(type) ? type.filter((t) => t !== "null") : [];
1574
+ return nonNull.length === 1 ? nonNull[0] : type;
1575
+ }
1576
+ /**
1382
1577
  * Creates a single-property object schema used as a discriminator literal.
1383
1578
  *
1384
1579
  * @example
@@ -1395,7 +1590,7 @@ function createDiscriminantNode({ propertyName, values }) {
1395
1590
  name: propertyName,
1396
1591
  schema: _kubb_kit.ast.factory.createSchema({
1397
1592
  type: "enum",
1398
- primitive: "string",
1593
+ primitive: (0, _kubb_kit.enumPrimitive)(values),
1399
1594
  enumValues: values
1400
1595
  }),
1401
1596
  required: true
@@ -1410,21 +1605,61 @@ function createDiscriminantNode({ propertyName, values }) {
1410
1605
  *
1411
1606
  * @example
1412
1607
  * ```ts
1413
- * findDiscriminators({ dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // ['dog', 'hound']
1608
+ * findDiscriminators({ mapping: { dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, ref: '#/components/schemas/Dog' }) // ['dog', 'hound']
1414
1609
  * ```
1415
1610
  */
1416
- function findDiscriminators(mapping, ref) {
1611
+ function findDiscriminators({ mapping, ref }) {
1417
1612
  if (!mapping || !ref) return [];
1418
1613
  return Object.entries(mapping).filter(([, value]) => value === ref).map(([key]) => key);
1419
1614
  }
1420
1615
  /**
1616
+ * Finds the discriminator a schema declares itself or through an inline `allOf` member.
1617
+ */
1618
+ function getDiscriminator(schema) {
1619
+ return (isDiscriminator(schema) ? schema : schema.allOf?.find(isDiscriminator))?.discriminator;
1620
+ }
1621
+ /**
1622
+ * Lists the mapping keys of a discriminator plus the keys of the nested discriminators its mapped
1623
+ * schemas declare on the same property, so a root `$type` accepts values from every level.
1624
+ */
1625
+ function getMappingKeys({ discriminator: { propertyName, mapping }, refs, seen = /* @__PURE__ */ new Set() }) {
1626
+ return Object.entries(mapping ?? {}).flatMap(([key, target]) => {
1627
+ if (seen.has(target)) return [key];
1628
+ seen.add(target);
1629
+ const resolved = refs.resolve(target, { report: false });
1630
+ const nested = resolved && getDiscriminator(resolved);
1631
+ return nested?.propertyName === propertyName ? [key, ...getMappingKeys({
1632
+ discriminator: nested,
1633
+ refs,
1634
+ seen
1635
+ })] : [key];
1636
+ });
1637
+ }
1638
+ /**
1639
+ * Drops the discriminator from an inline `allOf` member when a `$ref` sibling already declares one on
1640
+ * the same property. That mapping names the schema's own children, and keeping it pins the property
1641
+ * to those keys next to the parent's, which leaves `never`.
1642
+ */
1643
+ function removeInheritedDiscriminator({ members, refs }) {
1644
+ const inherited = new Set(members.flatMap((member) => {
1645
+ const parent = isReference(member) ? refs.resolve(member.$ref, { report: false }) : null;
1646
+ return parent && getDiscriminator(parent)?.propertyName || [];
1647
+ }));
1648
+ return members.map((member) => {
1649
+ if (isReference(member) || !isDiscriminator(member) || member.oneOf || member.anyOf) return member;
1650
+ if (!inherited.has(member.discriminator.propertyName)) return member;
1651
+ const { discriminator: _discriminator, ...rest } = member;
1652
+ return rest;
1653
+ });
1654
+ }
1655
+ /**
1421
1656
  * Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
1422
1657
  * node with either the shared-properties slice carrying that value, or a synthetic discriminant
1423
1658
  * literal. The referenced child schema's own definition is left untouched — narrowing happens only
1424
1659
  * at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
1425
1660
  * which additionally patches the child schema's own definition in a post-pass).
1426
1661
  */
1427
- function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, name, refs }) {
1662
+ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parentType, parse, name, refs }) {
1428
1663
  function pickDiscriminatorPropertyNode(node, propertyName) {
1429
1664
  const discriminatorProperty = _kubb_kit.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1430
1665
  if (!discriminatorProperty) return null;
@@ -1460,9 +1695,20 @@ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode,
1460
1695
  }
1461
1696
  return unionMembers.map((s) => {
1462
1697
  const ref = isReference(s) ? s.$ref : void 0;
1463
- const mappedValues = findDiscriminators(discriminator?.mapping, ref);
1698
+ const mappedValues = findDiscriminators({
1699
+ mapping: discriminator?.mapping,
1700
+ ref
1701
+ });
1464
1702
  const implicitValue = mappedValues.length ? null : implicitDiscriminantValue(s);
1465
- const discriminatorValues = mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [];
1703
+ const propertyType = propertyTypeOf({
1704
+ schema: s,
1705
+ propertyName: discriminator?.propertyName ?? "",
1706
+ refs
1707
+ }) ?? parentType;
1708
+ const discriminatorValues = typedKeys({
1709
+ keys: mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [],
1710
+ propertyType
1711
+ });
1466
1712
  const memberNode = parse({
1467
1713
  schema: s,
1468
1714
  name
@@ -1494,21 +1740,34 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
1494
1740
  members: allOfMembers.filter((item) => {
1495
1741
  if (!isReference(item) || !name) return true;
1496
1742
  const deref = refs.resolve(item.$ref);
1497
- if (!deref || !isDiscriminator(deref)) return true;
1498
- const parentUnion = deref.oneOf ?? deref.anyOf;
1499
- if (!parentUnion) return true;
1743
+ const discriminator = deref && getDiscriminator(deref);
1744
+ const parentUnion = deref?.oneOf ?? deref?.anyOf;
1745
+ if (!deref || !discriminator || isDiscriminator(deref) && !parentUnion) return true;
1500
1746
  const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1501
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1502
- const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1503
- if (inOneOf || inMapping) {
1504
- const values = findDiscriminators(deref.discriminator.mapping, childRef);
1505
- if (values.length) discriminantValues.push({
1506
- propertyName: deref.discriminator.propertyName,
1507
- values
1747
+ const values = findDiscriminators({
1748
+ mapping: discriminator.mapping,
1749
+ ref: childRef
1750
+ });
1751
+ if (values.length) {
1752
+ const { propertyName } = discriminator;
1753
+ const propertyType = propertyTypeOf({
1754
+ schema: deref,
1755
+ propertyName,
1756
+ refs
1757
+ }) ?? propertyTypeOf({
1758
+ schema: { $ref: childRef },
1759
+ propertyName,
1760
+ refs
1761
+ });
1762
+ discriminantValues.push({
1763
+ propertyName,
1764
+ values: typedKeys({
1765
+ keys: values,
1766
+ propertyType
1767
+ })
1508
1768
  });
1509
- return false;
1510
1769
  }
1511
- return true;
1770
+ return !(parentUnion && (values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
1512
1771
  }),
1513
1772
  discriminantValues
1514
1773
  };
@@ -1635,10 +1894,9 @@ function resolveUnionMembers(members, siblingProperties) {
1635
1894
  });
1636
1895
  }
1637
1896
  /**
1638
- * Enriches any `oneOf` or `anyOf` branches on a schema object with properties from sibling schemas.
1897
+ * Enriches a schema object, and any `oneOf` or `anyOf` branches on it, with properties from sibling schemas.
1639
1898
  */
1640
1899
  function resolveUnionSchema(schema, siblingProperties) {
1641
- if (!schema.oneOf && !schema.anyOf) return schema;
1642
1900
  const next = { ...resolveUnionMembers([schema], siblingProperties)[0] ?? schema };
1643
1901
  if (schema.oneOf) next.oneOf = resolveUnionMembers(schema.oneOf, siblingProperties);
1644
1902
  if (schema.anyOf) next.anyOf = resolveUnionMembers(schema.anyOf, siblingProperties);
@@ -1662,6 +1920,17 @@ function convertRef({ schema, name, nullable, defaultValue, document, parse, ref
1662
1920
  defaultValue
1663
1921
  };
1664
1922
  if (refPath && document.components && !refs.resolve(refPath, { report: false })) return createNode(ctx, { type: "unknown" });
1923
+ if (refPath && !refPath.startsWith("#/components/schemas/") && resolvedSchema) return _kubb_kit.ast.factory.createSchema({
1924
+ ...resolvedSchema,
1925
+ title: schema.title ?? resolvedSchema.title,
1926
+ description: schema.description ?? resolvedSchema.description,
1927
+ deprecated: schema.deprecated ?? resolvedSchema.deprecated,
1928
+ readOnly: schema.readOnly ?? resolvedSchema.readOnly,
1929
+ writeOnly: schema.writeOnly ?? resolvedSchema.writeOnly,
1930
+ default: defaultValue ?? resolvedSchema.default,
1931
+ name: name ?? resolvedSchema.name,
1932
+ nullable: nullable ?? resolvedSchema.nullable
1933
+ });
1665
1934
  const targetName = renames?.get(schema.$ref);
1666
1935
  return createNode(ctx, {
1667
1936
  type: "ref",
@@ -1693,9 +1962,41 @@ function convertNot({ schema, name, nullable, defaultValue, parse }) {
1693
1962
  });
1694
1963
  }
1695
1964
  /**
1965
+ * Keywords that describe a schema or conditionally constrain it, but add no structure.
1966
+ */
1967
+ const annotationKeys = /* @__PURE__ */ new Set([
1968
+ "description",
1969
+ "title",
1970
+ "example",
1971
+ "examples",
1972
+ "default",
1973
+ "deprecated",
1974
+ "readOnly",
1975
+ "writeOnly",
1976
+ "if",
1977
+ "then",
1978
+ "else"
1979
+ ]);
1980
+ /**
1981
+ * Moves `allOf` members that hold only annotations or `if`/`then`/`else` onto the parent, so they do
1982
+ * not become `unknown`. The parent's own values win, and the conditional keywords are not enforced.
1983
+ */
1984
+ function mergeAnnotations(schema) {
1985
+ const members = schema.allOf;
1986
+ const isAnnotation = (member) => !isReference(member) && Object.keys(member).every((key) => annotationKeys.has(key));
1987
+ const annotations = members.filter(isAnnotation);
1988
+ const rest = members.filter((member) => !isAnnotation(member));
1989
+ return annotations.length && rest.length ? {
1990
+ ...Object.assign({}, ...annotations),
1991
+ ...schema,
1992
+ allOf: rest
1993
+ } : schema;
1994
+ }
1995
+ /**
1696
1996
  * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
1697
1997
  */
1698
- function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
1998
+ function convertAllOf({ schema: original, name, nullable, defaultValue, parse, refs }) {
1999
+ const schema = mergeAnnotations(original);
1699
2000
  if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
1700
2001
  const [memberSchema] = schema.allOf;
1701
2002
  const memberNode = parse({
@@ -1721,7 +2022,10 @@ function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
1721
2022
  });
1722
2023
  }
1723
2024
  const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
1724
- allOfMembers: schema.allOf,
2025
+ allOfMembers: removeInheritedDiscriminator({
2026
+ members: schema.allOf,
2027
+ refs
2028
+ }),
1725
2029
  name,
1726
2030
  refs
1727
2031
  });
@@ -1801,6 +2105,11 @@ function convertUnion(context) {
1801
2105
  unionMembers,
1802
2106
  discriminator,
1803
2107
  sharedPropertiesNode,
2108
+ parentType: discriminator && propertyTypeOf({
2109
+ schema,
2110
+ propertyName: discriminator.propertyName,
2111
+ refs
2112
+ }),
1804
2113
  parse,
1805
2114
  name,
1806
2115
  refs
@@ -2064,7 +2373,7 @@ function nameEnums(node, options) {
2064
2373
  /**
2065
2374
  * Converts an object-like schema into an `ObjectSchemaNode`.
2066
2375
  */
2067
- function convertObject({ schema, name, nullable, defaultValue, options, parse }) {
2376
+ function convertObject({ schema, name, nullable, defaultValue, options, parse, refs }) {
2068
2377
  const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
2069
2378
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
2070
2379
  const resolvedPropSchema = propSchema;
@@ -2081,7 +2390,7 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
2081
2390
  name: propName,
2082
2391
  schema: {
2083
2392
  ...schemaNode,
2084
- nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
2393
+ nullable: schemaNode.type === "null" ? void 0 : propNullable || schemaNode.nullable || void 0
2085
2394
  },
2086
2395
  required
2087
2396
  });
@@ -2121,7 +2430,17 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
2121
2430
  });
2122
2431
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
2123
2432
  const discPropName = schema.discriminator.propertyName;
2124
- const values = Object.keys(schema.discriminator.mapping);
2433
+ const values = typedKeys({
2434
+ keys: getMappingKeys({
2435
+ discriminator: schema.discriminator,
2436
+ refs
2437
+ }),
2438
+ propertyType: propertyTypeOf({
2439
+ schema,
2440
+ propertyName: discPropName,
2441
+ refs
2442
+ })
2443
+ });
2125
2444
  const enumName = name ? (0, _kubb_kit.enumPropName)(name, discPropName, options.enumSuffix) : void 0;
2126
2445
  return _kubb_kit.ast.applyMacros(objectNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
2127
2446
  propertyName: discPropName,
@@ -2179,8 +2498,9 @@ function convertArray({ schema, name, nullable, defaultValue, options, parse })
2179
2498
  //#region src/emit/parseSchema.ts
2180
2499
  /**
2181
2500
  * Ordered schema rule table. Order is significant: composition keywords (`not`, `$ref`, `allOf`,
2182
- * `oneOf`/`anyOf`) take precedence over `const`, which takes precedence over a genuine OAS 3.1
2183
- * multi-type array (more than one non-`null` type), which takes precedence over `format`/`type`.
2501
+ * `oneOf`/`anyOf`) take precedence over `const` (except a `$ref` with a sibling `const`, where the
2502
+ * `const` wins), which takes precedence over a genuine OAS 3.1 multi-type array (more than one
2503
+ * non-`null` type), which takes precedence over `format`/`type`.
2184
2504
  * A multi-type array must split into its per-type members before `format` runs, otherwise
2185
2505
  * `format` collapses the whole schema to one type (e.g. `type: ['null', 'integer', 'string'],
2186
2506
  * format: 'int32'` would drop `string` and emit just `integer`); each split-off member still
@@ -2194,7 +2514,7 @@ const schemaRules = [
2194
2514
  convert: convertNot
2195
2515
  },
2196
2516
  {
2197
- match: ({ schema }) => isReference(schema),
2517
+ match: ({ schema }) => isReference(schema) && !("const" in schema && schema.const !== void 0),
2198
2518
  convert: convertRef
2199
2519
  },
2200
2520
  {
@@ -2362,114 +2682,6 @@ function refPromotedEnums(node, promoted) {
2362
2682
  } });
2363
2683
  }
2364
2684
  //#endregion
2365
- //#region src/refs.ts
2366
- /**
2367
- * Creates the `$ref` resolution service for one document. One pointer walk backs every method.
2368
- *
2369
- * @example
2370
- * ```ts
2371
- * const refs = createRefs(document)
2372
- * refs.resolve<SchemaObject>('#/components/schemas/Pet')
2373
- * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
2374
- * refs.resolveNode('#/components/schemas/Pet', parseSchema)
2375
- * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
2376
- * refs.derefKeepingRef(parameter)
2377
- * ```
2378
- */
2379
- function createRefs(document) {
2380
- const pointerCache = /* @__PURE__ */ new Map();
2381
- const resolvedNodeCache = /* @__PURE__ */ new Map();
2382
- const resolvingRefs = /* @__PURE__ */ new Set();
2383
- function walkPointer($ref) {
2384
- const trimmed = $ref.trim();
2385
- if (trimmed === "" || !trimmed.startsWith("#")) return {
2386
- applicable: false,
2387
- value: null
2388
- };
2389
- const pointer = trimmed.substring(1);
2390
- if (pointerCache.has(pointer)) return {
2391
- applicable: true,
2392
- value: pointerCache.get(pointer)
2393
- };
2394
- const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
2395
- if (current) pointerCache.set(pointer, current);
2396
- return {
2397
- applicable: true,
2398
- value: current ?? null
2399
- };
2400
- }
2401
- /**
2402
- * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
2403
- * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
2404
- * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
2405
- * for a speculative lookup where a missing ref is not an error.
2406
- */
2407
- function resolve(refPath, options) {
2408
- const { applicable, value } = walkPointer(refPath);
2409
- if (!applicable || value || options?.report === false) return value;
2410
- const diagnostic = {
2411
- code: _kubb_kit.Diagnostics.code.refNotFound,
2412
- severity: "error",
2413
- message: `Could not find a definition for ${refPath}.`,
2414
- help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
2415
- location: {
2416
- kind: "schema",
2417
- pointer: refPath,
2418
- ref: refPath
2419
- }
2420
- };
2421
- if (!_kubb_kit.Diagnostics.report(diagnostic)) throw new _kubb_kit.Diagnostics.Error(diagnostic);
2422
- return null;
2423
- }
2424
- /**
2425
- * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
2426
- * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
2427
- * resolved (e.g. a minimal document in a unit test).
2428
- */
2429
- function resolveNode(refPath, parse) {
2430
- if (resolvingRefs.has(refPath)) return null;
2431
- if (!resolvedNodeCache.has(refPath)) {
2432
- let resolved = null;
2433
- try {
2434
- const referenced = resolve(refPath);
2435
- if (referenced) {
2436
- resolvingRefs.add(refPath);
2437
- resolved = parse({ schema: referenced });
2438
- resolvingRefs.delete(refPath);
2439
- }
2440
- } catch {}
2441
- resolvedNodeCache.set(refPath, resolved);
2442
- }
2443
- return resolvedNodeCache.get(refPath) ?? null;
2444
- }
2445
- /**
2446
- * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
2447
- * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
2448
- * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
2449
- * returned as-is.
2450
- */
2451
- function deref(value) {
2452
- if (!isReference(value)) return value ? value : null;
2453
- const resolved = resolve(value.$ref);
2454
- return resolved && !isReference(resolved) ? resolved : null;
2455
- }
2456
- /** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
2457
- function derefKeepingRef(value) {
2458
- if (isReference(value)) return {
2459
- ...value,
2460
- ...resolve(value.$ref),
2461
- $ref: value.$ref
2462
- };
2463
- return value;
2464
- }
2465
- return {
2466
- resolve,
2467
- resolveNode,
2468
- deref,
2469
- derefKeepingRef
2470
- };
2471
- }
2472
- //#endregion
2473
2685
  //#region src/sourceLocator.ts
2474
2686
  let cached;
2475
2687
  const toRange = ([start, end]) => ({
@@ -2702,7 +2914,7 @@ const adapterOas = (0, _kubb_core.defineExtension)({
2702
2914
  };
2703
2915
  parsedByName.set(name, node);
2704
2916
  scanSchema(node);
2705
- if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2917
+ if (node.type === "ref" && node.name) refAliasMap.set(name, node);
2706
2918
  if (_kubb_kit.ast.narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
2707
2919
  if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2708
2920
  }
@@ -2717,6 +2929,36 @@ const adapterOas = (0, _kubb_core.defineExtension)({
2717
2929
  }));
2718
2930
  const promotedEnums = enums === "root" ? collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas))) : null;
2719
2931
  if (promotedEnums) for (const name of promotedEnums.keys()) enumNames.push(name);
2932
+ const aliasTargetOf = (name) => {
2933
+ const node = refAliasMap.get(name);
2934
+ return node?.ref ? _kubb_kit.ast.refName(node.ref) : node?.name;
2935
+ };
2936
+ function isAliasCycle(name) {
2937
+ const seen = /* @__PURE__ */ new Set([name]);
2938
+ for (let next = aliasTargetOf(name); next; next = aliasTargetOf(next)) {
2939
+ if (next === name) return true;
2940
+ if (seen.has(next)) return false;
2941
+ seen.add(next);
2942
+ }
2943
+ return false;
2944
+ }
2945
+ function createCycleNode(name, pointer) {
2946
+ _kubb_kit.Diagnostics.report({
2947
+ code: _kubb_kit.Diagnostics.code.aliasCycle,
2948
+ severity: "warning",
2949
+ message: `${name} only refers to other schemas that lead back to it, so it is resolved to unknown.`,
2950
+ help: "Give one of the schemas in the loop a real definition, or remove the `$ref` that closes it.",
2951
+ location: {
2952
+ kind: "schema",
2953
+ pointer: pointer ?? `#/components/schemas/${name}`
2954
+ }
2955
+ });
2956
+ return _kubb_kit.ast.factory.createSchema({
2957
+ type: "unknown",
2958
+ name,
2959
+ pointer
2960
+ });
2961
+ }
2720
2962
  const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
2721
2963
  for (const name of Object.keys(schemas)) {
2722
2964
  const aliasTarget = refAliasMap.get(name)?.name;
@@ -2724,11 +2966,12 @@ const adapterOas = (0, _kubb_core.defineExtension)({
2724
2966
  const parsed = parsedByName.get(name);
2725
2967
  const child = discriminatorChildMap?.get(name);
2726
2968
  const base = child ? patchDiscriminatorNode(parsed, child) : parsed;
2727
- const node = aliased ? {
2969
+ const resolved = aliased ? {
2728
2970
  ...aliased,
2729
2971
  name,
2730
2972
  pointer: parsed.pointer
2731
2973
  } : base;
2974
+ const node = isAliasCycle(name) ? createCycleNode(name, parsed.pointer) : resolved;
2732
2975
  schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
2733
2976
  }
2734
2977
  const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;