@kubb/adapter-oas 5.0.0-beta.99 → 5.0.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.cjs CHANGED
@@ -16,20 +16,20 @@ var __copyProps = (to, from, except, desc) => {
16
16
  }
17
17
  return to;
18
18
  };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
20
  value: mod,
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
24
  let _kubb_ast = require("@kubb/ast");
25
25
  let _kubb_core = require("@kubb/core");
26
+ let node_fs_promises = require("node:fs/promises");
26
27
  let node_path = require("node:path");
27
28
  node_path = __toESM(node_path, 1);
28
- let node_fs_promises = require("node:fs/promises");
29
- let _readme_openapi_parser = require("@readme/openapi-parser");
29
+ let yaml = require("yaml");
30
30
  let _scalar_openapi_upgrader = require("@scalar/openapi-upgrader");
31
31
  let api_ref_bundler = require("api-ref-bundler");
32
- let yaml = require("yaml");
32
+ let _kubb_kit = require("@kubb/kit");
33
33
  //#region src/constants.ts
34
34
  /**
35
35
  * Default parser options applied when no explicit options are provided.
@@ -37,8 +37,8 @@ let yaml = require("yaml");
37
37
  const DEFAULT_PARSER_OPTIONS = {
38
38
  dateType: "string",
39
39
  integerType: "bigint",
40
- unknownType: "any",
41
- emptySchemaType: "any",
40
+ unknownType: "unknown",
41
+ emptySchemaType: "unknown",
42
42
  enumSuffix: "enum"
43
43
  };
44
44
  /**
@@ -83,17 +83,32 @@ const structuralKeys = /* @__PURE__ */ new Set([
83
83
  ]);
84
84
  /**
85
85
  * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
86
- * `int64` and the date/time family. Keep this in sync with the `convertFormat`
86
+ * `int64`, `uint64` and the date/time family. Keep this in sync with the `convertFormat`
87
87
  * special-cases in `parser.ts`. `isHandledFormat` reads it so the
88
88
  * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
89
89
  */
90
90
  const specialCasedFormats = /* @__PURE__ */ new Set([
91
91
  "int64",
92
+ "uint64",
92
93
  "date-time",
93
94
  "date",
94
95
  "time"
95
96
  ]);
96
97
  /**
98
+ * Formats that describe a number, whether they resolve through `formatMap` or through the
99
+ * `convertFormat` special cases. On a `type: 'string'` schema these do not make the value a
100
+ * number: gRPC-gateway and other ProtoJSON producers send 64-bit integers as JSON strings.
101
+ *
102
+ * @see https://protobuf.dev/programming-guides/json/#int64-strings
103
+ */
104
+ const numericFormats = /* @__PURE__ */ new Set([
105
+ "int32",
106
+ "int64",
107
+ "uint64",
108
+ "float",
109
+ "double"
110
+ ]);
111
+ /**
97
112
  * Static map from OAS `format` strings to Kubb `SchemaType` values.
98
113
  *
99
114
  * Only formats whose AST type differs from the OAS `type` field appear here.
@@ -127,7 +142,7 @@ const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
127
142
  */
128
143
  const enumDescriptionKeys = ["x-enumDescriptions", "x-enum-descriptions"];
129
144
  //#endregion
130
- //#region src/discriminator.ts
145
+ //#region src/emit/discriminator/propagate.ts
131
146
  /**
132
147
  * Maps each child schema name to its discriminator patch data by scanning the given
133
148
  * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
@@ -203,42 +218,6 @@ function patchDiscriminatorNode(node, entry) {
203
218
  properties: newProperties
204
219
  };
205
220
  }
206
- /**
207
- * Creates a single-property object schema used as a discriminator literal.
208
- *
209
- * @example
210
- * ```ts
211
- * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
212
- * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
213
- * ```
214
- */
215
- function createDiscriminantNode({ propertyName, value }) {
216
- return _kubb_ast.ast.factory.createSchema({
217
- type: "object",
218
- primitive: "object",
219
- properties: [_kubb_ast.ast.factory.createProperty({
220
- name: propertyName,
221
- schema: _kubb_ast.ast.factory.createSchema({
222
- type: "enum",
223
- primitive: "string",
224
- enumValues: [value]
225
- }),
226
- required: true
227
- })]
228
- });
229
- }
230
- /**
231
- * Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
232
- *
233
- * @example
234
- * ```ts
235
- * findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
236
- * ```
237
- */
238
- function findDiscriminator(mapping, ref) {
239
- if (!mapping || !ref) return null;
240
- return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
241
- }
242
221
  //#endregion
243
222
  //#region ../../internals/utils/src/casing.ts
244
223
  /**
@@ -267,6 +246,20 @@ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
267
246
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
268
247
  }
269
248
  //#endregion
249
+ //#region ../../internals/utils/src/errors.ts
250
+ /**
251
+ * Extracts a human-readable message from any thrown value.
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * getErrorMessage(new Error('oops')) // 'oops'
256
+ * getErrorMessage('plain string') // 'plain string'
257
+ * ```
258
+ */
259
+ function getErrorMessage(value) {
260
+ return value instanceof Error ? value.message : String(value);
261
+ }
262
+ //#endregion
270
263
  //#region ../../internals/utils/src/runtime.ts
271
264
  /**
272
265
  * Detects the JavaScript runtime executing the current process and exposes its name and version.
@@ -367,21 +360,104 @@ async function read(path) {
367
360
  return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
368
361
  }
369
362
  //#endregion
370
- //#region src/factory.ts
363
+ //#region src/load/source.ts
371
364
  const urlRegExp = /^https?:\/+/i;
365
+ /**
366
+ * Node reports every connection failure as `TypeError: fetch failed` and keeps the useful part
367
+ * (`connect ECONNREFUSED 127.0.0.1:8000`) on `cause`, one level deeper again when a host resolves
368
+ * to several addresses and the attempts collect into an `AggregateError`.
369
+ */
370
+ function describeFetchFailure(error) {
371
+ if (error instanceof AggregateError && error.errors.length > 0) return describeFetchFailure(error.errors[0]);
372
+ if (error instanceof Error && error.cause instanceof Error) return describeFetchFailure(error.cause) || error.message;
373
+ return getErrorMessage(error);
374
+ }
375
+ function helpForStatus(status) {
376
+ if (status === 401 || status === 403) return "The server refused the request. Kubb sends no credentials, so serve the document without authentication or download it and set `input` to the local file.";
377
+ if (status === 404) return "Check the URL. Open it in a browser or with `curl` to confirm it serves the OpenAPI document.";
378
+ if (status >= 500) return "The server failed while serving the document. Check that it is healthy, then run Kubb again.";
379
+ return "Open the URL in a browser or with `curl` to see what the server returns, then point `input` at a URL that serves the OpenAPI document.";
380
+ }
381
+ async function fetchSource(url) {
382
+ try {
383
+ return await fetch(url);
384
+ } catch (error) {
385
+ throw new _kubb_core.Diagnostics.Error({
386
+ code: _kubb_core.Diagnostics.code.inputUnreachable,
387
+ severity: "error",
388
+ message: `Cannot reach ${url.href}: ${describeFetchFailure(error)}`,
389
+ help: "Check that the host is running and reachable from this machine. For a local server, start it and confirm the port matches the one in `input`.",
390
+ location: { kind: "config" },
391
+ cause: error instanceof Error ? error : void 0
392
+ });
393
+ }
394
+ }
372
395
  async function readSource(sourcePath) {
373
396
  if (urlRegExp.test(sourcePath)) {
374
397
  const url = new URL(sourcePath);
375
- const response = await fetch(url);
376
- if (!response.ok) throw new Error(`Cannot fetch the OAS document at ${url.href} (HTTP ${response.status})`);
398
+ const response = await fetchSource(url);
399
+ if (!response.ok) {
400
+ const status = response.statusText ? `${response.status} ${response.statusText}` : String(response.status);
401
+ throw new _kubb_core.Diagnostics.Error({
402
+ code: _kubb_core.Diagnostics.code.inputRequestFailed,
403
+ severity: "error",
404
+ message: `The server at ${url.href} answered with HTTP ${status} instead of the OpenAPI document.`,
405
+ help: helpForStatus(response.status),
406
+ location: { kind: "config" }
407
+ });
408
+ }
377
409
  return response.text();
378
410
  }
379
411
  return read(sourcePath);
380
412
  }
413
+ /**
414
+ * Reads and parses one source file or URL referenced during bundling: YAML/JSON is parsed into an
415
+ * object, Markdown is returned as-is (bundled inline rather than dereferenced).
416
+ *
417
+ * JSON is valid YAML, so `yaml`'s `parse` handles both, but its general-purpose parser (comments,
418
+ * anchors, block scalars, multi-document streams) does much more work than `JSON.parse` needs to.
419
+ * `JSON.parse` runs first and fails fast on the first non-JSON character, so a real YAML document
420
+ * falls through to `parse` at negligible cost.
421
+ */
381
422
  async function resolveSource(sourcePath) {
382
423
  const data = await readSource(sourcePath);
383
424
  if (sourcePath.toLowerCase().endsWith(".md")) return data;
384
- return (0, yaml.parse)(data);
425
+ try {
426
+ return JSON.parse(data);
427
+ } catch {
428
+ return (0, yaml.parse)(data);
429
+ }
430
+ }
431
+ /**
432
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
433
+ * URLs are skipped: a remote input reports `KUBB_INPUT_REQUEST_FAILED` or `KUBB_INPUT_UNREACHABLE`
434
+ * from the request itself. A malformed but readable file is left for `parseDocument` to surface
435
+ * its parse error instead.
436
+ */
437
+ async function assertInputExists(input) {
438
+ if (URL.canParse(input)) return;
439
+ if (!await exists(input)) throw new _kubb_core.Diagnostics.Error({
440
+ code: _kubb_core.Diagnostics.code.inputNotFound,
441
+ severity: "error",
442
+ message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
443
+ help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
444
+ location: { kind: "config" }
445
+ });
446
+ }
447
+ //#endregion
448
+ //#region src/load/normalize.ts
449
+ /**
450
+ * True when `node` contains a `$ref` pointing outside the current document (a relative path,
451
+ * absolute path, or URL). An internal `#/...` fragment does not count.
452
+ *
453
+ * `Object.values` reads array elements and object property values alike, so the same recursion
454
+ * walks both without a separate array branch.
455
+ */
456
+ function hasExternalRef(node) {
457
+ if (!node || typeof node !== "object") return false;
458
+ const ref = node.$ref;
459
+ if (typeof ref === "string" && !ref.startsWith("#")) return true;
460
+ return Object.values(node).some(hasExternalRef);
385
461
  }
386
462
  /**
387
463
  * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
@@ -391,6 +467,11 @@ async function resolveSource(sourcePath) {
391
467
  * can then emit a named type with an import instead of inlining the shape. Sources are read with
392
468
  * the Bun-aware `read` util for local YAML and JSON files, and with `fetch` for HTTP(S) URLs.
393
469
  *
470
+ * A document with no `$ref` outside itself has nothing to bundle, so it skips `api-ref-bundler`
471
+ * and returns as parsed. `bundle` only rewrites external refs into internal ones; on an
472
+ * all-internal document it is a no-op that still walks the whole tree to confirm that, which
473
+ * costs real time on a large spec.
474
+ *
394
475
  * @example Local file
395
476
  * `const document = await bundleDocument('./openapi.yaml')`
396
477
  *
@@ -407,7 +488,8 @@ async function bundleDocument(pathOrUrl) {
407
488
  cache.set(key, result);
408
489
  return result;
409
490
  };
410
- await resolver(pathOrUrl);
491
+ const root = await resolver(pathOrUrl);
492
+ if (typeof root === "object" && root !== null && !hasExternalRef(root)) return root;
411
493
  return await (0, api_ref_bundler.bundle)(pathOrUrl, resolver);
412
494
  }
413
495
  /**
@@ -448,17 +530,20 @@ async function parseFromConfig(source) {
448
530
  return parseDocument(resolved);
449
531
  }
450
532
  /**
451
- * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
452
- * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
453
- * its parse error instead.
454
- */
455
- async function assertInputExists(input) {
456
- if (URL.canParse(input)) return;
457
- if (!await exists(input)) throw new _kubb_core.Diagnostics.Error({
458
- code: _kubb_core.Diagnostics.code.inputNotFound,
533
+ * Asserts the parsed input is an OpenAPI or Swagger document.
534
+ *
535
+ * {@link validateDocument} keeps spec violations non-fatal so imperfect but usable documents still
536
+ * generate. That leniency also swallowed input that is not a document at all, which then produced
537
+ * an empty build with a success exit code. A missing version field is the one failure that cannot
538
+ * be a usable document, so it is fatal regardless of the `validate` option.
539
+ */
540
+ function assertDocument(document) {
541
+ if (document && ("openapi" in document || "swagger" in document)) return;
542
+ throw new _kubb_core.Diagnostics.Error({
543
+ code: _kubb_core.Diagnostics.code.invalidDocument,
459
544
  severity: "error",
460
- message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
461
- help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
545
+ message: "The resolved `input` is not an OpenAPI or Swagger document: it declares no `openapi` or `swagger` version.",
546
+ help: "Point `input` at a document that declares `openapi` or `swagger`. If you pass an object, pass the spec itself rather than a wrapper such as `{ path }` or `{ data }`.",
462
547
  location: { kind: "config" }
463
548
  });
464
549
  }
@@ -471,9 +556,10 @@ async function assertInputExists(input) {
471
556
  * ```
472
557
  */
473
558
  async function validateDocument(document, { throwOnError = false } = {}) {
559
+ const { compileErrors, validate } = await import("@readme/openapi-parser");
474
560
  try {
475
- const result = await (0, _readme_openapi_parser.validate)(structuredClone(document), { validate: { errors: { colorize: true } } });
476
- if (!result.valid) throw new Error((0, _readme_openapi_parser.compileErrors)(result));
561
+ const result = await validate(structuredClone(document), { validate: { errors: { colorize: true } } });
562
+ if (!result.valid) throw new Error(compileErrors(result));
477
563
  } catch (error) {
478
564
  if (throwOnError) throw error;
479
565
  }
@@ -539,97 +625,232 @@ const jsonMimeFragments = [
539
625
  function isJsonMimeType(mimeType) {
540
626
  return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
541
627
  }
628
+ /**
629
+ * Picks a media-type entry from a `content` map: the first JSON-like media type, falling back to
630
+ * the first declared one. Returns `false` when `content` has no entries.
631
+ *
632
+ * @example
633
+ * ```ts
634
+ * pickContentEntry({ 'application/xml': xmlEntry, 'application/json': jsonEntry })
635
+ * // ['application/json', jsonEntry]
636
+ * ```
637
+ */
638
+ function pickContentEntry(content) {
639
+ const mediaTypes = Object.keys(content);
640
+ const available = mediaTypes.find(isJsonMimeType) ?? mediaTypes[0];
641
+ return available ? [available, content[available]] : false;
642
+ }
542
643
  //#endregion
543
- //#region src/refs.ts
544
- const _refCache = /* @__PURE__ */ new WeakMap();
644
+ //#region src/model/components.ts
545
645
  /**
546
- * Resolves a local JSON pointer reference from a document.
646
+ * Extracts the inline schema from a media-type `content` map.
547
647
  *
548
- * Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
549
- * resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
550
- * build there is no sink to collect it, so it throws instead.
648
+ * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
649
+ * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
551
650
  *
552
651
  * @example
553
652
  * ```ts
554
- * resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
653
+ * extractSchemaFromContent(operation.content, 'application/json')
654
+ * // SchemaObject | null
555
655
  * ```
556
656
  */
557
- function resolveRef(document, $ref) {
558
- const origRef = $ref;
559
- $ref = $ref.trim();
560
- if ($ref === "") return null;
561
- if (!$ref.startsWith("#")) return null;
562
- $ref = globalThis.decodeURIComponent($ref.substring(1));
563
- let docCache = _refCache.get(document);
564
- if (!docCache) {
565
- docCache = /* @__PURE__ */ new Map();
566
- _refCache.set(document, docCache);
657
+ function extractSchemaFromContent(content, preferredContentType) {
658
+ if (!content) return null;
659
+ const firstContentType = Object.keys(content)[0] ?? "application/json";
660
+ const schema = content[preferredContentType ?? firstContentType]?.schema;
661
+ if (isReference(schema)) return null;
662
+ return schema ?? null;
663
+ }
664
+ /**
665
+ * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
666
+ */
667
+ function* collectRefs(schema) {
668
+ if (Array.isArray(schema)) {
669
+ for (const item of schema) yield* collectRefs(item);
670
+ return;
567
671
  }
568
- if (docCache.has($ref)) return docCache.get($ref);
569
- const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
570
- if (!current) {
571
- const diagnostic = {
572
- code: _kubb_core.Diagnostics.code.refNotFound,
573
- severity: "error",
574
- message: `Could not find a definition for ${origRef}.`,
575
- help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
576
- location: {
577
- kind: "schema",
578
- pointer: origRef,
579
- ref: origRef
580
- }
581
- };
582
- if (!_kubb_core.Diagnostics.report(diagnostic)) throw new _kubb_core.Diagnostics.Error(diagnostic);
583
- return null;
672
+ if (schema && typeof schema === "object") for (const key in schema) {
673
+ const value = schema[key];
674
+ if (!(key === "$ref" && typeof value === "string")) {
675
+ yield* collectRefs(value);
676
+ continue;
677
+ }
678
+ if (value.startsWith("#/components/schemas/")) {
679
+ const name = value.slice(21);
680
+ if (name) yield name;
681
+ }
584
682
  }
585
- docCache.set($ref, current);
586
- return current;
587
683
  }
588
684
  /**
589
- * Resolves a `$ref` object while preserving the original `$ref` field on the result.
685
+ * Returns a copy of `schemas` topologically sorted by `$ref` dependency.
590
686
  *
591
- * Useful for parser flows that need both dereferenced fields and pointer
592
- * identity (for naming/import purposes). Non-reference values are returned as-is.
687
+ * Referenced schemas appear before the schemas that depend on them, so code generators
688
+ * can emit types in the correct order. Cycles are silently skipped.
593
689
  *
594
690
  * @example
595
691
  * ```ts
596
- * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
597
- * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
692
+ * const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
693
+ * // Pet appears before Order when Order.$ref points at Pet
598
694
  * ```
599
695
  */
600
- function dereferenceWithRef(document, schema) {
601
- if (isReference(schema)) return {
602
- ...schema,
603
- ...resolveRef(document, schema.$ref),
604
- $ref: schema.$ref
605
- };
606
- return schema;
696
+ function sortSchemas(schemas) {
697
+ const deps = /* @__PURE__ */ new Map();
698
+ for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
699
+ const sorted = [];
700
+ const visited = /* @__PURE__ */ new Set();
701
+ function visit(name, stack) {
702
+ if (visited.has(name) || stack.has(name)) return;
703
+ stack.add(name);
704
+ for (const child of deps.get(name) ?? []) if (deps.has(child)) visit(child, stack);
705
+ stack.delete(name);
706
+ visited.add(name);
707
+ sorted.push(name);
708
+ }
709
+ for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
710
+ const result = {};
711
+ for (const name of sorted) result[name] = schemas[name];
712
+ return result;
713
+ }
714
+ const semanticSuffixes = {
715
+ schemas: "Schema",
716
+ responses: "Response",
717
+ requestBodies: "Request"
718
+ };
719
+ /**
720
+ * Picks the collision suffix for one name-colliding schema: none when the name is unique,
721
+ * a semantic suffix (`Schema`, `Response`, `Request`) when the collision spans sources, otherwise
722
+ * a numeric suffix (`2`, `3`, …) for same-source collisions.
723
+ */
724
+ function collisionSuffix({ isSingle, hasMultipleSources, source, index }) {
725
+ if (isSingle) return "";
726
+ if (hasMultipleSources) return semanticSuffixes[source];
727
+ if (index === 0) return "";
728
+ return String(index + 1);
607
729
  }
608
730
  /**
609
- * Resolves a `$ref` slot in place: when `container[key]` holds a `$ref`, replaces it with the
610
- * resolved value and returns that value. Returns `null` when the slot is empty, cannot be resolved,
611
- * or is still a `$ref` after resolving. A non-`$ref` value is returned untouched, without writing.
731
+ * Collects component schemas from one or more sources and resolves name collisions.
732
+ *
733
+ * Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
734
+ * topologically sorted by `$ref` dependency so generators emit types in the correct order.
735
+ *
736
+ * When two or more schemas normalize to the same PascalCase name:
737
+ * - Same source → numeric suffix (`2`, `3`, …).
738
+ * - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
612
739
  *
613
740
  * @example
614
741
  * ```ts
615
- * derefInPlace<ResponseObject>({ document, container: operation.schema.responses, key: '200' })
742
+ * const { schemas, renames } = getSchemas(document, { contentType: 'application/json' }, refs)
616
743
  * ```
617
744
  */
618
- function derefInPlace({ document, container, key }) {
619
- const value = container[key];
620
- if (!isReference(value)) return value ? value : null;
621
- const resolved = resolveRef(document, value.$ref);
622
- container[key] = resolved;
623
- return resolved && !isReference(resolved) ? resolved : null;
745
+ function getSchemas(document, { contentType }, refs) {
746
+ const components = document.components;
747
+ function resolveSchemaRef(schema) {
748
+ if (!isReference(schema)) return schema;
749
+ const resolved = refs.resolve(schema.$ref);
750
+ return resolved && !isReference(resolved) ? resolved : schema;
751
+ }
752
+ const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
753
+ schema: resolveSchemaRef(schema),
754
+ source: "schemas",
755
+ originalName: name
756
+ })), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
757
+ const schema = extractSchemaFromContent(item.content, contentType);
758
+ return schema ? [{
759
+ schema: resolveSchemaRef(schema),
760
+ source,
761
+ originalName: name
762
+ }] : [];
763
+ }))];
764
+ const normalizedNames = /* @__PURE__ */ new Map();
765
+ for (const item of candidates) {
766
+ const key = pascalCase(item.originalName);
767
+ const bucket = normalizedNames.get(key) ?? [];
768
+ bucket.push(item);
769
+ normalizedNames.set(key, bucket);
770
+ }
771
+ const schemas = {};
772
+ const renames = /* @__PURE__ */ new Map();
773
+ for (const [, items] of normalizedNames) {
774
+ const isSingle = items.length === 1;
775
+ const hasMultipleSources = !isSingle && new Set(items.map((item) => item.source)).size > 1;
776
+ items.forEach((item, index) => {
777
+ const suffix = collisionSuffix({
778
+ isSingle,
779
+ hasMultipleSources,
780
+ source: item.source,
781
+ index
782
+ });
783
+ const uniqueName = item.originalName + suffix;
784
+ schemas[uniqueName] = item.schema;
785
+ if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
786
+ });
787
+ }
788
+ return {
789
+ schemas: sortSchemas(schemas),
790
+ renames
791
+ };
624
792
  }
625
793
  //#endregion
626
- //#region src/operation.ts
794
+ //#region src/model/server.ts
627
795
  /**
628
- * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
629
- * with no leading or trailing dash.
796
+ * Reads the server URL from the document's `servers` array at `server.index`,
797
+ * interpolating any `server.variables` into the URL template.
798
+ *
799
+ * Returns `null` when `server.index` is omitted or out of range.
800
+ *
801
+ * @example Resolve the first server
802
+ * `resolveBaseUrl({ document, server: { index: 0 } })`
803
+ *
804
+ * @example Override a path variable
805
+ * `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
630
806
  */
631
- function slugify(value) {
632
- return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
807
+ function resolveBaseUrl({ document, server }) {
808
+ const index = server?.index;
809
+ const entry = index !== void 0 ? document.servers?.at(index) : void 0;
810
+ return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
811
+ }
812
+ /**
813
+ * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
814
+ * Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
815
+ * Throws if an override value is not in the variable's `enum` list.
816
+ *
817
+ * @example
818
+ * ```ts
819
+ * resolveServerUrl(
820
+ * { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
821
+ * { env: 'prod' },
822
+ * )
823
+ * // 'https://prod.api.example.com'
824
+ * ```
825
+ */
826
+ function resolveServerUrl(server, overrides) {
827
+ if (!server.variables) return server.url;
828
+ let url = server.url;
829
+ for (const [key, variable] of Object.entries(server.variables)) {
830
+ const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
831
+ if (value === void 0) continue;
832
+ if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.Diagnostics.Error({
833
+ code: _kubb_core.Diagnostics.code.invalidServerVariable,
834
+ severity: "error",
835
+ message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
836
+ help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
837
+ location: {
838
+ kind: "document",
839
+ pointer: "#/servers"
840
+ }
841
+ });
842
+ url = url.replaceAll(`{${key}}`, value);
843
+ }
844
+ return url;
845
+ }
846
+ //#endregion
847
+ //#region src/operation.ts
848
+ /**
849
+ * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
850
+ * with no leading or trailing dash.
851
+ */
852
+ function slugify(value) {
853
+ return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
633
854
  }
634
855
  /**
635
856
  * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
@@ -648,26 +869,28 @@ function getResponseStatusCodes({ schema }) {
648
869
  return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
649
870
  }
650
871
  /**
651
- * Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
872
+ * Returns the response object for a status code, resolving a `$ref` through `refs`. `false` when absent.
652
873
  */
653
- function getResponseByStatusCode({ document, operation, statusCode }) {
874
+ function getResponseByStatusCode({ operation, refs, statusCode }) {
654
875
  const responses = operation.schema.responses;
655
876
  if (!responses || isReference(responses)) return false;
656
- return derefInPlace({
657
- document,
658
- container: responses,
659
- key: statusCode
660
- }) ?? false;
877
+ return refs.deref(responses[statusCode]) ?? false;
878
+ }
879
+ /**
880
+ * Resolves the operation's request body, dereferencing a `$ref` through `refs`. Returns `null`
881
+ * when the operation has no request body or it cannot be resolved.
882
+ */
883
+ function getRequestBody({ operation, refs }) {
884
+ return refs.deref(operation.schema.requestBody);
661
885
  }
662
886
  /**
663
- * Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
887
+ * Resolves the request body (a `$ref` through `refs`) and returns its content map, or
664
888
  * `undefined` when the operation has no request body.
665
889
  */
666
- function getRequestBodyContent({ document, operation }) {
667
- return derefInPlace({
668
- document,
669
- container: operation.schema,
670
- key: "requestBody"
890
+ function getRequestBodyContent({ operation, refs }) {
891
+ return getRequestBody({
892
+ operation,
893
+ refs
671
894
  })?.content;
672
895
  }
673
896
  /**
@@ -675,25 +898,23 @@ function getRequestBodyContent({ document, operation }) {
675
898
  * Otherwise picks the first JSON-like media type, then the first declared one, as a
676
899
  * `[mediaType, object]` tuple.
677
900
  */
678
- function getRequestContent({ document, operation, mediaType }) {
901
+ function getRequestContent({ operation, refs, mediaType }) {
679
902
  const content = getRequestBodyContent({
680
- document,
681
- operation
903
+ operation,
904
+ refs
682
905
  });
683
906
  if (!content) return false;
684
907
  if (mediaType) return mediaType in content ? content[mediaType] : false;
685
- const mediaTypes = Object.keys(content);
686
- const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
687
- return available ? [available, content[available]] : false;
908
+ return pickContentEntry(content);
688
909
  }
689
910
  /**
690
911
  * Returns the primary request content type. Prefers a JSON-like media type (the last one wins
691
912
  * when several are declared), then the first declared one, defaulting to `'application/json'`.
692
913
  */
693
- function getRequestContentType({ document, operation }) {
914
+ function getRequestContentType({ operation, refs }) {
694
915
  const content = getRequestBodyContent({
695
- document,
696
- operation
916
+ operation,
917
+ refs
697
918
  });
698
919
  const mediaTypes = content ? Object.keys(content) : [];
699
920
  let result = mediaTypes[0] ?? "application/json";
@@ -706,22 +927,18 @@ function getRequestContentType({ document, operation }) {
706
927
  *
707
928
  * @example
708
929
  * ```ts
709
- * for (const operation of getOperations(document)) {
930
+ * for (const operation of getOperations(document, refs)) {
710
931
  * parseOperation(options, operation)
711
932
  * }
712
933
  * ```
713
934
  */
714
- function getOperations(document) {
935
+ function getOperations(document, refs) {
715
936
  const operations = [];
716
937
  const paths = document.paths;
717
938
  if (!paths) return operations;
718
939
  for (const path of Object.keys(paths)) {
719
940
  if (path.startsWith("x-")) continue;
720
- const pathItem = derefInPlace({
721
- document,
722
- container: paths,
723
- key: path
724
- });
941
+ const pathItem = refs.deref(paths[path]);
725
942
  if (!pathItem) continue;
726
943
  const item = pathItem;
727
944
  for (const method of Object.keys(item)) {
@@ -731,75 +948,25 @@ function getOperations(document) {
731
948
  operations.push({
732
949
  path,
733
950
  method,
734
- schema
951
+ schema,
952
+ pathItem
735
953
  });
736
954
  }
737
955
  }
738
956
  return operations;
739
957
  }
740
958
  //#endregion
741
- //#region src/resolvers.ts
742
- /**
743
- * Reads the server URL from the document's `servers` array at `server.index`,
744
- * interpolating any `server.variables` into the URL template.
745
- *
746
- * Returns `null` when `server.index` is omitted or out of range.
747
- *
748
- * @example Resolve the first server
749
- * `resolveBaseUrl({ document, server: { index: 0 } })`
750
- *
751
- * @example Override a path variable
752
- * `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
753
- */
754
- function resolveBaseUrl({ document, server }) {
755
- const index = server?.index;
756
- const entry = index !== void 0 ? document.servers?.at(index) : void 0;
757
- return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
758
- }
759
- /**
760
- * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
761
- * Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
762
- * Throws if an override value is not in the variable's `enum` list.
763
- *
764
- * @example
765
- * ```ts
766
- * resolveServerUrl(
767
- * { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
768
- * { env: 'prod' },
769
- * )
770
- * // 'https://prod.api.example.com'
771
- * ```
772
- */
773
- function resolveServerUrl(server, overrides) {
774
- if (!server.variables) return server.url;
775
- let url = server.url;
776
- for (const [key, variable] of Object.entries(server.variables)) {
777
- const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
778
- if (value === void 0) continue;
779
- if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.Diagnostics.Error({
780
- code: _kubb_core.Diagnostics.code.invalidServerVariable,
781
- severity: "error",
782
- message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
783
- help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
784
- location: {
785
- kind: "document",
786
- pointer: "#/servers"
787
- }
788
- });
789
- url = url.replaceAll(`{${key}}`, value);
790
- }
791
- return url;
792
- }
959
+ //#region src/emit/schemaShape.ts
793
960
  /**
794
961
  * Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
795
- * Formats not in `formatMap` (e.g., `int64`, `date-time`) are handled separately by parser options.
962
+ * Formats not in `formatMap` (e.g., `int64`, `uint64`, `date-time`) are handled separately by parser options.
796
963
  */
797
964
  function getSchemaType(format) {
798
965
  return formatMap[format] ?? null;
799
966
  }
800
967
  /**
801
968
  * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
802
- * `specialCasedFormats` that `convertFormat` handles directly. False means the format falls back to
969
+ * `specialCasedFormats` that `convertFormat` handles directly (int64, uint64, date-time, date, time). False means the format falls back to
803
970
  * the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
804
971
  * diagnostic in step with the parser as `formatMap` grows.
805
972
  */
@@ -816,96 +983,57 @@ function getPrimitiveType(type) {
816
983
  return "string";
817
984
  }
818
985
  /**
819
- * Returns all parameters for an operation, merging path-level and operation-level entries.
820
- * Operation-level parameters override path-level ones with the same `in:name` key.
821
- * Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
822
- *
823
- * @example
824
- * ```ts
825
- * getParameters(document, operation)
826
- * // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
827
- * ```
986
+ * Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
987
+ * Returns `null` when `dateType: false`, so the format falls through to `string`.
828
988
  */
829
- function getParameters(document, operation) {
830
- const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
831
- const operationParams = resolveParams(operation.schema?.parameters || []);
832
- const pathItem = document.paths?.[operation.path];
833
- const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : []);
834
- const paramMap = /* @__PURE__ */ new Map();
835
- for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
836
- for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
837
- return Array.from(paramMap.values());
838
- }
839
- function getResponseBody(responseBody, contentType) {
840
- if (!responseBody) return false;
841
- if (isReference(responseBody)) return false;
842
- const body = responseBody;
843
- if (!body.content) return false;
844
- if (contentType) {
845
- if (!(contentType in body.content)) return false;
846
- return body.content[contentType];
989
+ function getDateType(options, format) {
990
+ if (!options.dateType) return null;
991
+ if (format === "date-time") {
992
+ if (options.dateType === "date") return {
993
+ type: "date",
994
+ representation: "date"
995
+ };
996
+ if (options.dateType === "stringOffset") return {
997
+ type: "datetime",
998
+ offset: true
999
+ };
1000
+ if (options.dateType === "stringLocal") return {
1001
+ type: "datetime",
1002
+ local: true
1003
+ };
1004
+ return {
1005
+ type: "datetime",
1006
+ offset: false
1007
+ };
847
1008
  }
848
- const contentTypes = Object.keys(body.content);
849
- const availableContentType = contentTypes.find(isJsonMimeType) ?? contentTypes[0];
850
- if (!availableContentType) return false;
851
- return body.content[availableContentType];
852
- }
853
- function resolveResponseRefs(document, operation) {
854
- const responses = operation.schema.responses;
855
- if (!responses) return;
856
- for (const key in responses) derefInPlace({
857
- document,
858
- container: responses,
859
- key
860
- });
1009
+ if (format === "date") return {
1010
+ type: "date",
1011
+ representation: options.dateType === "date" ? "date" : "string"
1012
+ };
1013
+ return {
1014
+ type: "time",
1015
+ representation: options.dateType === "date" ? "date" : "string"
1016
+ };
861
1017
  }
862
1018
  /**
863
- * Returns the response schema for a given operation and HTTP status code.
864
- *
865
- * Returns an empty object `{}` when no response body schema is available.
866
- *
867
- * @example
868
- * ```ts
869
- * getResponseSchema(document, operation, 200) // SchemaObject
870
- * getResponseSchema(document, operation, '4XX') // {}
871
- * ```
1019
+ * Reads a schema's numeric `exclusiveMinimum`/`exclusiveMaximum` bounds (the OAS 3.1 numeric
1020
+ * form). Either key is `undefined` when absent or, for the legacy OAS 3.0 boolean form, not a
1021
+ * number.
872
1022
  */
873
- function getResponseSchema(document, operation, statusCode, options = {}) {
874
- resolveResponseRefs(document, operation);
875
- const responseBody = getResponseBody(getResponseByStatusCode({
876
- document,
877
- operation,
878
- statusCode
879
- }), options.contentType);
880
- if (responseBody === false) return {};
881
- const schema = responseBody.schema;
882
- if (!schema) return {};
883
- return dereferenceWithRef(document, schema);
1023
+ function getExclusiveBounds(schema) {
1024
+ return {
1025
+ exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1026
+ exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
1027
+ };
884
1028
  }
885
1029
  /**
886
- * Returns the request body schema for an operation, or `null` when absent.
887
- *
888
- * @example
889
- * ```ts
890
- * getRequestSchema(document, operation) // SchemaObject | null
891
- * ```
1030
+ * Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
1031
+ * labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
1032
+ * 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
892
1033
  */
893
- function getRequestSchema(document, operation, options = {}) {
894
- if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
895
- const requestBody = getRequestContent({
896
- document,
897
- operation,
898
- mediaType: options.contentType
899
- });
900
- if (requestBody === false) return null;
901
- const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
902
- const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
903
- if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
904
- type: "string",
905
- contentMediaType: "application/octet-stream"
906
- };
907
- if (!schema) return null;
908
- return dereferenceWithRef(document, schema);
1034
+ function extractExamples(schema) {
1035
+ if (Array.isArray(schema.examples)) return schema.examples;
1036
+ return schema.example !== void 0 ? [schema.example] : void 0;
909
1037
  }
910
1038
  /**
911
1039
  * Returns `true` when `fragment` carries any JSON Schema keyword that makes it
@@ -914,8 +1042,7 @@ function getRequestSchema(document, operation, options = {}) {
914
1042
  * A fragment with a structural keyword can't be safely merged into a parent schema.
915
1043
  */
916
1044
  function hasStructuralKeywords(fragment) {
917
- for (const key in fragment) if (structuralKeys.has(key)) return true;
918
- return false;
1045
+ return Object.keys(fragment).some((key) => structuralKeys.has(key));
919
1046
  }
920
1047
  /**
921
1048
  * Flattens a keyword-only `allOf` into its parent schema.
@@ -946,192 +1073,15 @@ function flattenSchema(schema) {
946
1073
  for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
947
1074
  return merged;
948
1075
  }
1076
+ //#endregion
1077
+ //#region src/emit/createNode.ts
949
1078
  /**
950
- * Extracts the inline schema from a media-type `content` map.
951
- *
952
- * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
953
- * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
954
- *
955
- * @example
956
- * ```ts
957
- * extractSchemaFromContent(operation.content, 'application/json')
958
- * // SchemaObject | null
959
- * ```
960
- */
961
- function extractSchemaFromContent(content, preferredContentType) {
962
- if (!content) return null;
963
- const firstContentType = Object.keys(content)[0] ?? "application/json";
964
- const schema = content[preferredContentType ?? firstContentType]?.schema;
965
- if (schema && "$ref" in schema) return null;
966
- return schema ?? null;
967
- }
968
- /**
969
- * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
970
- */
971
- function* collectRefs(schema) {
972
- if (Array.isArray(schema)) {
973
- for (const item of schema) yield* collectRefs(item);
974
- return;
975
- }
976
- if (schema && typeof schema === "object") for (const key in schema) {
977
- const value = schema[key];
978
- if (!(key === "$ref" && typeof value === "string")) {
979
- yield* collectRefs(value);
980
- continue;
981
- }
982
- if (value.startsWith("#/components/schemas/")) {
983
- const name = value.slice(21);
984
- if (name) yield name;
985
- }
986
- }
987
- }
988
- /**
989
- * Returns a copy of `schemas` topologically sorted by `$ref` dependency.
990
- *
991
- * Referenced schemas appear before the schemas that depend on them, so code generators
992
- * can emit types in the correct order. Cycles are silently skipped.
993
- *
994
- * @example
995
- * ```ts
996
- * const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
997
- * // Pet appears before Order when Order.$ref points at Pet
998
- * ```
999
- */
1000
- function sortSchemas(schemas) {
1001
- const deps = /* @__PURE__ */ new Map();
1002
- for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
1003
- const sorted = [];
1004
- const visited = /* @__PURE__ */ new Set();
1005
- function visit(name, stack) {
1006
- if (visited.has(name) || stack.has(name)) return;
1007
- stack.add(name);
1008
- for (const child of deps.get(name) ?? []) if (deps.has(child)) visit(child, stack);
1009
- stack.delete(name);
1010
- visited.add(name);
1011
- sorted.push(name);
1012
- }
1013
- for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
1014
- const result = {};
1015
- for (const name of sorted) result[name] = schemas[name];
1016
- return result;
1017
- }
1018
- const semanticSuffixes = {
1019
- schemas: "Schema",
1020
- responses: "Response",
1021
- requestBodies: "Request"
1022
- };
1023
- function resolveSchemaRef(document, schema) {
1024
- if (!isReference(schema)) return schema;
1025
- const resolved = resolveRef(document, schema.$ref);
1026
- return resolved && !isReference(resolved) ? resolved : schema;
1027
- }
1028
- /**
1029
- * Collects component schemas from one or more sources and resolves name collisions.
1030
- *
1031
- * Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
1032
- * topologically sorted by `$ref` dependency so generators emit types in the correct order.
1033
- *
1034
- * When two or more schemas normalize to the same PascalCase name:
1035
- * - Same source → numeric suffix (`2`, `3`, …).
1036
- * - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
1037
- *
1038
- * @example
1039
- * ```ts
1040
- * const { schemas, renames } = getSchemas(document, { contentType: 'application/json' })
1041
- * ```
1042
- */
1043
- function getSchemas(document, { contentType }) {
1044
- const components = document.components;
1045
- const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
1046
- schema: resolveSchemaRef(document, schema),
1047
- source: "schemas",
1048
- originalName: name
1049
- })), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
1050
- const schema = extractSchemaFromContent(item.content, contentType);
1051
- return schema ? [{
1052
- schema: resolveSchemaRef(document, schema),
1053
- source,
1054
- originalName: name
1055
- }] : [];
1056
- }))];
1057
- const normalizedNames = /* @__PURE__ */ new Map();
1058
- for (const item of candidates) {
1059
- const key = pascalCase(item.originalName);
1060
- const bucket = normalizedNames.get(key) ?? [];
1061
- bucket.push(item);
1062
- normalizedNames.set(key, bucket);
1063
- }
1064
- const schemas = {};
1065
- const renames = /* @__PURE__ */ new Map();
1066
- for (const [, items] of normalizedNames) {
1067
- const isSingle = items.length === 1;
1068
- let hasMultipleSources = false;
1069
- if (!isSingle) {
1070
- const firstSource = items[0].source;
1071
- for (const item of items) if (item.source !== firstSource) {
1072
- hasMultipleSources = true;
1073
- break;
1074
- }
1075
- }
1076
- items.forEach((item, index) => {
1077
- const suffix = isSingle ? "" : hasMultipleSources ? semanticSuffixes[item.source] : index === 0 ? "" : String(index + 1);
1078
- const uniqueName = item.originalName + suffix;
1079
- schemas[uniqueName] = item.schema;
1080
- if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
1081
- });
1082
- }
1083
- return {
1084
- schemas: sortSchemas(schemas),
1085
- renames
1086
- };
1087
- }
1088
- /**
1089
- * Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
1090
- * Returns `null` when `dateType: false`, so the format falls through to `string`.
1091
- */
1092
- function getDateType(options, format) {
1093
- if (!options.dateType) return null;
1094
- if (format === "date-time") {
1095
- if (options.dateType === "date") return {
1096
- type: "date",
1097
- representation: "date"
1098
- };
1099
- if (options.dateType === "stringOffset") return {
1100
- type: "datetime",
1101
- offset: true
1102
- };
1103
- if (options.dateType === "stringLocal") return {
1104
- type: "datetime",
1105
- local: true
1106
- };
1107
- return {
1108
- type: "datetime",
1109
- offset: false
1110
- };
1111
- }
1112
- if (format === "date") return {
1113
- type: "date",
1114
- representation: options.dateType === "date" ? "date" : "string"
1115
- };
1116
- return {
1117
- type: "time",
1118
- representation: options.dateType === "date" ? "date" : "string"
1119
- };
1120
- }
1121
- /**
1122
- * Collects the shared metadata fields passed to every `createSchema` call.
1123
- */
1124
- /**
1125
- * Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
1126
- * labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
1127
- * 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
1079
+ * Builds a schema node from a converter's base context plus its type-specific fields. Every
1080
+ * converter needs the same metadata fields (`title`, `description`, `examples`, ...) alongside
1081
+ * whatever makes its node distinct; this folds both into one call.
1128
1082
  */
1129
- function extractExamples(schema) {
1130
- if (Array.isArray(schema.examples)) return schema.examples;
1131
- return schema.example !== void 0 ? [schema.example] : void 0;
1132
- }
1133
- function buildSchemaNode(schema, name, nullable, defaultValue) {
1134
- return {
1083
+ function createNode({ schema, name, nullable, defaultValue }, extras) {
1084
+ return _kubb_ast.ast.factory.createSchema({
1135
1085
  name,
1136
1086
  nullable,
1137
1087
  title: schema.title,
@@ -1141,125 +1091,166 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
1141
1091
  writeOnly: schema.writeOnly,
1142
1092
  default: defaultValue,
1143
1093
  examples: extractExamples(schema),
1144
- format: schema.format
1145
- };
1094
+ format: schema.format,
1095
+ ...extras
1096
+ });
1146
1097
  }
1098
+ //#endregion
1099
+ //#region src/emit/discriminator/preserve.ts
1147
1100
  /**
1148
- * Returns all request body content type keys for an operation.
1149
- *
1150
- * The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
1151
- * `getRequestSchema` already performs), so the returned list accurately reflects the
1152
- * available content types even for referenced bodies.
1101
+ * Creates a single-property object schema used as a discriminator literal.
1153
1102
  *
1154
1103
  * @example
1155
1104
  * ```ts
1156
- * getRequestBodyContentTypes(document, operation)
1157
- * // ['application/json', 'multipart/form-data']
1105
+ * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
1106
+ * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
1158
1107
  * ```
1159
1108
  */
1160
- function getRequestBodyContentTypes(document, operation) {
1161
- if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
1162
- const body = operation.schema.requestBody;
1163
- if (!body) return [];
1164
- return body.content ? Object.keys(body.content) : [];
1109
+ function createDiscriminantNode({ propertyName, value }) {
1110
+ return _kubb_ast.ast.factory.createSchema({
1111
+ type: "object",
1112
+ primitive: "object",
1113
+ properties: [_kubb_ast.ast.factory.createProperty({
1114
+ name: propertyName,
1115
+ schema: _kubb_ast.ast.factory.createSchema({
1116
+ type: "enum",
1117
+ primitive: "string",
1118
+ enumValues: [value]
1119
+ }),
1120
+ required: true
1121
+ })]
1122
+ });
1165
1123
  }
1166
1124
  /**
1167
- * Returns all response content type keys for an operation at a given status code.
1168
- *
1169
- * Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
1170
- * so the returned list reflects the available content types even for referenced responses.
1125
+ * Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
1171
1126
  *
1172
1127
  * @example
1173
1128
  * ```ts
1174
- * getResponseBodyContentTypes(document, operation, 200)
1175
- * // ['application/json', 'application/xml']
1129
+ * findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
1176
1130
  * ```
1177
1131
  */
1178
- function getResponseBodyContentTypes(document, operation, statusCode) {
1179
- resolveResponseRefs(document, operation);
1180
- const responseObj = getResponseByStatusCode({
1181
- document,
1182
- operation,
1183
- statusCode
1184
- });
1185
- if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
1186
- const body = responseObj;
1187
- return body.content ? Object.keys(body.content) : [];
1188
- }
1189
- //#endregion
1190
- //#region src/converters.ts
1191
- /**
1192
- * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
1193
- *
1194
- * This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
1195
- * from the array to its items sub-schema, so they are valid for downstream processing.
1196
- *
1197
- * @note A defensive measure for non-compliant specs.
1198
- */
1199
- function normalizeArrayEnum(schema) {
1200
- const normalizedItems = {
1201
- ...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
1202
- enum: schema.enum
1203
- };
1204
- const { enum: _enum, ...schemaWithoutEnum } = schema;
1205
- return {
1206
- ...schemaWithoutEnum,
1207
- items: normalizedItems
1208
- };
1132
+ function findDiscriminator(mapping, ref) {
1133
+ if (!mapping || !ref) return null;
1134
+ return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
1209
1135
  }
1210
1136
  /**
1211
- * Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
1212
- * and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
1137
+ * Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
1138
+ * node with either the shared-properties slice carrying that value, or a synthetic discriminant
1139
+ * literal. The referenced child schema's own definition is left untouched — narrowing happens only
1140
+ * at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
1141
+ * which additionally patches the child schema's own definition in a post-pass).
1213
1142
  */
1214
- function createNullNode(schema, name, nullable) {
1215
- return _kubb_ast.ast.factory.createSchema({
1216
- type: "null",
1217
- primitive: "null",
1218
- name,
1219
- title: schema.title,
1220
- description: schema.description,
1221
- deprecated: schema.deprecated,
1222
- nullable,
1223
- format: schema.format
1143
+ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, rawOptions, name, refs }) {
1144
+ function pickDiscriminatorPropertyNode(node, propertyName) {
1145
+ const discriminatorProperty = _kubb_ast.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1146
+ if (!discriminatorProperty) return null;
1147
+ return _kubb_ast.ast.factory.createSchema({
1148
+ type: "object",
1149
+ primitive: "object",
1150
+ properties: [discriminatorProperty]
1151
+ });
1152
+ }
1153
+ function implicitDiscriminantValue(member) {
1154
+ if (!discriminator || discriminator.mapping || !isReference(member)) return null;
1155
+ const value = (0, _kubb_kit.extractRefName)(member.$ref);
1156
+ if (!value) return null;
1157
+ const variant = refs.resolve(member.$ref, { report: false });
1158
+ if (!variant) return null;
1159
+ const propertyName = discriminator.propertyName;
1160
+ const seen = /* @__PURE__ */ new Set([member.$ref]);
1161
+ function constrains(v) {
1162
+ const prop = v.properties?.[propertyName];
1163
+ const resolved = prop && isReference(prop) ? refs.resolve(prop.$ref, { report: false }) : prop;
1164
+ if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
1165
+ const composition = v.allOf ?? v.oneOf ?? v.anyOf;
1166
+ if (!composition) return false;
1167
+ return composition.some((m) => {
1168
+ if (!isReference(m)) return constrains(m);
1169
+ if (seen.has(m.$ref)) return false;
1170
+ seen.add(m.$ref);
1171
+ const r = refs.resolve(m.$ref, { report: false });
1172
+ return r ? constrains(r) : false;
1173
+ });
1174
+ }
1175
+ return constrains(variant) ? null : value;
1176
+ }
1177
+ return unionMembers.map((s) => {
1178
+ const ref = isReference(s) ? s.$ref : void 0;
1179
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
1180
+ const memberNode = parse({
1181
+ schema: s,
1182
+ name
1183
+ }, rawOptions);
1184
+ if (!discriminatorValue || !discriminator) return memberNode;
1185
+ const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(_kubb_ast.ast.applyMacros(sharedPropertiesNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
1186
+ propertyName: discriminator.propertyName,
1187
+ values: [discriminatorValue]
1188
+ })], { depth: "shallow" }), discriminator.propertyName) : void 0;
1189
+ return _kubb_ast.ast.factory.createSchema({
1190
+ type: "intersection",
1191
+ members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
1192
+ propertyName: discriminator.propertyName,
1193
+ value: discriminatorValue
1194
+ })]
1195
+ });
1224
1196
  });
1225
1197
  }
1226
1198
  /**
1227
- * Names the inline enums on a property's schema, and on each item when the property is a tuple, from
1228
- * the parent and property name. Wraps `macroEnumName` at the property construction site.
1199
+ * Filters the discriminated members out of an `allOf` list: an `allOf` member that `$ref`s a
1200
+ * discriminated union's parent, where this schema is itself one of that union's children, is
1201
+ * dropped from `members` and its discriminant value collected instead — the same synthetic
1202
+ * literal `narrowUnionMembers` produces, so the emitted node stays a plain intersection rather
1203
+ * than nesting the whole parent union one level deeper.
1229
1204
  */
1230
- function nameEnums(node, options) {
1231
- const macro = (0, _kubb_ast.macroEnumName)(options);
1232
- const named = _kubb_ast.ast.applyMacros(node, [macro], { depth: "shallow" });
1233
- const tupleNode = _kubb_ast.ast.narrowSchema(named, "tuple");
1234
- if (tupleNode?.items) {
1235
- const namedItems = tupleNode.items.map((item) => _kubb_ast.ast.applyMacros(item, [macro], { depth: "shallow" }));
1236
- if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
1237
- ...tupleNode,
1238
- items: namedItems
1239
- };
1240
- }
1241
- return named;
1205
+ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
1206
+ const discriminantValues = [];
1207
+ return {
1208
+ members: allOfMembers.filter((item) => {
1209
+ if (!isReference(item) || !name) return true;
1210
+ const deref = refs.resolve(item.$ref);
1211
+ if (!deref || !isDiscriminator(deref)) return true;
1212
+ const parentUnion = deref.oneOf ?? deref.anyOf;
1213
+ if (!parentUnion) return true;
1214
+ const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1215
+ const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1216
+ const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1217
+ if (inOneOf || inMapping) {
1218
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
1219
+ if (discriminatorValue) discriminantValues.push({
1220
+ propertyName: deref.discriminator.propertyName,
1221
+ value: discriminatorValue
1222
+ });
1223
+ return false;
1224
+ }
1225
+ return true;
1226
+ }),
1227
+ discriminantValues
1228
+ };
1242
1229
  }
1230
+ //#endregion
1231
+ //#region src/emit/converters/composition.ts
1243
1232
  /**
1244
1233
  * Converts a `$ref` schema into a `RefSchemaNode`.
1245
1234
  *
1246
1235
  * The resolved schema is stored in `node.schema`. Usage-site sibling fields
1247
1236
  * (description, readOnly, nullable, etc.) are stored directly on the ref node.
1248
1237
  * Use `syncSchemaRef(node)` in printers to get a merged view of both.
1249
- * Circular refs are detected in `resolveRefNode` and leave `schema` as `null`.
1238
+ * Circular refs are detected in `refs.resolveNode` and leave `schema` as `null`.
1250
1239
  */
1251
- function convertRef({ schema, name, nullable, defaultValue, rawOptions, document, resolveRefNode, refExists, renames }) {
1240
+ function convertRef({ schema, name, nullable, defaultValue, rawOptions, document, parse, refs, renames }) {
1252
1241
  const refPath = schema.$ref;
1253
- const resolvedSchema = refPath ? resolveRefNode(refPath, rawOptions) : null;
1254
- if (refPath && document.components && !refExists(refPath)) return _kubb_ast.ast.factory.createSchema({
1255
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1256
- type: "unknown"
1257
- });
1242
+ const resolvedSchema = refPath ? refs.resolveNode(refPath, parse, rawOptions) : null;
1243
+ const ctx = {
1244
+ schema,
1245
+ name,
1246
+ nullable,
1247
+ defaultValue
1248
+ };
1249
+ if (refPath && document.components && !refs.exists(refPath)) return createNode(ctx, { type: "unknown" });
1258
1250
  const targetName = renames?.get(schema.$ref);
1259
- return _kubb_ast.ast.factory.createSchema({
1260
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1251
+ return createNode(ctx, {
1261
1252
  type: "ref",
1262
- name: (0, _kubb_ast.extractRefName)(schema.$ref),
1253
+ name: (0, _kubb_kit.extractRefName)(schema.$ref),
1263
1254
  ref: schema.$ref,
1264
1255
  ...targetName ? { targetName } : {},
1265
1256
  schema: resolvedSchema
@@ -1268,7 +1259,7 @@ function convertRef({ schema, name, nullable, defaultValue, rawOptions, document
1268
1259
  /**
1269
1260
  * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
1270
1261
  */
1271
- function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse, document }) {
1262
+ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
1272
1263
  if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
1273
1264
  const [memberSchema] = schema.allOf;
1274
1265
  const memberNode = parse({
@@ -1293,26 +1284,12 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
1293
1284
  format: schema.format ?? memberNode.format
1294
1285
  });
1295
1286
  }
1296
- const filteredDiscriminantValues = [];
1297
- const allOfMembers = schema.allOf.filter((item) => {
1298
- if (!isReference(item) || !name) return true;
1299
- const deref = resolveRef(document, item.$ref);
1300
- if (!deref || !isDiscriminator(deref)) return true;
1301
- const parentUnion = deref.oneOf ?? deref.anyOf;
1302
- if (!parentUnion) return true;
1303
- const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1304
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1305
- const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1306
- if (inOneOf || inMapping) {
1307
- const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
1308
- if (discriminatorValue) filteredDiscriminantValues.push({
1309
- propertyName: deref.discriminator.propertyName,
1310
- value: discriminatorValue
1311
- });
1312
- return false;
1313
- }
1314
- return true;
1315
- }).map((s) => parse({
1287
+ const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
1288
+ allOfMembers: schema.allOf,
1289
+ name,
1290
+ refs
1291
+ });
1292
+ const allOfMembers = discriminatedAllOf.map((s) => parse({
1316
1293
  schema: s,
1317
1294
  name
1318
1295
  }, rawOptions));
@@ -1323,7 +1300,7 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
1323
1300
  if (missingRequired.length) {
1324
1301
  const resolvedMembers = schema.allOf.flatMap((item) => {
1325
1302
  if (!isReference(item)) return [item];
1326
- const deref = resolveRef(document, item.$ref);
1303
+ const deref = refs.resolve(item.$ref);
1327
1304
  return deref && !isReference(deref) ? [deref] : [];
1328
1305
  });
1329
1306
  for (const key of missingRequired) for (const resolved of resolvedMembers) {
@@ -1346,61 +1323,33 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
1346
1323
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
1347
1324
  allOfMembers.push(parse({ schema: schemaWithoutAllOf }, rawOptions));
1348
1325
  }
1349
- for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(createDiscriminantNode({
1326
+ for (const { propertyName, value } of discriminantValues) allOfMembers.push(createDiscriminantNode({
1350
1327
  propertyName,
1351
1328
  value
1352
1329
  }));
1353
- return _kubb_ast.ast.factory.createSchema({
1330
+ return createNode({
1331
+ schema,
1332
+ name,
1333
+ nullable,
1334
+ defaultValue
1335
+ }, {
1354
1336
  type: "intersection",
1355
- members: [...(0, _kubb_ast.mergeAdjacentObjectsLazy)(allOfMembers.slice(0, syntheticStart)), ...(0, _kubb_ast.mergeAdjacentObjectsLazy)(allOfMembers.slice(syntheticStart))],
1356
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1337
+ members: [...(0, _kubb_kit.mergeAdjacentObjectsLazy)(allOfMembers.slice(0, syntheticStart)), ...(0, _kubb_kit.mergeAdjacentObjectsLazy)(allOfMembers.slice(syntheticStart))]
1357
1338
  });
1358
1339
  }
1359
1340
  /**
1360
1341
  * Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
1361
1342
  */
1362
- function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse, document }) {
1363
- function pickDiscriminatorPropertyNode(node, propertyName) {
1364
- const discriminatorProperty = _kubb_ast.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1365
- if (!discriminatorProperty) return null;
1366
- return _kubb_ast.ast.factory.createSchema({
1367
- type: "object",
1368
- primitive: "object",
1369
- properties: [discriminatorProperty]
1370
- });
1371
- }
1372
- function resolveRefSilent($ref) {
1373
- if (!$ref.startsWith("#")) return null;
1374
- return decodeURIComponent($ref.substring(1)).split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document) ?? null;
1375
- }
1376
- function implicitDiscriminantValue(member) {
1377
- if (!discriminator || discriminator.mapping || !isReference(member)) return null;
1378
- const value = (0, _kubb_ast.extractRefName)(member.$ref);
1379
- if (!value) return null;
1380
- const variant = resolveRefSilent(member.$ref);
1381
- if (!variant) return null;
1382
- const propertyName = discriminator.propertyName;
1383
- const seen = /* @__PURE__ */ new Set([member.$ref]);
1384
- function constrains(v) {
1385
- const prop = v.properties?.[propertyName];
1386
- const resolved = prop && isReference(prop) ? resolveRefSilent(prop.$ref) : prop;
1387
- if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
1388
- const composition = v.allOf ?? v.oneOf ?? v.anyOf;
1389
- if (!composition) return false;
1390
- return composition.some((m) => {
1391
- if (!isReference(m)) return constrains(m);
1392
- if (seen.has(m.$ref)) return false;
1393
- seen.add(m.$ref);
1394
- const r = resolveRefSilent(m.$ref);
1395
- return r ? constrains(r) : false;
1396
- });
1397
- }
1398
- return constrains(variant) ? null : value;
1399
- }
1343
+ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
1344
+ const ctx = {
1345
+ schema,
1346
+ name,
1347
+ nullable,
1348
+ defaultValue
1349
+ };
1400
1350
  const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
1401
1351
  const strategy = schema.oneOf ? "one" : "any";
1402
- const unionBase = {
1403
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1352
+ const unionExtras = {
1404
1353
  discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1405
1354
  strategy
1406
1355
  };
@@ -1411,47 +1360,99 @@ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
1411
1360
  name
1412
1361
  }, rawOptions) : void 0;
1413
1362
  if (sharedPropertiesNode || discriminator) {
1414
- const members = unionMembers.map((s) => {
1415
- const ref = isReference(s) ? s.$ref : void 0;
1416
- const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
1417
- const memberNode = parse({
1418
- schema: s,
1419
- name
1420
- }, rawOptions);
1421
- if (!discriminatorValue || !discriminator) return memberNode;
1422
- const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(_kubb_ast.ast.applyMacros(sharedPropertiesNode, [(0, _kubb_ast.macroDiscriminatorEnum)({
1423
- propertyName: discriminator.propertyName,
1424
- values: [discriminatorValue]
1425
- })], { depth: "shallow" }), discriminator.propertyName) : void 0;
1426
- return _kubb_ast.ast.factory.createSchema({
1427
- type: "intersection",
1428
- members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
1429
- propertyName: discriminator.propertyName,
1430
- value: discriminatorValue
1431
- })]
1432
- });
1363
+ const members = narrowUnionMembers({
1364
+ unionMembers,
1365
+ discriminator,
1366
+ sharedPropertiesNode,
1367
+ parse,
1368
+ rawOptions,
1369
+ name,
1370
+ refs
1433
1371
  });
1434
- const unionNode = _kubb_ast.ast.factory.createSchema({
1372
+ const unionNode = createNode(ctx, {
1435
1373
  type: "union",
1436
- ...unionBase,
1374
+ ...unionExtras,
1437
1375
  members
1438
1376
  });
1439
1377
  if (!sharedPropertiesNode) return unionNode;
1440
- return _kubb_ast.ast.factory.createSchema({
1378
+ return createNode(ctx, {
1441
1379
  type: "intersection",
1442
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1443
1380
  members: [unionNode, sharedPropertiesNode]
1444
1381
  });
1445
1382
  }
1446
- const unionNode = _kubb_ast.ast.factory.createSchema({
1383
+ const unionNode = createNode(ctx, {
1447
1384
  type: "union",
1448
- ...unionBase,
1385
+ ...unionExtras,
1449
1386
  members: unionMembers.map((s) => parse({
1450
1387
  schema: s,
1451
1388
  name
1452
1389
  }, rawOptions))
1453
1390
  });
1454
- return _kubb_ast.ast.applyMacros(unionNode, [_kubb_ast.macroSimplifyUnion], { depth: "shallow" });
1391
+ return _kubb_ast.ast.applyMacros(unionNode, [_kubb_kit.macroSimplifyUnion], { depth: "shallow" });
1392
+ }
1393
+ /**
1394
+ * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
1395
+ * Only called once the multi-type rule's `match` has confirmed more than one non-`null` type
1396
+ * remains; a single remaining type (e.g. `['string', 'null']`) is handled as that type instead,
1397
+ * with nullability already folded in.
1398
+ */
1399
+ function convertMultiType({ schema, name, nullable, defaultValue, rawOptions, parse }) {
1400
+ const types = schema.type;
1401
+ const nonNullTypes = types.filter((t) => t !== "null");
1402
+ return createNode({
1403
+ schema,
1404
+ name,
1405
+ nullable: types.includes("null") || nullable || void 0,
1406
+ defaultValue
1407
+ }, {
1408
+ type: "union",
1409
+ members: nonNullTypes.map((t) => {
1410
+ return parse({
1411
+ schema: {
1412
+ ...schema,
1413
+ type: t
1414
+ },
1415
+ name
1416
+ }, rawOptions);
1417
+ })
1418
+ });
1419
+ }
1420
+ //#endregion
1421
+ //#region src/emit/converters/scalar.ts
1422
+ /**
1423
+ * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
1424
+ *
1425
+ * This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
1426
+ * from the array to its items sub-schema, so they are valid for downstream processing.
1427
+ *
1428
+ * @note A defensive measure for non-compliant specs.
1429
+ */
1430
+ function normalizeArrayEnum(schema) {
1431
+ const normalizedItems = {
1432
+ ...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
1433
+ enum: schema.enum
1434
+ };
1435
+ const { enum: _enum, ...schemaWithoutEnum } = schema;
1436
+ return {
1437
+ ...schemaWithoutEnum,
1438
+ items: normalizedItems
1439
+ };
1440
+ }
1441
+ /**
1442
+ * Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
1443
+ * and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
1444
+ */
1445
+ function createNullNode(schema, name, nullable) {
1446
+ return _kubb_ast.ast.factory.createSchema({
1447
+ type: "null",
1448
+ primitive: "null",
1449
+ name,
1450
+ title: schema.title,
1451
+ description: schema.description,
1452
+ deprecated: schema.deprecated,
1453
+ nullable,
1454
+ format: schema.format
1455
+ });
1455
1456
  }
1456
1457
  /**
1457
1458
  * Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
@@ -1460,54 +1461,57 @@ function convertConst({ schema, name, nullable, defaultValue }) {
1460
1461
  const constValue = schema.const;
1461
1462
  if (constValue === null) return createNullNode(schema, name);
1462
1463
  const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
1463
- return _kubb_ast.ast.factory.createSchema({
1464
+ return createNode({
1465
+ schema,
1466
+ name,
1467
+ nullable,
1468
+ defaultValue
1469
+ }, {
1464
1470
  type: "enum",
1465
1471
  primitive: constPrimitive,
1466
- enumValues: [constValue],
1467
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1472
+ enumValues: [constValue]
1468
1473
  });
1469
1474
  }
1470
1475
  /**
1471
- * Converts a format-annotated schema into a special-type `SchemaNode`.
1472
- * Returns `null` when the format should fall through to string handling (`dateType: false`).
1476
+ * Converts a format-annotated schema into a special-type `SchemaNode`. Only called once the
1477
+ * `format` rule's `match` has confirmed the format is handled (see `isHandledFormat`) and, for
1478
+ * a date-ish format, that `dateType` is not `false`.
1473
1479
  */
1474
- function convertFormat({ schema, name, nullable, defaultValue, options }) {
1475
- const base = buildSchemaNode(schema, name, nullable, defaultValue);
1476
- if (schema.format === "int64") return _kubb_ast.ast.factory.createSchema({
1480
+ function convertFormat(context) {
1481
+ const { schema, name, nullable, defaultValue, options, type } = context;
1482
+ const ctx = {
1483
+ schema,
1484
+ name,
1485
+ nullable,
1486
+ defaultValue
1487
+ };
1488
+ if (type === "string" && numericFormats.has(schema.format)) return convertString(context);
1489
+ if (schema.format === "int64" || schema.format === "uint64") return createNode(ctx, {
1477
1490
  type: options.integerType === "bigint" ? "bigint" : "integer",
1478
1491
  primitive: "integer",
1479
- ...base,
1480
1492
  min: schema.minimum,
1481
1493
  max: schema.maximum,
1482
- exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1483
- exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
1494
+ ...getExclusiveBounds(schema)
1484
1495
  });
1485
1496
  if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
1486
1497
  const dateType = getDateType(options, schema.format);
1487
- if (!dateType) return null;
1488
- if (dateType.type === "datetime") return _kubb_ast.ast.factory.createSchema({
1489
- ...base,
1498
+ if (dateType.type === "datetime") return createNode(ctx, {
1490
1499
  primitive: "string",
1491
1500
  type: "datetime",
1492
1501
  offset: dateType.offset,
1493
1502
  local: dateType.local
1494
1503
  });
1495
- return _kubb_ast.ast.factory.createSchema({
1496
- ...base,
1504
+ return createNode(ctx, {
1497
1505
  primitive: "string",
1498
1506
  type: dateType.type,
1499
1507
  representation: dateType.representation
1500
1508
  });
1501
1509
  }
1502
1510
  const specialType = getSchemaType(schema.format);
1503
- if (!specialType) return null;
1504
- const specialPrimitive = specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string";
1505
- const hasLength = specialType === "url" || specialType === "uuid" || specialType === "email";
1506
- return _kubb_ast.ast.factory.createSchema({
1507
- ...base,
1508
- primitive: specialPrimitive,
1511
+ return createNode(ctx, {
1512
+ primitive: specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string",
1509
1513
  type: specialType,
1510
- ...hasLength ? {
1514
+ ...specialType === "url" || specialType === "uuid" || specialType === "email" ? {
1511
1515
  min: schema.minLength,
1512
1516
  max: schema.maxLength
1513
1517
  } : {}
@@ -1527,21 +1531,28 @@ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
1527
1531
  const enumNullable = nullable || nullInEnum || void 0;
1528
1532
  const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1529
1533
  const enumPrimitive = getPrimitiveType(type);
1530
- const enumBase = {
1534
+ const ctx = {
1535
+ schema,
1536
+ name,
1537
+ nullable: enumNullable,
1538
+ defaultValue: enumDefault
1539
+ };
1540
+ const enumExtras = {
1531
1541
  type: "enum",
1532
- primitive: enumPrimitive,
1533
- ...buildSchemaNode(schema, name, enumNullable, enumDefault)
1542
+ primitive: enumPrimitive
1534
1543
  };
1535
1544
  const extensionKey = enumExtensionKeys.find((key) => key in schema);
1536
1545
  const descriptionKey = enumDescriptionKeys.find((key) => key in schema);
1537
1546
  if (extensionKey || descriptionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
1538
- const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
1547
+ let enumPrimitiveType = "string";
1548
+ if (enumPrimitive === "number" || enumPrimitive === "integer") enumPrimitiveType = "number";
1549
+ else if (enumPrimitive === "boolean") enumPrimitiveType = "boolean";
1539
1550
  const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
1540
1551
  const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
1541
1552
  const uniqueValues = [...new Set(filteredValues)];
1542
1553
  const seenNames = /* @__PURE__ */ new Set();
1543
- return _kubb_ast.ast.factory.createSchema({
1544
- ...enumBase,
1554
+ return createNode(ctx, {
1555
+ ...enumExtras,
1545
1556
  primitive: enumPrimitiveType,
1546
1557
  namedEnumValues: uniqueValues.map((value, index) => ({
1547
1558
  name: String(rawEnumNames?.[index] ?? value),
@@ -1555,12 +1566,103 @@ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
1555
1566
  })
1556
1567
  });
1557
1568
  }
1558
- return _kubb_ast.ast.factory.createSchema({
1559
- ...enumBase,
1569
+ return createNode(ctx, {
1570
+ ...enumExtras,
1560
1571
  enumValues: [...new Set(filteredValues)]
1561
1572
  });
1562
1573
  }
1563
1574
  /**
1575
+ * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1576
+ */
1577
+ function convertString({ schema, name, nullable, defaultValue }) {
1578
+ return createNode({
1579
+ schema,
1580
+ name,
1581
+ nullable,
1582
+ defaultValue
1583
+ }, {
1584
+ type: "string",
1585
+ primitive: "string",
1586
+ min: schema.minLength,
1587
+ max: schema.maxLength,
1588
+ pattern: schema.pattern
1589
+ });
1590
+ }
1591
+ /**
1592
+ * Converts a `type: 'number'` or `type: 'integer'` schema.
1593
+ */
1594
+ function convertNumeric({ schema, name, nullable, defaultValue }, type) {
1595
+ return createNode({
1596
+ schema,
1597
+ name,
1598
+ nullable,
1599
+ defaultValue
1600
+ }, {
1601
+ type,
1602
+ primitive: type,
1603
+ min: schema.minimum,
1604
+ max: schema.maximum,
1605
+ ...getExclusiveBounds(schema),
1606
+ multipleOf: schema.multipleOf
1607
+ });
1608
+ }
1609
+ /**
1610
+ * Converts a `type: 'boolean'` schema.
1611
+ */
1612
+ function convertBoolean({ schema, name, nullable, defaultValue }) {
1613
+ return createNode({
1614
+ schema,
1615
+ name,
1616
+ nullable,
1617
+ defaultValue
1618
+ }, {
1619
+ type: "boolean",
1620
+ primitive: "boolean"
1621
+ });
1622
+ }
1623
+ /**
1624
+ * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
1625
+ * into a `blob` node.
1626
+ */
1627
+ function convertBinary({ schema, name, nullable, defaultValue }) {
1628
+ return createNode({
1629
+ schema,
1630
+ name,
1631
+ nullable,
1632
+ defaultValue
1633
+ }, {
1634
+ type: "blob",
1635
+ primitive: "string"
1636
+ });
1637
+ }
1638
+ //#endregion
1639
+ //#region src/emit/converters/structural.ts
1640
+ /**
1641
+ * Resolves a `true` or empty-object map schema (`additionalProperties`/`patternProperties`) to
1642
+ * `options.unknownType`, otherwise parses it as a regular schema.
1643
+ */
1644
+ function resolveMapSchema(mapSchema, options, parse, rawOptions) {
1645
+ if (mapSchema === true || typeof mapSchema === "object" && Object.keys(mapSchema).length === 0) return _kubb_ast.ast.factory.createSchema({ type: options.unknownType });
1646
+ return parse({ schema: mapSchema }, rawOptions);
1647
+ }
1648
+ /**
1649
+ * Names the inline enums on a property's schema, and on each item when the property is a tuple, from
1650
+ * the parent and property name. Wraps `macroEnumName` at the property construction site.
1651
+ */
1652
+ function nameEnums(node, options) {
1653
+ const macro = (0, _kubb_kit.macroEnumName)(options);
1654
+ const named = _kubb_ast.ast.applyMacros(node, [macro], { depth: "shallow" });
1655
+ const tupleNode = _kubb_ast.ast.narrowSchema(named, "tuple");
1656
+ if (tupleNode?.items) {
1657
+ const namedItems = tupleNode.items.map((item) => _kubb_ast.ast.applyMacros(item, [macro], { depth: "shallow" }));
1658
+ if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
1659
+ ...tupleNode,
1660
+ items: namedItems
1661
+ };
1662
+ }
1663
+ return named;
1664
+ }
1665
+ /**
1564
1666
  * Converts an object-like schema into an `ObjectSchemaNode`.
1565
1667
  */
1566
1668
  function convertObject({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
@@ -1570,7 +1672,7 @@ function convertObject({ schema, name, nullable, defaultValue, rawOptions, optio
1570
1672
  const propNullable = isNullable(resolvedPropSchema);
1571
1673
  const schemaNode = nameEnums(parse({
1572
1674
  schema: resolvedPropSchema,
1573
- name: (0, _kubb_ast.childName)(name, propName)
1675
+ name: (0, _kubb_kit.childName)(name, propName)
1574
1676
  }, rawOptions), {
1575
1677
  parentName: name,
1576
1678
  propName,
@@ -1586,146 +1688,84 @@ function convertObject({ schema, name, nullable, defaultValue, rawOptions, optio
1586
1688
  });
1587
1689
  }) : [];
1588
1690
  const additionalProperties = schema.additionalProperties;
1589
- const additionalPropertiesNode = (() => {
1590
- if (additionalProperties === true) return true;
1591
- if (additionalProperties === false) return false;
1592
- if (additionalProperties && Object.keys(additionalProperties).length > 0) return parse({ schema: additionalProperties }, rawOptions);
1593
- if (additionalProperties) return _kubb_ast.ast.factory.createSchema({ type: options.unknownType });
1594
- })();
1691
+ let additionalPropertiesNode;
1692
+ if (additionalProperties === true) additionalPropertiesNode = true;
1693
+ else if (additionalProperties) additionalPropertiesNode = resolveMapSchema(additionalProperties, options, parse, rawOptions);
1694
+ else additionalPropertiesNode = additionalProperties;
1595
1695
  const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
1596
- const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? _kubb_ast.ast.factory.createSchema({ type: options.unknownType }) : parse({ schema: patternSchema }, rawOptions)])) : void 0;
1597
- const objectNode = _kubb_ast.ast.factory.createSchema({
1696
+ const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, resolveMapSchema(patternSchema, options, parse, rawOptions)])) : void 0;
1697
+ const objectNode = createNode({
1698
+ schema,
1699
+ name,
1700
+ nullable,
1701
+ defaultValue
1702
+ }, {
1598
1703
  type: "object",
1599
1704
  primitive: "object",
1600
1705
  properties,
1601
1706
  additionalProperties: additionalPropertiesNode,
1602
1707
  patternProperties,
1603
1708
  minProperties: schema.minProperties,
1604
- maxProperties: schema.maxProperties,
1605
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1709
+ maxProperties: schema.maxProperties
1606
1710
  });
1607
1711
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
1608
- const discPropName = schema.discriminator.propertyName;
1609
- const values = Object.keys(schema.discriminator.mapping);
1610
- const enumName = name ? (0, _kubb_ast.enumPropName)(name, discPropName, options.enumSuffix) : void 0;
1611
- return _kubb_ast.ast.applyMacros(objectNode, [(0, _kubb_ast.macroDiscriminatorEnum)({
1612
- propertyName: discPropName,
1613
- values,
1614
- enumName
1615
- })], { depth: "shallow" });
1616
- }
1617
- return objectNode;
1618
- }
1619
- /**
1620
- * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
1621
- */
1622
- function convertTuple({ schema, name, nullable, defaultValue, rawOptions, parse }) {
1623
- const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
1624
- const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? _kubb_ast.ast.factory.createSchema({ type: "any" }) : parse({ schema: schema.items }, rawOptions);
1625
- return _kubb_ast.ast.factory.createSchema({
1626
- type: "tuple",
1627
- primitive: "array",
1628
- items: tupleItems,
1629
- rest,
1630
- min: schema.minItems,
1631
- max: schema.maxItems,
1632
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1633
- });
1634
- }
1635
- /**
1636
- * Converts a `type: 'array'` schema into an `ArraySchemaNode`.
1637
- */
1638
- function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
1639
- const rawItems = schema.items;
1640
- const itemName = rawItems?.enum?.length && name ? (0, _kubb_ast.enumPropName)(null, name, options.enumSuffix) : name;
1641
- const items = rawItems ? [parse({
1642
- schema: rawItems,
1643
- name: itemName
1644
- }, rawOptions)] : [];
1645
- return _kubb_ast.ast.factory.createSchema({
1646
- type: "array",
1647
- primitive: "array",
1648
- items,
1649
- min: schema.minItems,
1650
- max: schema.maxItems,
1651
- unique: schema.uniqueItems ?? void 0,
1652
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1653
- });
1654
- }
1655
- /**
1656
- * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1657
- */
1658
- function convertString({ schema, name, nullable, defaultValue }) {
1659
- return _kubb_ast.ast.factory.createSchema({
1660
- type: "string",
1661
- primitive: "string",
1662
- min: schema.minLength,
1663
- max: schema.maxLength,
1664
- pattern: schema.pattern,
1665
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1666
- });
1667
- }
1668
- /**
1669
- * Converts a `type: 'number'` or `type: 'integer'` schema.
1670
- */
1671
- function convertNumeric({ schema, name, nullable, defaultValue }, type) {
1672
- return _kubb_ast.ast.factory.createSchema({
1673
- type,
1674
- primitive: type,
1675
- min: schema.minimum,
1676
- max: schema.maximum,
1677
- exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1678
- exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
1679
- multipleOf: schema.multipleOf,
1680
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1681
- });
1682
- }
1683
- /**
1684
- * Converts a `type: 'boolean'` schema.
1685
- */
1686
- function convertBoolean({ schema, name, nullable, defaultValue }) {
1687
- return _kubb_ast.ast.factory.createSchema({
1688
- type: "boolean",
1689
- primitive: "boolean",
1690
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1691
- });
1712
+ const discPropName = schema.discriminator.propertyName;
1713
+ const values = Object.keys(schema.discriminator.mapping);
1714
+ const enumName = name ? (0, _kubb_kit.enumPropName)(name, discPropName, options.enumSuffix) : void 0;
1715
+ return _kubb_ast.ast.applyMacros(objectNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
1716
+ propertyName: discPropName,
1717
+ values,
1718
+ enumName
1719
+ })], { depth: "shallow" });
1720
+ }
1721
+ return objectNode;
1692
1722
  }
1693
1723
  /**
1694
- * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
1695
- * into a `blob` node.
1724
+ * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
1696
1725
  */
1697
- function convertBinary({ schema, name, nullable, defaultValue }) {
1698
- return _kubb_ast.ast.factory.createSchema({
1699
- type: "blob",
1700
- primitive: "string",
1701
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1726
+ function convertTuple({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
1727
+ const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
1728
+ const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? _kubb_ast.ast.factory.createSchema({ type: options.unknownType }) : parse({ schema: schema.items }, rawOptions);
1729
+ return createNode({
1730
+ schema,
1731
+ name,
1732
+ nullable,
1733
+ defaultValue
1734
+ }, {
1735
+ type: "tuple",
1736
+ primitive: "array",
1737
+ items: tupleItems,
1738
+ rest,
1739
+ min: schema.minItems,
1740
+ max: schema.maxItems
1702
1741
  });
1703
1742
  }
1704
1743
  /**
1705
- * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
1706
- *
1707
- * Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parse`
1708
- * falls through and handles it as that single type with nullability already folded in.
1744
+ * Converts a `type: 'array'` schema into an `ArraySchemaNode`.
1709
1745
  */
1710
- function convertMultiType({ schema, name, nullable, defaultValue, rawOptions, parse }) {
1711
- const types = schema.type;
1712
- const nonNullTypes = types.filter((t) => t !== "null");
1713
- if (nonNullTypes.length <= 1) return null;
1714
- const arrayNullable = types.includes("null") || nullable || void 0;
1715
- return _kubb_ast.ast.factory.createSchema({
1716
- type: "union",
1717
- members: nonNullTypes.map((t) => {
1718
- return parse({
1719
- schema: {
1720
- ...schema,
1721
- type: t
1722
- },
1723
- name
1724
- }, rawOptions);
1725
- }),
1726
- ...buildSchemaNode(schema, name, arrayNullable, defaultValue)
1746
+ function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
1747
+ const rawItems = schema.items;
1748
+ const itemName = rawItems?.enum?.length && name ? (0, _kubb_kit.enumPropName)(null, name, options.enumSuffix) : name;
1749
+ const items = rawItems ? [parse({
1750
+ schema: rawItems,
1751
+ name: itemName
1752
+ }, rawOptions)] : [];
1753
+ return createNode({
1754
+ schema,
1755
+ name,
1756
+ nullable,
1757
+ defaultValue
1758
+ }, {
1759
+ type: "array",
1760
+ primitive: "array",
1761
+ items,
1762
+ min: schema.minItems,
1763
+ max: schema.maxItems,
1764
+ unique: schema.uniqueItems ?? void 0
1727
1765
  });
1728
1766
  }
1767
+ //#endregion
1768
+ //#region src/emit/parseSchema.ts
1729
1769
  /**
1730
1770
  * Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
1731
1771
  * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
@@ -1750,7 +1790,11 @@ const schemaRules = [
1750
1790
  convert: convertConst
1751
1791
  },
1752
1792
  {
1753
- match: ({ schema }) => !!schema.format,
1793
+ match: ({ schema, options }) => {
1794
+ if (!schema.format) return false;
1795
+ if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") return options.dateType !== false;
1796
+ return isHandledFormat(schema.format);
1797
+ },
1754
1798
  convert: convertFormat
1755
1799
  },
1756
1800
  {
@@ -1758,7 +1802,7 @@ const schemaRules = [
1758
1802
  convert: convertBinary
1759
1803
  },
1760
1804
  {
1761
- match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
1805
+ match: ({ schema }) => Array.isArray(schema.type) && schema.type.filter((t) => t !== "null").length > 1,
1762
1806
  convert: convertMultiType
1763
1807
  },
1764
1808
  {
@@ -1807,72 +1851,306 @@ const schemaRules = [
1807
1851
  }
1808
1852
  ];
1809
1853
  //#endregion
1810
- //#region src/parser.ts
1854
+ //#region src/refs.ts
1855
+ const _refCache = /* @__PURE__ */ new WeakMap();
1811
1856
  /**
1812
- * Creates the schema and operation converters bound to one OpenAPI document.
1857
+ * Walks a local `#/...` JSON pointer against `document`, memoized per document. `applicable` is
1858
+ * `false` for an empty or non-local ref (the caller should not treat that as a failed lookup).
1859
+ * Shared by `resolveRef`'s reporting walk and `createRefs().resolve`'s silent walk, so both use
1860
+ * the same trimming and caching instead of two separate implementations.
1861
+ */
1862
+ function walkPointer(document, $ref) {
1863
+ const trimmed = $ref.trim();
1864
+ if (trimmed === "" || !trimmed.startsWith("#")) return {
1865
+ applicable: false,
1866
+ value: null
1867
+ };
1868
+ const pointer = globalThis.decodeURIComponent(trimmed.substring(1));
1869
+ let docCache = _refCache.get(document);
1870
+ if (!docCache) {
1871
+ docCache = /* @__PURE__ */ new Map();
1872
+ _refCache.set(document, docCache);
1873
+ }
1874
+ if (docCache.has(pointer)) return {
1875
+ applicable: true,
1876
+ value: docCache.get(pointer)
1877
+ };
1878
+ const current = pointer.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
1879
+ if (current) docCache.set(pointer, current);
1880
+ return {
1881
+ applicable: true,
1882
+ value: current ?? null
1883
+ };
1884
+ }
1885
+ /**
1886
+ * Resolves a local JSON pointer reference from a document.
1813
1887
  *
1814
- * Owns the per-instance `$ref` state (cycle detection, resolved-node cache, existence cache) and
1815
- * the `parseSchema` recursion seam, then dispatches each schema through the ordered `schemaRules`
1816
- * table from `converters.ts`. Every converter is a standalone function that recurses through the
1817
- * `parse` function passed to it, so this file only wires state to the converters.
1888
+ * Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
1889
+ * resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
1890
+ * build there is no sink to collect it, so it throws instead.
1818
1891
  *
1819
- * @internal
1892
+ * @example
1893
+ * ```ts
1894
+ * resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
1895
+ * ```
1820
1896
  */
1821
- function createSchemaParser(ctx) {
1822
- const document = ctx.document;
1823
- /**
1824
- * Tracks `$ref` paths that are currently being resolved to prevent infinite
1825
- * recursion when schemas contain circular references (e.g. `Pet → parent → Pet`).
1826
- */
1897
+ function resolveRef(document, $ref) {
1898
+ const { applicable, value } = walkPointer(document, $ref);
1899
+ if (!applicable) return null;
1900
+ if (value) return value;
1901
+ const diagnostic = {
1902
+ code: _kubb_core.Diagnostics.code.refNotFound,
1903
+ severity: "error",
1904
+ message: `Could not find a definition for ${$ref}.`,
1905
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
1906
+ location: {
1907
+ kind: "schema",
1908
+ pointer: $ref,
1909
+ ref: $ref
1910
+ }
1911
+ };
1912
+ if (!_kubb_core.Diagnostics.report(diagnostic)) throw new _kubb_core.Diagnostics.Error(diagnostic);
1913
+ return null;
1914
+ }
1915
+ /**
1916
+ * Resolves a `$ref` object while preserving the original `$ref` field on the result.
1917
+ *
1918
+ * Useful for parser flows that need both dereferenced fields and pointer
1919
+ * identity (for naming/import purposes). Non-reference values are returned as-is.
1920
+ *
1921
+ * @example
1922
+ * ```ts
1923
+ * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
1924
+ * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
1925
+ * ```
1926
+ */
1927
+ function dereferenceWithRef(document, schema) {
1928
+ if (isReference(schema)) return {
1929
+ ...schema,
1930
+ ...resolveRef(document, schema.$ref),
1931
+ $ref: schema.$ref
1932
+ };
1933
+ return schema;
1934
+ }
1935
+ /**
1936
+ * Creates the `$ref` resolution service for one document.
1937
+ *
1938
+ * Replaces what used to be six overlapping resolvers (a reporting walk, a silent walk, an
1939
+ * existence check, and a resolve-then-parse-into-a-node step, each with its own cache) with one
1940
+ * pointer walk and one explicit `report` contract for a missing ref: `report: true` (the default)
1941
+ * reports a `refNotFound` diagnostic (or throws outside a build), `report: false` resolves to
1942
+ * `null` silently for a speculative lookup.
1943
+ *
1944
+ * @example
1945
+ * ```ts
1946
+ * const refs = createRefs(document)
1947
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet')
1948
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
1949
+ * refs.exists('#/components/schemas/Pet')
1950
+ * refs.resolveNode('#/components/schemas/Pet', parseSchema)
1951
+ * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
1952
+ * ```
1953
+ */
1954
+ function createRefs(document) {
1955
+ const resolvedNodeCache = /* @__PURE__ */ new Map();
1956
+ const existenceCache = /* @__PURE__ */ new Map();
1827
1957
  const resolvingRefs = /* @__PURE__ */ new Set();
1828
1958
  /**
1829
- * Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
1830
- *
1831
- * Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
1832
- * it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
1833
- * since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
1834
- * Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
1959
+ * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
1960
+ * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
1961
+ * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
1962
+ * for a speculative lookup where a missing ref is not an error.
1835
1963
  */
1836
- const resolvedRefCache = /* @__PURE__ */ new Map();
1964
+ function resolve(refPath, options) {
1965
+ if (options?.report === false) {
1966
+ const { applicable, value } = walkPointer(document, refPath);
1967
+ return applicable ? value : null;
1968
+ }
1969
+ return resolveRef(document, refPath);
1970
+ }
1837
1971
  /**
1838
- * Memoized record of whether a `$ref` path resolves to a node the document actually defines.
1972
+ * Returns `true` when a `$ref` path resolves to a component the document actually defines.
1839
1973
  * A circular ref still resolves to an existing target, so this stays `true` for cycles and only
1840
- * goes `false` for a `$ref` that points at a component the spec never declares.
1974
+ * goes `false` for a `$ref` that points at a component the spec never declares. Memoized.
1841
1975
  */
1842
- const refExistence = /* @__PURE__ */ new Map();
1843
- function refExists(refPath) {
1844
- if (!refExistence.has(refPath)) {
1845
- let exists = false;
1846
- try {
1847
- exists = !!resolveRef(document, refPath);
1848
- } catch {
1849
- exists = false;
1850
- }
1851
- refExistence.set(refPath, exists);
1852
- }
1853
- return refExistence.get(refPath) ?? false;
1976
+ function exists(refPath) {
1977
+ if (!existenceCache.has(refPath)) existenceCache.set(refPath, !!resolve(refPath, { report: false }));
1978
+ return existenceCache.get(refPath) ?? false;
1854
1979
  }
1855
1980
  /**
1856
- * Resolves a `$ref` to its parsed node, guarding against cycles and memoizing per instance.
1857
- * Returns `null` when the ref is currently being resolved (a cycle) or cannot be resolved
1858
- * (e.g. a minimal document in a unit test).
1981
+ * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
1982
+ * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
1983
+ * resolved (e.g. a minimal document in a unit test).
1859
1984
  */
1860
- function resolveRefNode(refPath, rawOptions) {
1985
+ function resolveNode(refPath, parse, rawOptions) {
1861
1986
  if (resolvingRefs.has(refPath)) return null;
1862
- if (!resolvedRefCache.has(refPath)) {
1987
+ if (!resolvedNodeCache.has(refPath)) {
1863
1988
  let resolved = null;
1864
1989
  try {
1865
- const referenced = resolveRef(document, refPath);
1990
+ const referenced = resolve(refPath);
1866
1991
  if (referenced) {
1867
1992
  resolvingRefs.add(refPath);
1868
- resolved = parseSchema({ schema: referenced }, rawOptions);
1993
+ resolved = parse({ schema: referenced }, rawOptions);
1869
1994
  resolvingRefs.delete(refPath);
1870
1995
  }
1871
1996
  } catch {}
1872
- resolvedRefCache.set(refPath, resolved);
1997
+ resolvedNodeCache.set(refPath, resolved);
1873
1998
  }
1874
- return resolvedRefCache.get(refPath) ?? null;
1999
+ return resolvedNodeCache.get(refPath) ?? null;
2000
+ }
2001
+ /**
2002
+ * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
2003
+ * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
2004
+ * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
2005
+ * returned as-is.
2006
+ *
2007
+ * @example
2008
+ * ```ts
2009
+ * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
2010
+ * ```
2011
+ */
2012
+ function deref(value) {
2013
+ if (!isReference(value)) return value ? value : null;
2014
+ const resolved = resolve(value.$ref);
2015
+ return resolved && !isReference(resolved) ? resolved : null;
1875
2016
  }
2017
+ return {
2018
+ resolve,
2019
+ exists,
2020
+ resolveNode,
2021
+ deref
2022
+ };
2023
+ }
2024
+ //#endregion
2025
+ //#region src/model/operations.ts
2026
+ /**
2027
+ * Returns all parameters for an operation, merging path-level and operation-level entries.
2028
+ * Operation-level parameters override path-level ones with the same `in:name` key.
2029
+ * Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
2030
+ *
2031
+ * @example
2032
+ * ```ts
2033
+ * getParameters({ document, operation })
2034
+ * // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
2035
+ * ```
2036
+ */
2037
+ function getParameters({ document, operation }) {
2038
+ const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
2039
+ const operationParams = resolveParams(operation.schema?.parameters || []);
2040
+ const pathLevelParams = resolveParams(operation.pathItem.parameters ?? []);
2041
+ const paramMap = /* @__PURE__ */ new Map();
2042
+ for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
2043
+ for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
2044
+ return Array.from(paramMap.values());
2045
+ }
2046
+ function getResponseBody(responseBody, contentType) {
2047
+ if (!responseBody) return false;
2048
+ if (isReference(responseBody)) return false;
2049
+ const body = responseBody;
2050
+ if (!body.content) return false;
2051
+ if (contentType) return contentType in body.content ? body.content[contentType] : false;
2052
+ const picked = pickContentEntry(body.content);
2053
+ return picked ? picked[1] : false;
2054
+ }
2055
+ /**
2056
+ * Returns the response schema for a given operation and HTTP status code.
2057
+ *
2058
+ * Returns an empty object `{}` when no response body schema is available.
2059
+ *
2060
+ * @example
2061
+ * ```ts
2062
+ * getResponseSchema({ document, operation, refs, statusCode: 200 }) // SchemaObject
2063
+ * getResponseSchema({ document, operation, refs, statusCode: '4XX' }) // {}
2064
+ * ```
2065
+ */
2066
+ function getResponseSchema({ document, operation, refs, statusCode, options = {} }) {
2067
+ const responseBody = getResponseBody(getResponseByStatusCode({
2068
+ operation,
2069
+ refs,
2070
+ statusCode
2071
+ }), options.contentType);
2072
+ if (responseBody === false) return {};
2073
+ const schema = responseBody.schema;
2074
+ if (!schema) return {};
2075
+ return dereferenceWithRef(document, schema);
2076
+ }
2077
+ /**
2078
+ * Returns the request body schema for an operation, or `null` when absent.
2079
+ *
2080
+ * @example
2081
+ * ```ts
2082
+ * getRequestSchema({ document, operation, refs }) // SchemaObject | null
2083
+ * ```
2084
+ */
2085
+ function getRequestSchema({ document, operation, refs, options = {} }) {
2086
+ const requestBody = getRequestContent({
2087
+ operation,
2088
+ refs,
2089
+ mediaType: options.contentType
2090
+ });
2091
+ if (requestBody === false) return null;
2092
+ const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
2093
+ const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
2094
+ if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
2095
+ type: "string",
2096
+ contentMediaType: "application/octet-stream"
2097
+ };
2098
+ if (!schema) return null;
2099
+ return dereferenceWithRef(document, schema);
2100
+ }
2101
+ /**
2102
+ * Returns all request body content type keys for an operation, resolving a `$ref` requestBody
2103
+ * through `refs`.
2104
+ *
2105
+ * @example
2106
+ * ```ts
2107
+ * getRequestBodyContentTypes(operation, refs)
2108
+ * // ['application/json', 'multipart/form-data']
2109
+ * ```
2110
+ */
2111
+ function getRequestBodyContentTypes(operation, refs) {
2112
+ const body = getRequestBody({
2113
+ operation,
2114
+ refs
2115
+ });
2116
+ return body?.content ? Object.keys(body.content) : [];
2117
+ }
2118
+ /**
2119
+ * Returns all response content type keys for an operation at a given status code, resolving the
2120
+ * response `$ref` through `refs`.
2121
+ *
2122
+ * @example
2123
+ * ```ts
2124
+ * getResponseBodyContentTypes(operation, refs, 200)
2125
+ * // ['application/json', 'application/xml']
2126
+ * ```
2127
+ */
2128
+ function getResponseBodyContentTypes(operation, refs, statusCode) {
2129
+ const responseObj = getResponseByStatusCode({
2130
+ operation,
2131
+ refs,
2132
+ statusCode
2133
+ });
2134
+ if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
2135
+ const body = responseObj;
2136
+ return body.content ? Object.keys(body.content) : [];
2137
+ }
2138
+ //#endregion
2139
+ //#region src/parser.ts
2140
+ /**
2141
+ * Creates the schema and operation converters bound to one OpenAPI document.
2142
+ *
2143
+ * Takes the `$ref` service for this document (shared with the rest of the pipeline, see
2144
+ * `adapter.ts`) and owns the `parseSchema` recursion seam, then dispatches each schema through
2145
+ * the ordered `schemaRules` table from `emit/parseSchema.ts`. Every converter is a standalone
2146
+ * function that recurses through the `parse` function passed to it, so this file only wires
2147
+ * state to the converters.
2148
+ *
2149
+ * @internal
2150
+ */
2151
+ function createSchemaParser(ctx) {
2152
+ const document = ctx.document;
2153
+ const refs = ctx.refs;
1876
2154
  /**
1877
2155
  * Converts an OAS `SchemaObject` into a `SchemaNode`.
1878
2156
  *
@@ -1901,15 +2179,10 @@ function createSchemaParser(ctx) {
1901
2179
  options,
1902
2180
  parse: parseSchema,
1903
2181
  document,
1904
- resolveRefNode,
1905
- refExists,
2182
+ refs,
1906
2183
  renames: ctx.renames
1907
2184
  };
1908
- for (const rule of schemaRules) {
1909
- if (!rule.match(context)) continue;
1910
- const node = rule.convert(context);
1911
- if (node) return node;
1912
- }
2185
+ for (const rule of schemaRules) if (rule.match(context)) return rule.convert(context);
1913
2186
  const emptyType = options.emptySchemaType;
1914
2187
  return _kubb_ast.ast.factory.createSchema({
1915
2188
  type: emptyType,
@@ -1946,10 +2219,14 @@ function createSchemaParser(ctx) {
1946
2219
  }
1947
2220
  /**
1948
2221
  * Reads the inline `requestBody` metadata (description / required) that OAS exposes
1949
- * outside the schema itself. Returns an empty object when the request body is missing or a `$ref`.
2222
+ * outside the schema itself, resolving a `$ref` requestBody through `refs`. Returns an
2223
+ * empty object when the request body is missing or cannot be resolved.
1950
2224
  */
1951
2225
  function getRequestBodyMeta(operation) {
1952
- const body = operation.schema.requestBody;
2226
+ const body = getRequestBody({
2227
+ operation,
2228
+ refs
2229
+ });
1953
2230
  if (!body) return { required: false };
1954
2231
  return {
1955
2232
  description: body.description,
@@ -1975,12 +2252,20 @@ function createSchemaParser(ctx) {
1975
2252
  function parseOperation(options, operation) {
1976
2253
  const operationId = getOperationId(operation);
1977
2254
  const operationName = operationId ? pascalCase(operationId) : void 0;
1978
- const parameters = getParameters(document, operation).map((param) => parseParameter(options, param, operationName));
1979
- const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
2255
+ const parameters = getParameters({
2256
+ document,
2257
+ operation
2258
+ }).map((param) => parseParameter(options, param, operationName));
2259
+ const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(operation, refs);
1980
2260
  const requestBodyMeta = getRequestBodyMeta(operation);
1981
2261
  const requestBodyName = operationName ? `${operationName}Request` : void 0;
1982
2262
  const content = allContentTypes.flatMap((ct) => {
1983
- const schema = getRequestSchema(document, operation, { contentType: ct });
2263
+ const schema = getRequestSchema({
2264
+ document,
2265
+ operation,
2266
+ refs,
2267
+ options: { contentType: ct }
2268
+ });
1984
2269
  if (!schema) return [];
1985
2270
  return [_kubb_ast.ast.factory.createContent({
1986
2271
  contentType: ct,
@@ -1998,14 +2283,20 @@ function createSchemaParser(ctx) {
1998
2283
  } : void 0;
1999
2284
  const responses = getResponseStatusCodes(operation).map((statusCode) => {
2000
2285
  const responseObj = getResponseByStatusCode({
2001
- document,
2002
2286
  operation,
2287
+ refs,
2003
2288
  statusCode
2004
2289
  });
2005
2290
  const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
2006
2291
  const description = typeof responseObj === "object" && responseObj !== null ? responseObj.description : void 0;
2007
2292
  const parseEntrySchema = (contentType) => {
2008
- const raw = getResponseSchema(document, operation, statusCode, { contentType });
2293
+ const raw = getResponseSchema({
2294
+ document,
2295
+ operation,
2296
+ refs,
2297
+ statusCode,
2298
+ options: { contentType }
2299
+ });
2009
2300
  return {
2010
2301
  schema: raw && Object.keys(raw).length > 0 ? parseSchema({
2011
2302
  schema: raw,
@@ -2014,14 +2305,14 @@ function createSchemaParser(ctx) {
2014
2305
  keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
2015
2306
  };
2016
2307
  };
2017
- const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)).map((contentType) => _kubb_ast.ast.factory.createContent({
2308
+ const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(operation, refs, statusCode)).map((contentType) => _kubb_ast.ast.factory.createContent({
2018
2309
  contentType,
2019
2310
  ...parseEntrySchema(contentType)
2020
2311
  }));
2021
2312
  if (content.length === 0) content.push(_kubb_ast.ast.factory.createContent({
2022
2313
  contentType: getRequestContentType({
2023
- document,
2024
- operation
2314
+ operation,
2315
+ refs
2025
2316
  }) || "application/json",
2026
2317
  ...parseEntrySchema(ctx.contentType)
2027
2318
  }));
@@ -2031,12 +2322,10 @@ function createSchemaParser(ctx) {
2031
2322
  content
2032
2323
  });
2033
2324
  });
2034
- const pathItem = document.paths?.[operation.path];
2035
- const pathItemDoc = pathItem && !isReference(pathItem) ? pathItem : void 0;
2036
2325
  const pickDoc = (key) => {
2037
2326
  const own = operation.schema[key];
2038
2327
  if (typeof own === "string") return own;
2039
- const fallback = pathItemDoc?.[key];
2328
+ const fallback = operation.pathItem[key];
2040
2329
  return typeof fallback === "string" ? fallback : void 0;
2041
2330
  };
2042
2331
  return _kubb_ast.ast.factory.createOperation({
@@ -2109,15 +2398,20 @@ function refPromotedEnums(node, promoted) {
2109
2398
  //#endregion
2110
2399
  //#region src/schemaDiagnostics.ts
2111
2400
  /**
2112
- * Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
2113
- * top-level schema. Walks the node the parser produced, threading the RFC 6901
2114
- * pointer as it descends so a nested field reports against its full path
2115
- * (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
2116
- * resolved schema is reported under its own walk. Reports land in the active build run, are a
2117
- * no-op outside one, and repeats are deduped by the build.
2401
+ * Scans one freshly converted top-level schema in a single walk, so the post-convert pass never
2402
+ * sweeps the same nodes twice. It reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`,
2403
+ * `KUBB_DEPRECATED`) and returns the names of every schema the node references, ready to feed the
2404
+ * circular-dependency graph.
2405
+ *
2406
+ * Walks the node the parser produced, threading the RFC 6901 pointer as it descends so a nested
2407
+ * field reports against its full path (`#/components/schemas/Pet/properties/owner/properties/name`).
2408
+ * Refs are recorded by name and not followed, so the resolved schema is reported under its own walk.
2409
+ * Reports land in the active build run, are a no-op outside one, and repeats are deduped by the build.
2118
2410
  */
2119
- function reportSchemaDiagnostics({ node, name }) {
2120
- visit(node, `#/components/schemas/${escapePointerToken(name)}`);
2411
+ function scanSchema({ node, name }) {
2412
+ const refs = /* @__PURE__ */ new Set();
2413
+ visit(node, `#/components/schemas/${escapePointerToken(name)}`, refs);
2414
+ return refs;
2121
2415
  }
2122
2416
  /**
2123
2417
  * Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
@@ -2126,7 +2420,11 @@ function reportSchemaDiagnostics({ node, name }) {
2126
2420
  function escapePointerToken(token) {
2127
2421
  return token.replace(/~/g, "~0").replace(/\//g, "~1");
2128
2422
  }
2129
- function visit(node, pointer) {
2423
+ function visit(node, pointer, refs) {
2424
+ if (node.type === "ref") {
2425
+ const refName = (0, _kubb_ast.resolveRefName)(node);
2426
+ if (refName) refs.add(refName);
2427
+ }
2130
2428
  if (node.deprecated) _kubb_core.Diagnostics.report({
2131
2429
  code: _kubb_core.Diagnostics.code.deprecated,
2132
2430
  severity: "info",
@@ -2147,19 +2445,19 @@ function visit(node, pointer) {
2147
2445
  }
2148
2446
  });
2149
2447
  if (node.type === "object") {
2150
- for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
2151
- if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
2448
+ for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`, refs);
2449
+ if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`, refs);
2152
2450
  return;
2153
2451
  }
2154
2452
  if (node.type === "array") {
2155
- for (const item of node.items ?? []) visit(item, `${pointer}/items`);
2453
+ for (const item of node.items ?? []) visit(item, `${pointer}/items`, refs);
2156
2454
  return;
2157
2455
  }
2158
2456
  if (node.type === "tuple") {
2159
- for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
2457
+ for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`, refs);
2160
2458
  return;
2161
2459
  }
2162
- if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
2460
+ if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`, refs);
2163
2461
  }
2164
2462
  //#endregion
2165
2463
  //#region src/adapter.ts
@@ -2205,63 +2503,33 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2205
2503
  enumSuffix
2206
2504
  };
2207
2505
  let parsedDocument = null;
2208
- const documentCache = /* @__PURE__ */ new WeakMap();
2209
- const schemasCache = /* @__PURE__ */ new WeakMap();
2210
- const schemaParserCache = /* @__PURE__ */ new WeakMap();
2211
- function ensureDocument(source) {
2212
- const cached = documentCache.get(source);
2213
- if (cached) return cached;
2214
- const promise = (async () => {
2215
- const fresh = await parseFromConfig(source);
2216
- if (validate) await validateDocument(fresh);
2217
- parsedDocument = fresh;
2218
- return fresh;
2219
- })();
2220
- documentCache.set(source, promise);
2221
- return promise;
2222
- }
2223
- function ensureSchemas(document) {
2224
- const cached = schemasCache.get(document);
2225
- if (cached) return cached;
2226
- const result = getSchemas(document, { contentType });
2227
- schemasCache.set(document, result);
2228
- return result;
2229
- }
2230
- function ensureSchemaParser({ document, renames }) {
2231
- const cached = schemaParserCache.get(document);
2232
- if (cached) return cached;
2233
- const parser = createSchemaParser({
2234
- document,
2235
- contentType,
2236
- renames
2237
- });
2238
- schemaParserCache.set(document, parser);
2239
- return parser;
2240
- }
2241
- function parseInput({ document, schemas, parser }) {
2506
+ const inputCache = /* @__PURE__ */ new WeakMap();
2507
+ function parseInput({ document, refs, schemas, parser }) {
2242
2508
  const { parseSchema, parseOperation } = parser;
2243
2509
  const parsedByName = /* @__PURE__ */ new Map();
2244
2510
  const refAliasMap = /* @__PURE__ */ new Map();
2245
2511
  const enumNames = [];
2246
2512
  const discriminatorParentNodes = [];
2513
+ const refGraph = /* @__PURE__ */ new Map();
2247
2514
  for (const [name, schema] of Object.entries(schemas)) {
2248
2515
  const node = parseSchema({
2249
2516
  schema,
2250
2517
  name
2251
2518
  }, parserOptions);
2252
2519
  parsedByName.set(name, node);
2253
- reportSchemaDiagnostics({
2520
+ const refs = scanSchema({
2254
2521
  node,
2255
2522
  name
2256
2523
  });
2524
+ if (node.name) refGraph.set(node.name, refs);
2257
2525
  if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2258
2526
  if ((0, _kubb_ast.narrowSchema)(node, "enum") && node.name) enumNames.push(node.name);
2259
2527
  if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2260
2528
  }
2261
- const circularNames = [...(0, _kubb_ast.findCircularSchemas)([...parsedByName.values()])];
2529
+ const circularNames = [...(0, _kubb_ast.findCircularSchemasFromGraph)(refGraph)];
2262
2530
  const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
2263
2531
  const operationNodes = [];
2264
- for (const operation of getOperations(document)) {
2532
+ for (const operation of getOperations(document, refs)) {
2265
2533
  const operationNode = parseOperation(parserOptions, operation);
2266
2534
  if (operationNode) operationNodes.push(operationNode);
2267
2535
  }
@@ -2323,19 +2591,34 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2323
2591
  },
2324
2592
  async validate(input, options) {
2325
2593
  await assertInputExists(input);
2326
- await validateDocument(await parseDocument(input), options);
2594
+ const document = await parseDocument(input);
2595
+ assertDocument(document);
2596
+ await validateDocument(document, options);
2327
2597
  },
2328
2598
  async parse(source) {
2329
- const document = await ensureDocument(source);
2330
- const { schemas, renames } = ensureSchemas(document);
2331
- return parseInput({
2332
- document,
2333
- schemas,
2334
- parser: ensureSchemaParser({
2599
+ const cached = inputCache.get(source);
2600
+ if (cached) return cached;
2601
+ const promise = (async () => {
2602
+ const document = await parseFromConfig(source);
2603
+ assertDocument(document);
2604
+ if (validate) await validateDocument(document);
2605
+ parsedDocument = document;
2606
+ const refs = createRefs(document);
2607
+ const { schemas, renames } = getSchemas(document, { contentType }, refs);
2608
+ return parseInput({
2335
2609
  document,
2336
- renames
2337
- })
2338
- });
2610
+ refs,
2611
+ schemas,
2612
+ parser: createSchemaParser({
2613
+ document,
2614
+ refs,
2615
+ contentType,
2616
+ renames
2617
+ })
2618
+ });
2619
+ })();
2620
+ inputCache.set(source, promise);
2621
+ return promise;
2339
2622
  }
2340
2623
  };
2341
2624
  });