@kubb/adapter-oas 5.0.0-beta.101 → 5.0.0-beta.102

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
@@ -23,13 +23,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
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 yaml = require("yaml");
29
30
  let _readme_openapi_parser = require("@readme/openapi-parser");
30
31
  let _scalar_openapi_upgrader = require("@scalar/openapi-upgrader");
31
32
  let api_ref_bundler = require("api-ref-bundler");
32
- let yaml = require("yaml");
33
33
  let _kubb_kit = require("@kubb/kit");
34
34
  //#region src/constants.ts
35
35
  /**
@@ -128,7 +128,7 @@ const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
128
128
  */
129
129
  const enumDescriptionKeys = ["x-enumDescriptions", "x-enum-descriptions"];
130
130
  //#endregion
131
- //#region src/discriminator.ts
131
+ //#region src/emit/discriminator/propagate.ts
132
132
  /**
133
133
  * Maps each child schema name to its discriminator patch data by scanning the given
134
134
  * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
@@ -204,42 +204,6 @@ function patchDiscriminatorNode(node, entry) {
204
204
  properties: newProperties
205
205
  };
206
206
  }
207
- /**
208
- * Creates a single-property object schema used as a discriminator literal.
209
- *
210
- * @example
211
- * ```ts
212
- * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
213
- * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
214
- * ```
215
- */
216
- function createDiscriminantNode({ propertyName, value }) {
217
- return _kubb_ast.ast.factory.createSchema({
218
- type: "object",
219
- primitive: "object",
220
- properties: [_kubb_ast.ast.factory.createProperty({
221
- name: propertyName,
222
- schema: _kubb_ast.ast.factory.createSchema({
223
- type: "enum",
224
- primitive: "string",
225
- enumValues: [value]
226
- }),
227
- required: true
228
- })]
229
- });
230
- }
231
- /**
232
- * Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
233
- *
234
- * @example
235
- * ```ts
236
- * findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
237
- * ```
238
- */
239
- function findDiscriminator(mapping, ref) {
240
- if (!mapping || !ref) return null;
241
- return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
242
- }
243
207
  //#endregion
244
208
  //#region ../../internals/utils/src/casing.ts
245
209
  /**
@@ -368,7 +332,7 @@ async function read(path) {
368
332
  return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
369
333
  }
370
334
  //#endregion
371
- //#region src/factory.ts
335
+ //#region src/load/source.ts
372
336
  const urlRegExp = /^https?:\/+/i;
373
337
  async function readSource(sourcePath) {
374
338
  if (urlRegExp.test(sourcePath)) {
@@ -379,12 +343,33 @@ async function readSource(sourcePath) {
379
343
  }
380
344
  return read(sourcePath);
381
345
  }
346
+ /**
347
+ * Reads and parses one source file or URL referenced during bundling: YAML/JSON is parsed into an
348
+ * object, Markdown is returned as-is (bundled inline rather than dereferenced).
349
+ */
382
350
  async function resolveSource(sourcePath) {
383
351
  const data = await readSource(sourcePath);
384
352
  if (sourcePath.toLowerCase().endsWith(".md")) return data;
385
353
  return (0, yaml.parse)(data);
386
354
  }
387
355
  /**
356
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
357
+ * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
358
+ * its parse error instead.
359
+ */
360
+ async function assertInputExists(input) {
361
+ if (URL.canParse(input)) return;
362
+ if (!await exists(input)) throw new _kubb_core.Diagnostics.Error({
363
+ code: _kubb_core.Diagnostics.code.inputNotFound,
364
+ severity: "error",
365
+ message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
366
+ help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
367
+ location: { kind: "config" }
368
+ });
369
+ }
370
+ //#endregion
371
+ //#region src/load/normalize.ts
372
+ /**
388
373
  * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
389
374
  *
390
375
  * External file schemas are hoisted into named `components.schemas` entries, so a property
@@ -449,21 +434,6 @@ async function parseFromConfig(source) {
449
434
  return parseDocument(resolved);
450
435
  }
451
436
  /**
452
- * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
453
- * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
454
- * its parse error instead.
455
- */
456
- async function assertInputExists(input) {
457
- if (URL.canParse(input)) return;
458
- if (!await exists(input)) throw new _kubb_core.Diagnostics.Error({
459
- code: _kubb_core.Diagnostics.code.inputNotFound,
460
- severity: "error",
461
- message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
462
- help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
463
- location: { kind: "config" }
464
- });
465
- }
466
- /**
467
437
  * Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
468
438
  *
469
439
  * @example
@@ -540,135 +510,272 @@ const jsonMimeFragments = [
540
510
  function isJsonMimeType(mimeType) {
541
511
  return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
542
512
  }
543
- //#endregion
544
- //#region src/refs.ts
545
- const _refCache = /* @__PURE__ */ new WeakMap();
546
513
  /**
547
- * Resolves a local JSON pointer reference from a document.
548
- *
549
- * Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
550
- * resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
551
- * build there is no sink to collect it, so it throws instead.
514
+ * Picks a media-type entry from a `content` map: the first JSON-like media type, falling back to
515
+ * the first declared one. Returns `false` when `content` has no entries.
552
516
  *
553
517
  * @example
554
518
  * ```ts
555
- * resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
519
+ * pickContentEntry({ 'application/xml': xmlEntry, 'application/json': jsonEntry })
520
+ * // ['application/json', jsonEntry]
556
521
  * ```
557
522
  */
558
- function resolveRef(document, $ref) {
559
- const origRef = $ref;
560
- $ref = $ref.trim();
561
- if ($ref === "") return null;
562
- if (!$ref.startsWith("#")) return null;
563
- $ref = globalThis.decodeURIComponent($ref.substring(1));
564
- let docCache = _refCache.get(document);
565
- if (!docCache) {
566
- docCache = /* @__PURE__ */ new Map();
567
- _refCache.set(document, docCache);
568
- }
569
- if (docCache.has($ref)) return docCache.get($ref);
570
- const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
571
- if (!current) {
572
- const diagnostic = {
573
- code: _kubb_core.Diagnostics.code.refNotFound,
574
- severity: "error",
575
- message: `Could not find a definition for ${origRef}.`,
576
- help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
577
- location: {
578
- kind: "schema",
579
- pointer: origRef,
580
- ref: origRef
581
- }
582
- };
583
- if (!_kubb_core.Diagnostics.report(diagnostic)) throw new _kubb_core.Diagnostics.Error(diagnostic);
584
- return null;
585
- }
586
- docCache.set($ref, current);
587
- return current;
523
+ function pickContentEntry(content) {
524
+ const mediaTypes = Object.keys(content);
525
+ const available = mediaTypes.find(isJsonMimeType) ?? mediaTypes[0];
526
+ return available ? [available, content[available]] : false;
588
527
  }
528
+ //#endregion
529
+ //#region src/model/components.ts
589
530
  /**
590
- * Resolves a `$ref` object while preserving the original `$ref` field on the result.
531
+ * Extracts the inline schema from a media-type `content` map.
591
532
  *
592
- * Useful for parser flows that need both dereferenced fields and pointer
593
- * identity (for naming/import purposes). Non-reference values are returned as-is.
533
+ * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
534
+ * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
594
535
  *
595
536
  * @example
596
537
  * ```ts
597
- * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
598
- * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
538
+ * extractSchemaFromContent(operation.content, 'application/json')
539
+ * // SchemaObject | null
599
540
  * ```
600
541
  */
601
- function dereferenceWithRef(document, schema) {
602
- if (isReference(schema)) return {
603
- ...schema,
604
- ...resolveRef(document, schema.$ref),
605
- $ref: schema.$ref
606
- };
607
- return schema;
542
+ function extractSchemaFromContent(content, preferredContentType) {
543
+ if (!content) return null;
544
+ const firstContentType = Object.keys(content)[0] ?? "application/json";
545
+ const schema = content[preferredContentType ?? firstContentType]?.schema;
546
+ if (isReference(schema)) return null;
547
+ return schema ?? null;
548
+ }
549
+ /**
550
+ * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
551
+ */
552
+ function* collectRefs(schema) {
553
+ if (Array.isArray(schema)) {
554
+ for (const item of schema) yield* collectRefs(item);
555
+ return;
556
+ }
557
+ if (schema && typeof schema === "object") for (const key in schema) {
558
+ const value = schema[key];
559
+ if (!(key === "$ref" && typeof value === "string")) {
560
+ yield* collectRefs(value);
561
+ continue;
562
+ }
563
+ if (value.startsWith("#/components/schemas/")) {
564
+ const name = value.slice(21);
565
+ if (name) yield name;
566
+ }
567
+ }
608
568
  }
609
569
  /**
610
- * Resolves a `$ref` slot in place: when `container[key]` holds a `$ref`, replaces it with the
611
- * resolved value and returns that value. Returns `null` when the slot is empty, cannot be resolved,
612
- * or is still a `$ref` after resolving. A non-`$ref` value is returned untouched, without writing.
570
+ * Returns a copy of `schemas` topologically sorted by `$ref` dependency.
571
+ *
572
+ * Referenced schemas appear before the schemas that depend on them, so code generators
573
+ * can emit types in the correct order. Cycles are silently skipped.
613
574
  *
614
575
  * @example
615
576
  * ```ts
616
- * derefInPlace<ResponseObject>({ document, container: operation.schema.responses, key: '200' })
577
+ * const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
578
+ * // Pet appears before Order when Order.$ref points at Pet
617
579
  * ```
618
580
  */
619
- function derefInPlace({ document, container, key }) {
620
- const value = container[key];
621
- if (!isReference(value)) return value ? value : null;
622
- const resolved = resolveRef(document, value.$ref);
623
- container[key] = resolved;
624
- return resolved && !isReference(resolved) ? resolved : null;
581
+ function sortSchemas(schemas) {
582
+ const deps = /* @__PURE__ */ new Map();
583
+ for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
584
+ const sorted = [];
585
+ const visited = /* @__PURE__ */ new Set();
586
+ function visit(name, stack) {
587
+ if (visited.has(name) || stack.has(name)) return;
588
+ stack.add(name);
589
+ for (const child of deps.get(name) ?? []) if (deps.has(child)) visit(child, stack);
590
+ stack.delete(name);
591
+ visited.add(name);
592
+ sorted.push(name);
593
+ }
594
+ for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
595
+ const result = {};
596
+ for (const name of sorted) result[name] = schemas[name];
597
+ return result;
625
598
  }
626
- //#endregion
627
- //#region src/operation.ts
599
+ const semanticSuffixes = {
600
+ schemas: "Schema",
601
+ responses: "Response",
602
+ requestBodies: "Request"
603
+ };
628
604
  /**
629
- * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
630
- * with no leading or trailing dash.
605
+ * Picks the collision suffix for one name-colliding schema: none when the name is unique,
606
+ * a semantic suffix (`Schema`, `Response`, `Request`) when the collision spans sources, otherwise
607
+ * a numeric suffix (`2`, `3`, …) for same-source collisions.
631
608
  */
632
- function slugify(value) {
633
- return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
609
+ function collisionSuffix({ isSingle, hasMultipleSources, source, index }) {
610
+ if (isSingle) return "";
611
+ if (hasMultipleSources) return semanticSuffixes[source];
612
+ if (index === 0) return "";
613
+ return String(index + 1);
634
614
  }
635
615
  /**
636
- * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
616
+ * Collects component schemas from one or more sources and resolves name collisions.
617
+ *
618
+ * Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
619
+ * topologically sorted by `$ref` dependency so generators emit types in the correct order.
620
+ *
621
+ * When two or more schemas normalize to the same PascalCase name:
622
+ * - Same source → numeric suffix (`2`, `3`, …).
623
+ * - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * const { schemas, renames } = getSchemas(document, { contentType: 'application/json' }, refs)
628
+ * ```
637
629
  */
638
- function getOperationId({ path, method, schema }) {
639
- const { operationId } = schema;
640
- if (typeof operationId === "string" && operationId.length > 0) return operationId;
641
- return `${method}_${slugify(path).toLowerCase()}`;
630
+ function getSchemas(document, { contentType }, refs) {
631
+ const components = document.components;
632
+ function resolveSchemaRef(schema) {
633
+ if (!isReference(schema)) return schema;
634
+ const resolved = refs.resolve(schema.$ref);
635
+ return resolved && !isReference(resolved) ? resolved : schema;
636
+ }
637
+ const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
638
+ schema: resolveSchemaRef(schema),
639
+ source: "schemas",
640
+ originalName: name
641
+ })), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
642
+ const schema = extractSchemaFromContent(item.content, contentType);
643
+ return schema ? [{
644
+ schema: resolveSchemaRef(schema),
645
+ source,
646
+ originalName: name
647
+ }] : [];
648
+ }))];
649
+ const normalizedNames = /* @__PURE__ */ new Map();
650
+ for (const item of candidates) {
651
+ const key = pascalCase(item.originalName);
652
+ const bucket = normalizedNames.get(key) ?? [];
653
+ bucket.push(item);
654
+ normalizedNames.set(key, bucket);
655
+ }
656
+ const schemas = {};
657
+ const renames = /* @__PURE__ */ new Map();
658
+ for (const [, items] of normalizedNames) {
659
+ const isSingle = items.length === 1;
660
+ const hasMultipleSources = !isSingle && new Set(items.map((item) => item.source)).size > 1;
661
+ items.forEach((item, index) => {
662
+ const suffix = collisionSuffix({
663
+ isSingle,
664
+ hasMultipleSources,
665
+ source: item.source,
666
+ index
667
+ });
668
+ const uniqueName = item.originalName + suffix;
669
+ schemas[uniqueName] = item.schema;
670
+ if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
671
+ });
672
+ }
673
+ return {
674
+ schemas: sortSchemas(schemas),
675
+ renames
676
+ };
642
677
  }
678
+ //#endregion
679
+ //#region src/model/server.ts
643
680
  /**
644
- * Returns the declared response status codes, skipping `x-` extensions and non-object entries.
681
+ * Reads the server URL from the document's `servers` array at `server.index`,
682
+ * interpolating any `server.variables` into the URL template.
683
+ *
684
+ * Returns `null` when `server.index` is omitted or out of range.
685
+ *
686
+ * @example Resolve the first server
687
+ * `resolveBaseUrl({ document, server: { index: 0 } })`
688
+ *
689
+ * @example Override a path variable
690
+ * `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
645
691
  */
646
- function getResponseStatusCodes({ schema }) {
647
- const responses = schema.responses;
648
- if (!responses || isReference(responses)) return [];
649
- return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
692
+ function resolveBaseUrl({ document, server }) {
693
+ const index = server?.index;
694
+ const entry = index !== void 0 ? document.servers?.at(index) : void 0;
695
+ return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
650
696
  }
651
697
  /**
652
- * Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
698
+ * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
699
+ * Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
700
+ * Throws if an override value is not in the variable's `enum` list.
701
+ *
702
+ * @example
703
+ * ```ts
704
+ * resolveServerUrl(
705
+ * { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
706
+ * { env: 'prod' },
707
+ * )
708
+ * // 'https://prod.api.example.com'
709
+ * ```
653
710
  */
654
- function getResponseByStatusCode({ document, operation, statusCode }) {
655
- const responses = operation.schema.responses;
711
+ function resolveServerUrl(server, overrides) {
712
+ if (!server.variables) return server.url;
713
+ let url = server.url;
714
+ for (const [key, variable] of Object.entries(server.variables)) {
715
+ const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
716
+ if (value === void 0) continue;
717
+ if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.Diagnostics.Error({
718
+ code: _kubb_core.Diagnostics.code.invalidServerVariable,
719
+ severity: "error",
720
+ message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
721
+ help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
722
+ location: {
723
+ kind: "document",
724
+ pointer: "#/servers"
725
+ }
726
+ });
727
+ url = url.replaceAll(`{${key}}`, value);
728
+ }
729
+ return url;
730
+ }
731
+ //#endregion
732
+ //#region src/operation.ts
733
+ /**
734
+ * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
735
+ * with no leading or trailing dash.
736
+ */
737
+ function slugify(value) {
738
+ return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
739
+ }
740
+ /**
741
+ * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
742
+ */
743
+ function getOperationId({ path, method, schema }) {
744
+ const { operationId } = schema;
745
+ if (typeof operationId === "string" && operationId.length > 0) return operationId;
746
+ return `${method}_${slugify(path).toLowerCase()}`;
747
+ }
748
+ /**
749
+ * Returns the declared response status codes, skipping `x-` extensions and non-object entries.
750
+ */
751
+ function getResponseStatusCodes({ schema }) {
752
+ const responses = schema.responses;
753
+ if (!responses || isReference(responses)) return [];
754
+ return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
755
+ }
756
+ /**
757
+ * Returns the response object for a status code, resolving a `$ref` through `refs`. `false` when absent.
758
+ */
759
+ function getResponseByStatusCode({ operation, refs, statusCode }) {
760
+ const responses = operation.schema.responses;
656
761
  if (!responses || isReference(responses)) return false;
657
- return derefInPlace({
658
- document,
659
- container: responses,
660
- key: statusCode
661
- }) ?? false;
762
+ return refs.deref(responses[statusCode]) ?? false;
662
763
  }
663
764
  /**
664
- * Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
765
+ * Resolves the operation's request body, dereferencing a `$ref` through `refs`. Returns `null`
766
+ * when the operation has no request body or it cannot be resolved.
767
+ */
768
+ function getRequestBody({ operation, refs }) {
769
+ return refs.deref(operation.schema.requestBody);
770
+ }
771
+ /**
772
+ * Resolves the request body (a `$ref` through `refs`) and returns its content map, or
665
773
  * `undefined` when the operation has no request body.
666
774
  */
667
- function getRequestBodyContent({ document, operation }) {
668
- return derefInPlace({
669
- document,
670
- container: operation.schema,
671
- key: "requestBody"
775
+ function getRequestBodyContent({ operation, refs }) {
776
+ return getRequestBody({
777
+ operation,
778
+ refs
672
779
  })?.content;
673
780
  }
674
781
  /**
@@ -676,25 +783,23 @@ function getRequestBodyContent({ document, operation }) {
676
783
  * Otherwise picks the first JSON-like media type, then the first declared one, as a
677
784
  * `[mediaType, object]` tuple.
678
785
  */
679
- function getRequestContent({ document, operation, mediaType }) {
786
+ function getRequestContent({ operation, refs, mediaType }) {
680
787
  const content = getRequestBodyContent({
681
- document,
682
- operation
788
+ operation,
789
+ refs
683
790
  });
684
791
  if (!content) return false;
685
792
  if (mediaType) return mediaType in content ? content[mediaType] : false;
686
- const mediaTypes = Object.keys(content);
687
- const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
688
- return available ? [available, content[available]] : false;
793
+ return pickContentEntry(content);
689
794
  }
690
795
  /**
691
796
  * Returns the primary request content type. Prefers a JSON-like media type (the last one wins
692
797
  * when several are declared), then the first declared one, defaulting to `'application/json'`.
693
798
  */
694
- function getRequestContentType({ document, operation }) {
799
+ function getRequestContentType({ operation, refs }) {
695
800
  const content = getRequestBodyContent({
696
- document,
697
- operation
801
+ operation,
802
+ refs
698
803
  });
699
804
  const mediaTypes = content ? Object.keys(content) : [];
700
805
  let result = mediaTypes[0] ?? "application/json";
@@ -707,22 +812,18 @@ function getRequestContentType({ document, operation }) {
707
812
  *
708
813
  * @example
709
814
  * ```ts
710
- * for (const operation of getOperations(document)) {
815
+ * for (const operation of getOperations(document, refs)) {
711
816
  * parseOperation(options, operation)
712
817
  * }
713
818
  * ```
714
819
  */
715
- function getOperations(document) {
820
+ function getOperations(document, refs) {
716
821
  const operations = [];
717
822
  const paths = document.paths;
718
823
  if (!paths) return operations;
719
824
  for (const path of Object.keys(paths)) {
720
825
  if (path.startsWith("x-")) continue;
721
- const pathItem = derefInPlace({
722
- document,
723
- container: paths,
724
- key: path
725
- });
826
+ const pathItem = refs.deref(paths[path]);
726
827
  if (!pathItem) continue;
727
828
  const item = pathItem;
728
829
  for (const method of Object.keys(item)) {
@@ -732,65 +833,15 @@ function getOperations(document) {
732
833
  operations.push({
733
834
  path,
734
835
  method,
735
- schema
836
+ schema,
837
+ pathItem
736
838
  });
737
839
  }
738
840
  }
739
841
  return operations;
740
842
  }
741
843
  //#endregion
742
- //#region src/resolvers.ts
743
- /**
744
- * Reads the server URL from the document's `servers` array at `server.index`,
745
- * interpolating any `server.variables` into the URL template.
746
- *
747
- * Returns `null` when `server.index` is omitted or out of range.
748
- *
749
- * @example Resolve the first server
750
- * `resolveBaseUrl({ document, server: { index: 0 } })`
751
- *
752
- * @example Override a path variable
753
- * `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
754
- */
755
- function resolveBaseUrl({ document, server }) {
756
- const index = server?.index;
757
- const entry = index !== void 0 ? document.servers?.at(index) : void 0;
758
- return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
759
- }
760
- /**
761
- * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
762
- * Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
763
- * Throws if an override value is not in the variable's `enum` list.
764
- *
765
- * @example
766
- * ```ts
767
- * resolveServerUrl(
768
- * { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
769
- * { env: 'prod' },
770
- * )
771
- * // 'https://prod.api.example.com'
772
- * ```
773
- */
774
- function resolveServerUrl(server, overrides) {
775
- if (!server.variables) return server.url;
776
- let url = server.url;
777
- for (const [key, variable] of Object.entries(server.variables)) {
778
- const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
779
- if (value === void 0) continue;
780
- if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.Diagnostics.Error({
781
- code: _kubb_core.Diagnostics.code.invalidServerVariable,
782
- severity: "error",
783
- message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
784
- help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
785
- location: {
786
- kind: "document",
787
- pointer: "#/servers"
788
- }
789
- });
790
- url = url.replaceAll(`{${key}}`, value);
791
- }
792
- return url;
793
- }
844
+ //#region src/emit/schemaShape.ts
794
845
  /**
795
846
  * Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
796
847
  * Formats not in `formatMap` (e.g., `int64`, `date-time`) are handled separately by parser options.
@@ -817,96 +868,57 @@ function getPrimitiveType(type) {
817
868
  return "string";
818
869
  }
819
870
  /**
820
- * Returns all parameters for an operation, merging path-level and operation-level entries.
821
- * Operation-level parameters override path-level ones with the same `in:name` key.
822
- * Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
823
- *
824
- * @example
825
- * ```ts
826
- * getParameters(document, operation)
827
- * // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
828
- * ```
871
+ * Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
872
+ * Returns `null` when `dateType: false`, so the format falls through to `string`.
829
873
  */
830
- function getParameters(document, operation) {
831
- const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
832
- const operationParams = resolveParams(operation.schema?.parameters || []);
833
- const pathItem = document.paths?.[operation.path];
834
- const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : []);
835
- const paramMap = /* @__PURE__ */ new Map();
836
- for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
837
- for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
838
- return Array.from(paramMap.values());
839
- }
840
- function getResponseBody(responseBody, contentType) {
841
- if (!responseBody) return false;
842
- if (isReference(responseBody)) return false;
843
- const body = responseBody;
844
- if (!body.content) return false;
845
- if (contentType) {
846
- if (!(contentType in body.content)) return false;
847
- return body.content[contentType];
874
+ function getDateType(options, format) {
875
+ if (!options.dateType) return null;
876
+ if (format === "date-time") {
877
+ if (options.dateType === "date") return {
878
+ type: "date",
879
+ representation: "date"
880
+ };
881
+ if (options.dateType === "stringOffset") return {
882
+ type: "datetime",
883
+ offset: true
884
+ };
885
+ if (options.dateType === "stringLocal") return {
886
+ type: "datetime",
887
+ local: true
888
+ };
889
+ return {
890
+ type: "datetime",
891
+ offset: false
892
+ };
848
893
  }
849
- const contentTypes = Object.keys(body.content);
850
- const availableContentType = contentTypes.find(isJsonMimeType) ?? contentTypes[0];
851
- if (!availableContentType) return false;
852
- return body.content[availableContentType];
853
- }
854
- function resolveResponseRefs(document, operation) {
855
- const responses = operation.schema.responses;
856
- if (!responses) return;
857
- for (const key in responses) derefInPlace({
858
- document,
859
- container: responses,
860
- key
861
- });
894
+ if (format === "date") return {
895
+ type: "date",
896
+ representation: options.dateType === "date" ? "date" : "string"
897
+ };
898
+ return {
899
+ type: "time",
900
+ representation: options.dateType === "date" ? "date" : "string"
901
+ };
862
902
  }
863
903
  /**
864
- * Returns the response schema for a given operation and HTTP status code.
865
- *
866
- * Returns an empty object `{}` when no response body schema is available.
867
- *
868
- * @example
869
- * ```ts
870
- * getResponseSchema(document, operation, 200) // SchemaObject
871
- * getResponseSchema(document, operation, '4XX') // {}
872
- * ```
904
+ * Reads a schema's numeric `exclusiveMinimum`/`exclusiveMaximum` bounds (the OAS 3.1 numeric
905
+ * form). Either key is `undefined` when absent or, for the legacy OAS 3.0 boolean form, not a
906
+ * number.
873
907
  */
874
- function getResponseSchema(document, operation, statusCode, options = {}) {
875
- resolveResponseRefs(document, operation);
876
- const responseBody = getResponseBody(getResponseByStatusCode({
877
- document,
878
- operation,
879
- statusCode
880
- }), options.contentType);
881
- if (responseBody === false) return {};
882
- const schema = responseBody.schema;
883
- if (!schema) return {};
884
- return dereferenceWithRef(document, schema);
908
+ function getExclusiveBounds(schema) {
909
+ return {
910
+ exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
911
+ exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
912
+ };
885
913
  }
886
914
  /**
887
- * Returns the request body schema for an operation, or `null` when absent.
888
- *
889
- * @example
890
- * ```ts
891
- * getRequestSchema(document, operation) // SchemaObject | null
892
- * ```
915
+ * Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
916
+ * labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
917
+ * 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
893
918
  */
894
- function getRequestSchema(document, operation, options = {}) {
895
- if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
896
- const requestBody = getRequestContent({
897
- document,
898
- operation,
899
- mediaType: options.contentType
900
- });
901
- if (requestBody === false) return null;
902
- const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
903
- const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
904
- if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
905
- type: "string",
906
- contentMediaType: "application/octet-stream"
907
- };
908
- if (!schema) return null;
909
- return dereferenceWithRef(document, schema);
919
+ function extractExamples(schema) {
920
+ if (Array.isArray(schema.examples)) return schema.examples;
921
+ return schema.example !== void 0 ? [schema.example] : void 0;
910
922
  }
911
923
  /**
912
924
  * Returns `true` when `fragment` carries any JSON Schema keyword that makes it
@@ -915,8 +927,7 @@ function getRequestSchema(document, operation, options = {}) {
915
927
  * A fragment with a structural keyword can't be safely merged into a parent schema.
916
928
  */
917
929
  function hasStructuralKeywords(fragment) {
918
- for (const key in fragment) if (structuralKeys.has(key)) return true;
919
- return false;
930
+ return Object.keys(fragment).some((key) => structuralKeys.has(key));
920
931
  }
921
932
  /**
922
933
  * Flattens a keyword-only `allOf` into its parent schema.
@@ -947,192 +958,15 @@ function flattenSchema(schema) {
947
958
  for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
948
959
  return merged;
949
960
  }
961
+ //#endregion
962
+ //#region src/emit/createNode.ts
950
963
  /**
951
- * Extracts the inline schema from a media-type `content` map.
952
- *
953
- * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
954
- * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
955
- *
956
- * @example
957
- * ```ts
958
- * extractSchemaFromContent(operation.content, 'application/json')
959
- * // SchemaObject | null
960
- * ```
961
- */
962
- function extractSchemaFromContent(content, preferredContentType) {
963
- if (!content) return null;
964
- const firstContentType = Object.keys(content)[0] ?? "application/json";
965
- const schema = content[preferredContentType ?? firstContentType]?.schema;
966
- if (schema && "$ref" in schema) return null;
967
- return schema ?? null;
968
- }
969
- /**
970
- * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
971
- */
972
- function* collectRefs(schema) {
973
- if (Array.isArray(schema)) {
974
- for (const item of schema) yield* collectRefs(item);
975
- return;
976
- }
977
- if (schema && typeof schema === "object") for (const key in schema) {
978
- const value = schema[key];
979
- if (!(key === "$ref" && typeof value === "string")) {
980
- yield* collectRefs(value);
981
- continue;
982
- }
983
- if (value.startsWith("#/components/schemas/")) {
984
- const name = value.slice(21);
985
- if (name) yield name;
986
- }
987
- }
988
- }
989
- /**
990
- * Returns a copy of `schemas` topologically sorted by `$ref` dependency.
991
- *
992
- * Referenced schemas appear before the schemas that depend on them, so code generators
993
- * can emit types in the correct order. Cycles are silently skipped.
994
- *
995
- * @example
996
- * ```ts
997
- * const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
998
- * // Pet appears before Order when Order.$ref points at Pet
999
- * ```
1000
- */
1001
- function sortSchemas(schemas) {
1002
- const deps = /* @__PURE__ */ new Map();
1003
- for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
1004
- const sorted = [];
1005
- const visited = /* @__PURE__ */ new Set();
1006
- function visit(name, stack) {
1007
- if (visited.has(name) || stack.has(name)) return;
1008
- stack.add(name);
1009
- for (const child of deps.get(name) ?? []) if (deps.has(child)) visit(child, stack);
1010
- stack.delete(name);
1011
- visited.add(name);
1012
- sorted.push(name);
1013
- }
1014
- for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
1015
- const result = {};
1016
- for (const name of sorted) result[name] = schemas[name];
1017
- return result;
1018
- }
1019
- const semanticSuffixes = {
1020
- schemas: "Schema",
1021
- responses: "Response",
1022
- requestBodies: "Request"
1023
- };
1024
- function resolveSchemaRef(document, schema) {
1025
- if (!isReference(schema)) return schema;
1026
- const resolved = resolveRef(document, schema.$ref);
1027
- return resolved && !isReference(resolved) ? resolved : schema;
1028
- }
1029
- /**
1030
- * Collects component schemas from one or more sources and resolves name collisions.
1031
- *
1032
- * Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
1033
- * topologically sorted by `$ref` dependency so generators emit types in the correct order.
1034
- *
1035
- * When two or more schemas normalize to the same PascalCase name:
1036
- * - Same source → numeric suffix (`2`, `3`, …).
1037
- * - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
1038
- *
1039
- * @example
1040
- * ```ts
1041
- * const { schemas, renames } = getSchemas(document, { contentType: 'application/json' })
1042
- * ```
1043
- */
1044
- function getSchemas(document, { contentType }) {
1045
- const components = document.components;
1046
- const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
1047
- schema: resolveSchemaRef(document, schema),
1048
- source: "schemas",
1049
- originalName: name
1050
- })), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
1051
- const schema = extractSchemaFromContent(item.content, contentType);
1052
- return schema ? [{
1053
- schema: resolveSchemaRef(document, schema),
1054
- source,
1055
- originalName: name
1056
- }] : [];
1057
- }))];
1058
- const normalizedNames = /* @__PURE__ */ new Map();
1059
- for (const item of candidates) {
1060
- const key = pascalCase(item.originalName);
1061
- const bucket = normalizedNames.get(key) ?? [];
1062
- bucket.push(item);
1063
- normalizedNames.set(key, bucket);
1064
- }
1065
- const schemas = {};
1066
- const renames = /* @__PURE__ */ new Map();
1067
- for (const [, items] of normalizedNames) {
1068
- const isSingle = items.length === 1;
1069
- let hasMultipleSources = false;
1070
- if (!isSingle) {
1071
- const firstSource = items[0].source;
1072
- for (const item of items) if (item.source !== firstSource) {
1073
- hasMultipleSources = true;
1074
- break;
1075
- }
1076
- }
1077
- items.forEach((item, index) => {
1078
- const suffix = isSingle ? "" : hasMultipleSources ? semanticSuffixes[item.source] : index === 0 ? "" : String(index + 1);
1079
- const uniqueName = item.originalName + suffix;
1080
- schemas[uniqueName] = item.schema;
1081
- if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
1082
- });
1083
- }
1084
- return {
1085
- schemas: sortSchemas(schemas),
1086
- renames
1087
- };
1088
- }
1089
- /**
1090
- * Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
1091
- * Returns `null` when `dateType: false`, so the format falls through to `string`.
1092
- */
1093
- function getDateType(options, format) {
1094
- if (!options.dateType) return null;
1095
- if (format === "date-time") {
1096
- if (options.dateType === "date") return {
1097
- type: "date",
1098
- representation: "date"
1099
- };
1100
- if (options.dateType === "stringOffset") return {
1101
- type: "datetime",
1102
- offset: true
1103
- };
1104
- if (options.dateType === "stringLocal") return {
1105
- type: "datetime",
1106
- local: true
1107
- };
1108
- return {
1109
- type: "datetime",
1110
- offset: false
1111
- };
1112
- }
1113
- if (format === "date") return {
1114
- type: "date",
1115
- representation: options.dateType === "date" ? "date" : "string"
1116
- };
1117
- return {
1118
- type: "time",
1119
- representation: options.dateType === "date" ? "date" : "string"
1120
- };
1121
- }
1122
- /**
1123
- * Collects the shared metadata fields passed to every `createSchema` call.
964
+ * Builds a schema node from a converter's base context plus its type-specific fields. Every
965
+ * converter needs the same metadata fields (`title`, `description`, `examples`, ...) alongside
966
+ * whatever makes its node distinct; this folds both into one call.
1124
967
  */
1125
- /**
1126
- * Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
1127
- * labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
1128
- * 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
1129
- */
1130
- function extractExamples(schema) {
1131
- if (Array.isArray(schema.examples)) return schema.examples;
1132
- return schema.example !== void 0 ? [schema.example] : void 0;
1133
- }
1134
- function buildSchemaNode(schema, name, nullable, defaultValue) {
1135
- return {
968
+ function createNode({ schema, name, nullable, defaultValue }, extras) {
969
+ return _kubb_ast.ast.factory.createSchema({
1136
970
  name,
1137
971
  nullable,
1138
972
  title: schema.title,
@@ -1142,123 +976,164 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
1142
976
  writeOnly: schema.writeOnly,
1143
977
  default: defaultValue,
1144
978
  examples: extractExamples(schema),
1145
- format: schema.format
1146
- };
979
+ format: schema.format,
980
+ ...extras
981
+ });
1147
982
  }
983
+ //#endregion
984
+ //#region src/emit/discriminator/preserve.ts
1148
985
  /**
1149
- * Returns all request body content type keys for an operation.
1150
- *
1151
- * The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
1152
- * `getRequestSchema` already performs), so the returned list accurately reflects the
1153
- * available content types even for referenced bodies.
986
+ * Creates a single-property object schema used as a discriminator literal.
1154
987
  *
1155
988
  * @example
1156
989
  * ```ts
1157
- * getRequestBodyContentTypes(document, operation)
1158
- * // ['application/json', 'multipart/form-data']
990
+ * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
991
+ * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
1159
992
  * ```
1160
993
  */
1161
- function getRequestBodyContentTypes(document, operation) {
1162
- if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
1163
- const body = operation.schema.requestBody;
1164
- if (!body) return [];
1165
- return body.content ? Object.keys(body.content) : [];
994
+ function createDiscriminantNode({ propertyName, value }) {
995
+ return _kubb_ast.ast.factory.createSchema({
996
+ type: "object",
997
+ primitive: "object",
998
+ properties: [_kubb_ast.ast.factory.createProperty({
999
+ name: propertyName,
1000
+ schema: _kubb_ast.ast.factory.createSchema({
1001
+ type: "enum",
1002
+ primitive: "string",
1003
+ enumValues: [value]
1004
+ }),
1005
+ required: true
1006
+ })]
1007
+ });
1166
1008
  }
1167
1009
  /**
1168
- * Returns all response content type keys for an operation at a given status code.
1169
- *
1170
- * Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
1171
- * so the returned list reflects the available content types even for referenced responses.
1010
+ * Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
1172
1011
  *
1173
1012
  * @example
1174
1013
  * ```ts
1175
- * getResponseBodyContentTypes(document, operation, 200)
1176
- * // ['application/json', 'application/xml']
1014
+ * findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
1177
1015
  * ```
1178
1016
  */
1179
- function getResponseBodyContentTypes(document, operation, statusCode) {
1180
- resolveResponseRefs(document, operation);
1181
- const responseObj = getResponseByStatusCode({
1182
- document,
1183
- operation,
1184
- statusCode
1185
- });
1186
- if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
1187
- const body = responseObj;
1188
- return body.content ? Object.keys(body.content) : [];
1189
- }
1190
- //#endregion
1191
- //#region src/converters.ts
1192
- /**
1193
- * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
1194
- *
1195
- * This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
1196
- * from the array to its items sub-schema, so they are valid for downstream processing.
1197
- *
1198
- * @note A defensive measure for non-compliant specs.
1199
- */
1200
- function normalizeArrayEnum(schema) {
1201
- const normalizedItems = {
1202
- ...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
1203
- enum: schema.enum
1204
- };
1205
- const { enum: _enum, ...schemaWithoutEnum } = schema;
1206
- return {
1207
- ...schemaWithoutEnum,
1208
- items: normalizedItems
1209
- };
1017
+ function findDiscriminator(mapping, ref) {
1018
+ if (!mapping || !ref) return null;
1019
+ return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
1210
1020
  }
1211
1021
  /**
1212
- * Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
1213
- * and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
1022
+ * Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
1023
+ * node with either the shared-properties slice carrying that value, or a synthetic discriminant
1024
+ * literal. The referenced child schema's own definition is left untouched — narrowing happens only
1025
+ * at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
1026
+ * which additionally patches the child schema's own definition in a post-pass).
1214
1027
  */
1215
- function createNullNode(schema, name, nullable) {
1216
- return _kubb_ast.ast.factory.createSchema({
1217
- type: "null",
1218
- primitive: "null",
1219
- name,
1220
- title: schema.title,
1221
- description: schema.description,
1222
- deprecated: schema.deprecated,
1223
- nullable,
1224
- format: schema.format
1028
+ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, rawOptions, name, refs }) {
1029
+ function pickDiscriminatorPropertyNode(node, propertyName) {
1030
+ const discriminatorProperty = _kubb_ast.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1031
+ if (!discriminatorProperty) return null;
1032
+ return _kubb_ast.ast.factory.createSchema({
1033
+ type: "object",
1034
+ primitive: "object",
1035
+ properties: [discriminatorProperty]
1036
+ });
1037
+ }
1038
+ function implicitDiscriminantValue(member) {
1039
+ if (!discriminator || discriminator.mapping || !isReference(member)) return null;
1040
+ const value = (0, _kubb_kit.extractRefName)(member.$ref);
1041
+ if (!value) return null;
1042
+ const variant = refs.resolve(member.$ref, { report: false });
1043
+ if (!variant) return null;
1044
+ const propertyName = discriminator.propertyName;
1045
+ const seen = /* @__PURE__ */ new Set([member.$ref]);
1046
+ function constrains(v) {
1047
+ const prop = v.properties?.[propertyName];
1048
+ const resolved = prop && isReference(prop) ? refs.resolve(prop.$ref, { report: false }) : prop;
1049
+ if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
1050
+ const composition = v.allOf ?? v.oneOf ?? v.anyOf;
1051
+ if (!composition) return false;
1052
+ return composition.some((m) => {
1053
+ if (!isReference(m)) return constrains(m);
1054
+ if (seen.has(m.$ref)) return false;
1055
+ seen.add(m.$ref);
1056
+ const r = refs.resolve(m.$ref, { report: false });
1057
+ return r ? constrains(r) : false;
1058
+ });
1059
+ }
1060
+ return constrains(variant) ? null : value;
1061
+ }
1062
+ return unionMembers.map((s) => {
1063
+ const ref = isReference(s) ? s.$ref : void 0;
1064
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
1065
+ const memberNode = parse({
1066
+ schema: s,
1067
+ name
1068
+ }, rawOptions);
1069
+ if (!discriminatorValue || !discriminator) return memberNode;
1070
+ const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(_kubb_ast.ast.applyMacros(sharedPropertiesNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
1071
+ propertyName: discriminator.propertyName,
1072
+ values: [discriminatorValue]
1073
+ })], { depth: "shallow" }), discriminator.propertyName) : void 0;
1074
+ return _kubb_ast.ast.factory.createSchema({
1075
+ type: "intersection",
1076
+ members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
1077
+ propertyName: discriminator.propertyName,
1078
+ value: discriminatorValue
1079
+ })]
1080
+ });
1225
1081
  });
1226
1082
  }
1227
1083
  /**
1228
- * Names the inline enums on a property's schema, and on each item when the property is a tuple, from
1229
- * the parent and property name. Wraps `macroEnumName` at the property construction site.
1084
+ * Filters the discriminated members out of an `allOf` list: an `allOf` member that `$ref`s a
1085
+ * discriminated union's parent, where this schema is itself one of that union's children, is
1086
+ * dropped from `members` and its discriminant value collected instead — the same synthetic
1087
+ * literal `narrowUnionMembers` produces, so the emitted node stays a plain intersection rather
1088
+ * than nesting the whole parent union one level deeper.
1230
1089
  */
1231
- function nameEnums(node, options) {
1232
- const macro = (0, _kubb_kit.macroEnumName)(options);
1233
- const named = _kubb_ast.ast.applyMacros(node, [macro], { depth: "shallow" });
1234
- const tupleNode = _kubb_ast.ast.narrowSchema(named, "tuple");
1235
- if (tupleNode?.items) {
1236
- const namedItems = tupleNode.items.map((item) => _kubb_ast.ast.applyMacros(item, [macro], { depth: "shallow" }));
1237
- if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
1238
- ...tupleNode,
1239
- items: namedItems
1240
- };
1241
- }
1242
- return named;
1090
+ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
1091
+ const discriminantValues = [];
1092
+ return {
1093
+ members: allOfMembers.filter((item) => {
1094
+ if (!isReference(item) || !name) return true;
1095
+ const deref = refs.resolve(item.$ref);
1096
+ if (!deref || !isDiscriminator(deref)) return true;
1097
+ const parentUnion = deref.oneOf ?? deref.anyOf;
1098
+ if (!parentUnion) return true;
1099
+ const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1100
+ const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1101
+ const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1102
+ if (inOneOf || inMapping) {
1103
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
1104
+ if (discriminatorValue) discriminantValues.push({
1105
+ propertyName: deref.discriminator.propertyName,
1106
+ value: discriminatorValue
1107
+ });
1108
+ return false;
1109
+ }
1110
+ return true;
1111
+ }),
1112
+ discriminantValues
1113
+ };
1243
1114
  }
1115
+ //#endregion
1116
+ //#region src/emit/converters/composition.ts
1244
1117
  /**
1245
1118
  * Converts a `$ref` schema into a `RefSchemaNode`.
1246
1119
  *
1247
1120
  * The resolved schema is stored in `node.schema`. Usage-site sibling fields
1248
1121
  * (description, readOnly, nullable, etc.) are stored directly on the ref node.
1249
1122
  * Use `syncSchemaRef(node)` in printers to get a merged view of both.
1250
- * Circular refs are detected in `resolveRefNode` and leave `schema` as `null`.
1123
+ * Circular refs are detected in `refs.resolveNode` and leave `schema` as `null`.
1251
1124
  */
1252
- function convertRef({ schema, name, nullable, defaultValue, rawOptions, document, resolveRefNode, refExists, renames }) {
1125
+ function convertRef({ schema, name, nullable, defaultValue, rawOptions, document, parse, refs, renames }) {
1253
1126
  const refPath = schema.$ref;
1254
- const resolvedSchema = refPath ? resolveRefNode(refPath, rawOptions) : null;
1255
- if (refPath && document.components && !refExists(refPath)) return _kubb_ast.ast.factory.createSchema({
1256
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1257
- type: "unknown"
1258
- });
1127
+ const resolvedSchema = refPath ? refs.resolveNode(refPath, parse, rawOptions) : null;
1128
+ const ctx = {
1129
+ schema,
1130
+ name,
1131
+ nullable,
1132
+ defaultValue
1133
+ };
1134
+ if (refPath && document.components && !refs.exists(refPath)) return createNode(ctx, { type: "unknown" });
1259
1135
  const targetName = renames?.get(schema.$ref);
1260
- return _kubb_ast.ast.factory.createSchema({
1261
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1136
+ return createNode(ctx, {
1262
1137
  type: "ref",
1263
1138
  name: (0, _kubb_kit.extractRefName)(schema.$ref),
1264
1139
  ref: schema.$ref,
@@ -1269,7 +1144,7 @@ function convertRef({ schema, name, nullable, defaultValue, rawOptions, document
1269
1144
  /**
1270
1145
  * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
1271
1146
  */
1272
- function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse, document }) {
1147
+ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
1273
1148
  if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
1274
1149
  const [memberSchema] = schema.allOf;
1275
1150
  const memberNode = parse({
@@ -1294,26 +1169,12 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
1294
1169
  format: schema.format ?? memberNode.format
1295
1170
  });
1296
1171
  }
1297
- const filteredDiscriminantValues = [];
1298
- const allOfMembers = schema.allOf.filter((item) => {
1299
- if (!isReference(item) || !name) return true;
1300
- const deref = resolveRef(document, item.$ref);
1301
- if (!deref || !isDiscriminator(deref)) return true;
1302
- const parentUnion = deref.oneOf ?? deref.anyOf;
1303
- if (!parentUnion) return true;
1304
- const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1305
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1306
- const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1307
- if (inOneOf || inMapping) {
1308
- const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
1309
- if (discriminatorValue) filteredDiscriminantValues.push({
1310
- propertyName: deref.discriminator.propertyName,
1311
- value: discriminatorValue
1312
- });
1313
- return false;
1314
- }
1315
- return true;
1316
- }).map((s) => parse({
1172
+ const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
1173
+ allOfMembers: schema.allOf,
1174
+ name,
1175
+ refs
1176
+ });
1177
+ const allOfMembers = discriminatedAllOf.map((s) => parse({
1317
1178
  schema: s,
1318
1179
  name
1319
1180
  }, rawOptions));
@@ -1324,7 +1185,7 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
1324
1185
  if (missingRequired.length) {
1325
1186
  const resolvedMembers = schema.allOf.flatMap((item) => {
1326
1187
  if (!isReference(item)) return [item];
1327
- const deref = resolveRef(document, item.$ref);
1188
+ const deref = refs.resolve(item.$ref);
1328
1189
  return deref && !isReference(deref) ? [deref] : [];
1329
1190
  });
1330
1191
  for (const key of missingRequired) for (const resolved of resolvedMembers) {
@@ -1347,61 +1208,33 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
1347
1208
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
1348
1209
  allOfMembers.push(parse({ schema: schemaWithoutAllOf }, rawOptions));
1349
1210
  }
1350
- for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(createDiscriminantNode({
1211
+ for (const { propertyName, value } of discriminantValues) allOfMembers.push(createDiscriminantNode({
1351
1212
  propertyName,
1352
1213
  value
1353
1214
  }));
1354
- return _kubb_ast.ast.factory.createSchema({
1215
+ return createNode({
1216
+ schema,
1217
+ name,
1218
+ nullable,
1219
+ defaultValue
1220
+ }, {
1355
1221
  type: "intersection",
1356
- members: [...(0, _kubb_kit.mergeAdjacentObjectsLazy)(allOfMembers.slice(0, syntheticStart)), ...(0, _kubb_kit.mergeAdjacentObjectsLazy)(allOfMembers.slice(syntheticStart))],
1357
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1222
+ members: [...(0, _kubb_kit.mergeAdjacentObjectsLazy)(allOfMembers.slice(0, syntheticStart)), ...(0, _kubb_kit.mergeAdjacentObjectsLazy)(allOfMembers.slice(syntheticStart))]
1358
1223
  });
1359
1224
  }
1360
1225
  /**
1361
1226
  * Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
1362
1227
  */
1363
- function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse, document }) {
1364
- function pickDiscriminatorPropertyNode(node, propertyName) {
1365
- const discriminatorProperty = _kubb_ast.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1366
- if (!discriminatorProperty) return null;
1367
- return _kubb_ast.ast.factory.createSchema({
1368
- type: "object",
1369
- primitive: "object",
1370
- properties: [discriminatorProperty]
1371
- });
1372
- }
1373
- function resolveRefSilent($ref) {
1374
- if (!$ref.startsWith("#")) return null;
1375
- return decodeURIComponent($ref.substring(1)).split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document) ?? null;
1376
- }
1377
- function implicitDiscriminantValue(member) {
1378
- if (!discriminator || discriminator.mapping || !isReference(member)) return null;
1379
- const value = (0, _kubb_kit.extractRefName)(member.$ref);
1380
- if (!value) return null;
1381
- const variant = resolveRefSilent(member.$ref);
1382
- if (!variant) return null;
1383
- const propertyName = discriminator.propertyName;
1384
- const seen = /* @__PURE__ */ new Set([member.$ref]);
1385
- function constrains(v) {
1386
- const prop = v.properties?.[propertyName];
1387
- const resolved = prop && isReference(prop) ? resolveRefSilent(prop.$ref) : prop;
1388
- if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
1389
- const composition = v.allOf ?? v.oneOf ?? v.anyOf;
1390
- if (!composition) return false;
1391
- return composition.some((m) => {
1392
- if (!isReference(m)) return constrains(m);
1393
- if (seen.has(m.$ref)) return false;
1394
- seen.add(m.$ref);
1395
- const r = resolveRefSilent(m.$ref);
1396
- return r ? constrains(r) : false;
1397
- });
1398
- }
1399
- return constrains(variant) ? null : value;
1400
- }
1228
+ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
1229
+ const ctx = {
1230
+ schema,
1231
+ name,
1232
+ nullable,
1233
+ defaultValue
1234
+ };
1401
1235
  const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
1402
1236
  const strategy = schema.oneOf ? "one" : "any";
1403
- const unionBase = {
1404
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1237
+ const unionExtras = {
1405
1238
  discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1406
1239
  strategy
1407
1240
  };
@@ -1412,41 +1245,29 @@ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
1412
1245
  name
1413
1246
  }, rawOptions) : void 0;
1414
1247
  if (sharedPropertiesNode || discriminator) {
1415
- const members = unionMembers.map((s) => {
1416
- const ref = isReference(s) ? s.$ref : void 0;
1417
- const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
1418
- const memberNode = parse({
1419
- schema: s,
1420
- name
1421
- }, rawOptions);
1422
- if (!discriminatorValue || !discriminator) return memberNode;
1423
- const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(_kubb_ast.ast.applyMacros(sharedPropertiesNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
1424
- propertyName: discriminator.propertyName,
1425
- values: [discriminatorValue]
1426
- })], { depth: "shallow" }), discriminator.propertyName) : void 0;
1427
- return _kubb_ast.ast.factory.createSchema({
1428
- type: "intersection",
1429
- members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
1430
- propertyName: discriminator.propertyName,
1431
- value: discriminatorValue
1432
- })]
1433
- });
1248
+ const members = narrowUnionMembers({
1249
+ unionMembers,
1250
+ discriminator,
1251
+ sharedPropertiesNode,
1252
+ parse,
1253
+ rawOptions,
1254
+ name,
1255
+ refs
1434
1256
  });
1435
- const unionNode = _kubb_ast.ast.factory.createSchema({
1257
+ const unionNode = createNode(ctx, {
1436
1258
  type: "union",
1437
- ...unionBase,
1259
+ ...unionExtras,
1438
1260
  members
1439
1261
  });
1440
1262
  if (!sharedPropertiesNode) return unionNode;
1441
- return _kubb_ast.ast.factory.createSchema({
1263
+ return createNode(ctx, {
1442
1264
  type: "intersection",
1443
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1444
1265
  members: [unionNode, sharedPropertiesNode]
1445
1266
  });
1446
1267
  }
1447
- const unionNode = _kubb_ast.ast.factory.createSchema({
1268
+ const unionNode = createNode(ctx, {
1448
1269
  type: "union",
1449
- ...unionBase,
1270
+ ...unionExtras,
1450
1271
  members: unionMembers.map((s) => parse({
1451
1272
  schema: s,
1452
1273
  name
@@ -1455,60 +1276,125 @@ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
1455
1276
  return _kubb_ast.ast.applyMacros(unionNode, [_kubb_kit.macroSimplifyUnion], { depth: "shallow" });
1456
1277
  }
1457
1278
  /**
1279
+ * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
1280
+ * Only called once the multi-type rule's `match` has confirmed more than one non-`null` type
1281
+ * remains; a single remaining type (e.g. `['string', 'null']`) is handled as that type instead,
1282
+ * with nullability already folded in.
1283
+ */
1284
+ function convertMultiType({ schema, name, nullable, defaultValue, rawOptions, parse }) {
1285
+ const types = schema.type;
1286
+ const nonNullTypes = types.filter((t) => t !== "null");
1287
+ return createNode({
1288
+ schema,
1289
+ name,
1290
+ nullable: types.includes("null") || nullable || void 0,
1291
+ defaultValue
1292
+ }, {
1293
+ type: "union",
1294
+ members: nonNullTypes.map((t) => {
1295
+ return parse({
1296
+ schema: {
1297
+ ...schema,
1298
+ type: t
1299
+ },
1300
+ name
1301
+ }, rawOptions);
1302
+ })
1303
+ });
1304
+ }
1305
+ //#endregion
1306
+ //#region src/emit/converters/scalar.ts
1307
+ /**
1308
+ * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
1309
+ *
1310
+ * This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
1311
+ * from the array to its items sub-schema, so they are valid for downstream processing.
1312
+ *
1313
+ * @note A defensive measure for non-compliant specs.
1314
+ */
1315
+ function normalizeArrayEnum(schema) {
1316
+ const normalizedItems = {
1317
+ ...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
1318
+ enum: schema.enum
1319
+ };
1320
+ const { enum: _enum, ...schemaWithoutEnum } = schema;
1321
+ return {
1322
+ ...schemaWithoutEnum,
1323
+ items: normalizedItems
1324
+ };
1325
+ }
1326
+ /**
1327
+ * Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
1328
+ * and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
1329
+ */
1330
+ function createNullNode(schema, name, nullable) {
1331
+ return _kubb_ast.ast.factory.createSchema({
1332
+ type: "null",
1333
+ primitive: "null",
1334
+ name,
1335
+ title: schema.title,
1336
+ description: schema.description,
1337
+ deprecated: schema.deprecated,
1338
+ nullable,
1339
+ format: schema.format
1340
+ });
1341
+ }
1342
+ /**
1458
1343
  * Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
1459
1344
  */
1460
1345
  function convertConst({ schema, name, nullable, defaultValue }) {
1461
1346
  const constValue = schema.const;
1462
1347
  if (constValue === null) return createNullNode(schema, name);
1463
1348
  const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
1464
- return _kubb_ast.ast.factory.createSchema({
1349
+ return createNode({
1350
+ schema,
1351
+ name,
1352
+ nullable,
1353
+ defaultValue
1354
+ }, {
1465
1355
  type: "enum",
1466
1356
  primitive: constPrimitive,
1467
- enumValues: [constValue],
1468
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1357
+ enumValues: [constValue]
1469
1358
  });
1470
1359
  }
1471
1360
  /**
1472
- * Converts a format-annotated schema into a special-type `SchemaNode`.
1473
- * Returns `null` when the format should fall through to string handling (`dateType: false`).
1361
+ * Converts a format-annotated schema into a special-type `SchemaNode`. Only called once the
1362
+ * `format` rule's `match` has confirmed the format is handled (see `isHandledFormat`) and, for
1363
+ * a date-ish format, that `dateType` is not `false`.
1474
1364
  */
1475
1365
  function convertFormat({ schema, name, nullable, defaultValue, options }) {
1476
- const base = buildSchemaNode(schema, name, nullable, defaultValue);
1477
- if (schema.format === "int64") return _kubb_ast.ast.factory.createSchema({
1366
+ const ctx = {
1367
+ schema,
1368
+ name,
1369
+ nullable,
1370
+ defaultValue
1371
+ };
1372
+ if (schema.format === "int64") return createNode(ctx, {
1478
1373
  type: options.integerType === "bigint" ? "bigint" : "integer",
1479
1374
  primitive: "integer",
1480
- ...base,
1481
1375
  min: schema.minimum,
1482
1376
  max: schema.maximum,
1483
- exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1484
- exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
1377
+ ...getExclusiveBounds(schema)
1485
1378
  });
1486
1379
  if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
1487
1380
  const dateType = getDateType(options, schema.format);
1488
- if (!dateType) return null;
1489
- if (dateType.type === "datetime") return _kubb_ast.ast.factory.createSchema({
1490
- ...base,
1381
+ if (dateType.type === "datetime") return createNode(ctx, {
1491
1382
  primitive: "string",
1492
1383
  type: "datetime",
1493
1384
  offset: dateType.offset,
1494
1385
  local: dateType.local
1495
1386
  });
1496
- return _kubb_ast.ast.factory.createSchema({
1497
- ...base,
1387
+ return createNode(ctx, {
1498
1388
  primitive: "string",
1499
1389
  type: dateType.type,
1500
1390
  representation: dateType.representation
1501
1391
  });
1502
1392
  }
1503
1393
  const specialType = getSchemaType(schema.format);
1504
- if (!specialType) return null;
1505
- const specialPrimitive = specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string";
1506
- const hasLength = specialType === "url" || specialType === "uuid" || specialType === "email";
1507
- return _kubb_ast.ast.factory.createSchema({
1508
- ...base,
1509
- primitive: specialPrimitive,
1394
+ return createNode(ctx, {
1395
+ primitive: specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string",
1510
1396
  type: specialType,
1511
- ...hasLength ? {
1397
+ ...specialType === "url" || specialType === "uuid" || specialType === "email" ? {
1512
1398
  min: schema.minLength,
1513
1399
  max: schema.maxLength
1514
1400
  } : {}
@@ -1528,21 +1414,28 @@ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
1528
1414
  const enumNullable = nullable || nullInEnum || void 0;
1529
1415
  const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1530
1416
  const enumPrimitive = getPrimitiveType(type);
1531
- const enumBase = {
1417
+ const ctx = {
1418
+ schema,
1419
+ name,
1420
+ nullable: enumNullable,
1421
+ defaultValue: enumDefault
1422
+ };
1423
+ const enumExtras = {
1532
1424
  type: "enum",
1533
- primitive: enumPrimitive,
1534
- ...buildSchemaNode(schema, name, enumNullable, enumDefault)
1425
+ primitive: enumPrimitive
1535
1426
  };
1536
1427
  const extensionKey = enumExtensionKeys.find((key) => key in schema);
1537
1428
  const descriptionKey = enumDescriptionKeys.find((key) => key in schema);
1538
1429
  if (extensionKey || descriptionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
1539
- const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
1430
+ let enumPrimitiveType = "string";
1431
+ if (enumPrimitive === "number" || enumPrimitive === "integer") enumPrimitiveType = "number";
1432
+ else if (enumPrimitive === "boolean") enumPrimitiveType = "boolean";
1540
1433
  const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
1541
1434
  const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
1542
1435
  const uniqueValues = [...new Set(filteredValues)];
1543
1436
  const seenNames = /* @__PURE__ */ new Set();
1544
- return _kubb_ast.ast.factory.createSchema({
1545
- ...enumBase,
1437
+ return createNode(ctx, {
1438
+ ...enumExtras,
1546
1439
  primitive: enumPrimitiveType,
1547
1440
  namedEnumValues: uniqueValues.map((value, index) => ({
1548
1441
  name: String(rawEnumNames?.[index] ?? value),
@@ -1556,12 +1449,103 @@ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
1556
1449
  })
1557
1450
  });
1558
1451
  }
1559
- return _kubb_ast.ast.factory.createSchema({
1560
- ...enumBase,
1452
+ return createNode(ctx, {
1453
+ ...enumExtras,
1561
1454
  enumValues: [...new Set(filteredValues)]
1562
1455
  });
1563
1456
  }
1564
1457
  /**
1458
+ * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1459
+ */
1460
+ function convertString({ schema, name, nullable, defaultValue }) {
1461
+ return createNode({
1462
+ schema,
1463
+ name,
1464
+ nullable,
1465
+ defaultValue
1466
+ }, {
1467
+ type: "string",
1468
+ primitive: "string",
1469
+ min: schema.minLength,
1470
+ max: schema.maxLength,
1471
+ pattern: schema.pattern
1472
+ });
1473
+ }
1474
+ /**
1475
+ * Converts a `type: 'number'` or `type: 'integer'` schema.
1476
+ */
1477
+ function convertNumeric({ schema, name, nullable, defaultValue }, type) {
1478
+ return createNode({
1479
+ schema,
1480
+ name,
1481
+ nullable,
1482
+ defaultValue
1483
+ }, {
1484
+ type,
1485
+ primitive: type,
1486
+ min: schema.minimum,
1487
+ max: schema.maximum,
1488
+ ...getExclusiveBounds(schema),
1489
+ multipleOf: schema.multipleOf
1490
+ });
1491
+ }
1492
+ /**
1493
+ * Converts a `type: 'boolean'` schema.
1494
+ */
1495
+ function convertBoolean({ schema, name, nullable, defaultValue }) {
1496
+ return createNode({
1497
+ schema,
1498
+ name,
1499
+ nullable,
1500
+ defaultValue
1501
+ }, {
1502
+ type: "boolean",
1503
+ primitive: "boolean"
1504
+ });
1505
+ }
1506
+ /**
1507
+ * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
1508
+ * into a `blob` node.
1509
+ */
1510
+ function convertBinary({ schema, name, nullable, defaultValue }) {
1511
+ return createNode({
1512
+ schema,
1513
+ name,
1514
+ nullable,
1515
+ defaultValue
1516
+ }, {
1517
+ type: "blob",
1518
+ primitive: "string"
1519
+ });
1520
+ }
1521
+ //#endregion
1522
+ //#region src/emit/converters/structural.ts
1523
+ /**
1524
+ * Resolves a `true` or empty-object map schema (`additionalProperties`/`patternProperties`) to
1525
+ * `options.unknownType`, otherwise parses it as a regular schema.
1526
+ */
1527
+ function resolveMapSchema(mapSchema, options, parse, rawOptions) {
1528
+ if (mapSchema === true || typeof mapSchema === "object" && Object.keys(mapSchema).length === 0) return _kubb_ast.ast.factory.createSchema({ type: options.unknownType });
1529
+ return parse({ schema: mapSchema }, rawOptions);
1530
+ }
1531
+ /**
1532
+ * Names the inline enums on a property's schema, and on each item when the property is a tuple, from
1533
+ * the parent and property name. Wraps `macroEnumName` at the property construction site.
1534
+ */
1535
+ function nameEnums(node, options) {
1536
+ const macro = (0, _kubb_kit.macroEnumName)(options);
1537
+ const named = _kubb_ast.ast.applyMacros(node, [macro], { depth: "shallow" });
1538
+ const tupleNode = _kubb_ast.ast.narrowSchema(named, "tuple");
1539
+ if (tupleNode?.items) {
1540
+ const namedItems = tupleNode.items.map((item) => _kubb_ast.ast.applyMacros(item, [macro], { depth: "shallow" }));
1541
+ if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
1542
+ ...tupleNode,
1543
+ items: namedItems
1544
+ };
1545
+ }
1546
+ return named;
1547
+ }
1548
+ /**
1565
1549
  * Converts an object-like schema into an `ObjectSchemaNode`.
1566
1550
  */
1567
1551
  function convertObject({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
@@ -1587,146 +1571,84 @@ function convertObject({ schema, name, nullable, defaultValue, rawOptions, optio
1587
1571
  });
1588
1572
  }) : [];
1589
1573
  const additionalProperties = schema.additionalProperties;
1590
- const additionalPropertiesNode = (() => {
1591
- if (additionalProperties === true) return true;
1592
- if (additionalProperties === false) return false;
1593
- if (additionalProperties && Object.keys(additionalProperties).length > 0) return parse({ schema: additionalProperties }, rawOptions);
1594
- if (additionalProperties) return _kubb_ast.ast.factory.createSchema({ type: options.unknownType });
1595
- })();
1574
+ let additionalPropertiesNode;
1575
+ if (additionalProperties === true) additionalPropertiesNode = true;
1576
+ else if (additionalProperties) additionalPropertiesNode = resolveMapSchema(additionalProperties, options, parse, rawOptions);
1577
+ else additionalPropertiesNode = additionalProperties;
1596
1578
  const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
1597
- 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;
1598
- const objectNode = _kubb_ast.ast.factory.createSchema({
1579
+ const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, resolveMapSchema(patternSchema, options, parse, rawOptions)])) : void 0;
1580
+ const objectNode = createNode({
1581
+ schema,
1582
+ name,
1583
+ nullable,
1584
+ defaultValue
1585
+ }, {
1599
1586
  type: "object",
1600
1587
  primitive: "object",
1601
1588
  properties,
1602
1589
  additionalProperties: additionalPropertiesNode,
1603
1590
  patternProperties,
1604
1591
  minProperties: schema.minProperties,
1605
- maxProperties: schema.maxProperties,
1606
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1592
+ maxProperties: schema.maxProperties
1607
1593
  });
1608
1594
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
1609
1595
  const discPropName = schema.discriminator.propertyName;
1610
1596
  const values = Object.keys(schema.discriminator.mapping);
1611
1597
  const enumName = name ? (0, _kubb_kit.enumPropName)(name, discPropName, options.enumSuffix) : void 0;
1612
- return _kubb_ast.ast.applyMacros(objectNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
1613
- propertyName: discPropName,
1614
- values,
1615
- enumName
1616
- })], { depth: "shallow" });
1617
- }
1618
- return objectNode;
1619
- }
1620
- /**
1621
- * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
1622
- */
1623
- function convertTuple({ schema, name, nullable, defaultValue, rawOptions, parse }) {
1624
- const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
1625
- const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? _kubb_ast.ast.factory.createSchema({ type: "any" }) : parse({ schema: schema.items }, rawOptions);
1626
- return _kubb_ast.ast.factory.createSchema({
1627
- type: "tuple",
1628
- primitive: "array",
1629
- items: tupleItems,
1630
- rest,
1631
- min: schema.minItems,
1632
- max: schema.maxItems,
1633
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1634
- });
1635
- }
1636
- /**
1637
- * Converts a `type: 'array'` schema into an `ArraySchemaNode`.
1638
- */
1639
- function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
1640
- const rawItems = schema.items;
1641
- const itemName = rawItems?.enum?.length && name ? (0, _kubb_kit.enumPropName)(null, name, options.enumSuffix) : name;
1642
- const items = rawItems ? [parse({
1643
- schema: rawItems,
1644
- name: itemName
1645
- }, rawOptions)] : [];
1646
- return _kubb_ast.ast.factory.createSchema({
1647
- type: "array",
1648
- primitive: "array",
1649
- items,
1650
- min: schema.minItems,
1651
- max: schema.maxItems,
1652
- unique: schema.uniqueItems ?? void 0,
1653
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1654
- });
1655
- }
1656
- /**
1657
- * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1658
- */
1659
- function convertString({ schema, name, nullable, defaultValue }) {
1660
- return _kubb_ast.ast.factory.createSchema({
1661
- type: "string",
1662
- primitive: "string",
1663
- min: schema.minLength,
1664
- max: schema.maxLength,
1665
- pattern: schema.pattern,
1666
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1667
- });
1668
- }
1669
- /**
1670
- * Converts a `type: 'number'` or `type: 'integer'` schema.
1671
- */
1672
- function convertNumeric({ schema, name, nullable, defaultValue }, type) {
1673
- return _kubb_ast.ast.factory.createSchema({
1674
- type,
1675
- primitive: type,
1676
- min: schema.minimum,
1677
- max: schema.maximum,
1678
- exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1679
- exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
1680
- multipleOf: schema.multipleOf,
1681
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1682
- });
1683
- }
1684
- /**
1685
- * Converts a `type: 'boolean'` schema.
1686
- */
1687
- function convertBoolean({ schema, name, nullable, defaultValue }) {
1688
- return _kubb_ast.ast.factory.createSchema({
1689
- type: "boolean",
1690
- primitive: "boolean",
1691
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1692
- });
1598
+ return _kubb_ast.ast.applyMacros(objectNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
1599
+ propertyName: discPropName,
1600
+ values,
1601
+ enumName
1602
+ })], { depth: "shallow" });
1603
+ }
1604
+ return objectNode;
1693
1605
  }
1694
1606
  /**
1695
- * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
1696
- * into a `blob` node.
1607
+ * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
1697
1608
  */
1698
- function convertBinary({ schema, name, nullable, defaultValue }) {
1699
- return _kubb_ast.ast.factory.createSchema({
1700
- type: "blob",
1701
- primitive: "string",
1702
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1609
+ function convertTuple({ schema, name, nullable, defaultValue, rawOptions, parse }) {
1610
+ const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
1611
+ const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? _kubb_ast.ast.factory.createSchema({ type: "any" }) : parse({ schema: schema.items }, rawOptions);
1612
+ return createNode({
1613
+ schema,
1614
+ name,
1615
+ nullable,
1616
+ defaultValue
1617
+ }, {
1618
+ type: "tuple",
1619
+ primitive: "array",
1620
+ items: tupleItems,
1621
+ rest,
1622
+ min: schema.minItems,
1623
+ max: schema.maxItems
1703
1624
  });
1704
1625
  }
1705
1626
  /**
1706
- * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
1707
- *
1708
- * Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parse`
1709
- * falls through and handles it as that single type with nullability already folded in.
1627
+ * Converts a `type: 'array'` schema into an `ArraySchemaNode`.
1710
1628
  */
1711
- function convertMultiType({ schema, name, nullable, defaultValue, rawOptions, parse }) {
1712
- const types = schema.type;
1713
- const nonNullTypes = types.filter((t) => t !== "null");
1714
- if (nonNullTypes.length <= 1) return null;
1715
- const arrayNullable = types.includes("null") || nullable || void 0;
1716
- return _kubb_ast.ast.factory.createSchema({
1717
- type: "union",
1718
- members: nonNullTypes.map((t) => {
1719
- return parse({
1720
- schema: {
1721
- ...schema,
1722
- type: t
1723
- },
1724
- name
1725
- }, rawOptions);
1726
- }),
1727
- ...buildSchemaNode(schema, name, arrayNullable, defaultValue)
1629
+ function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
1630
+ const rawItems = schema.items;
1631
+ const itemName = rawItems?.enum?.length && name ? (0, _kubb_kit.enumPropName)(null, name, options.enumSuffix) : name;
1632
+ const items = rawItems ? [parse({
1633
+ schema: rawItems,
1634
+ name: itemName
1635
+ }, rawOptions)] : [];
1636
+ return createNode({
1637
+ schema,
1638
+ name,
1639
+ nullable,
1640
+ defaultValue
1641
+ }, {
1642
+ type: "array",
1643
+ primitive: "array",
1644
+ items,
1645
+ min: schema.minItems,
1646
+ max: schema.maxItems,
1647
+ unique: schema.uniqueItems ?? void 0
1728
1648
  });
1729
1649
  }
1650
+ //#endregion
1651
+ //#region src/emit/parseSchema.ts
1730
1652
  /**
1731
1653
  * Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
1732
1654
  * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
@@ -1751,7 +1673,11 @@ const schemaRules = [
1751
1673
  convert: convertConst
1752
1674
  },
1753
1675
  {
1754
- match: ({ schema }) => !!schema.format,
1676
+ match: ({ schema, options }) => {
1677
+ if (!schema.format) return false;
1678
+ if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") return options.dateType !== false;
1679
+ return isHandledFormat(schema.format);
1680
+ },
1755
1681
  convert: convertFormat
1756
1682
  },
1757
1683
  {
@@ -1759,7 +1685,7 @@ const schemaRules = [
1759
1685
  convert: convertBinary
1760
1686
  },
1761
1687
  {
1762
- match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
1688
+ match: ({ schema }) => Array.isArray(schema.type) && schema.type.filter((t) => t !== "null").length > 1,
1763
1689
  convert: convertMultiType
1764
1690
  },
1765
1691
  {
@@ -1808,72 +1734,306 @@ const schemaRules = [
1808
1734
  }
1809
1735
  ];
1810
1736
  //#endregion
1811
- //#region src/parser.ts
1737
+ //#region src/refs.ts
1738
+ const _refCache = /* @__PURE__ */ new WeakMap();
1812
1739
  /**
1813
- * Creates the schema and operation converters bound to one OpenAPI document.
1740
+ * Walks a local `#/...` JSON pointer against `document`, memoized per document. `applicable` is
1741
+ * `false` for an empty or non-local ref (the caller should not treat that as a failed lookup).
1742
+ * Shared by `resolveRef`'s reporting walk and `createRefs().resolve`'s silent walk, so both use
1743
+ * the same trimming and caching instead of two separate implementations.
1744
+ */
1745
+ function walkPointer(document, $ref) {
1746
+ const trimmed = $ref.trim();
1747
+ if (trimmed === "" || !trimmed.startsWith("#")) return {
1748
+ applicable: false,
1749
+ value: null
1750
+ };
1751
+ const pointer = globalThis.decodeURIComponent(trimmed.substring(1));
1752
+ let docCache = _refCache.get(document);
1753
+ if (!docCache) {
1754
+ docCache = /* @__PURE__ */ new Map();
1755
+ _refCache.set(document, docCache);
1756
+ }
1757
+ if (docCache.has(pointer)) return {
1758
+ applicable: true,
1759
+ value: docCache.get(pointer)
1760
+ };
1761
+ const current = pointer.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
1762
+ if (current) docCache.set(pointer, current);
1763
+ return {
1764
+ applicable: true,
1765
+ value: current ?? null
1766
+ };
1767
+ }
1768
+ /**
1769
+ * Resolves a local JSON pointer reference from a document.
1814
1770
  *
1815
- * Owns the per-instance `$ref` state (cycle detection, resolved-node cache, existence cache) and
1816
- * the `parseSchema` recursion seam, then dispatches each schema through the ordered `schemaRules`
1817
- * table from `converters.ts`. Every converter is a standalone function that recurses through the
1818
- * `parse` function passed to it, so this file only wires state to the converters.
1771
+ * Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
1772
+ * resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
1773
+ * build there is no sink to collect it, so it throws instead.
1819
1774
  *
1820
- * @internal
1775
+ * @example
1776
+ * ```ts
1777
+ * resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
1778
+ * ```
1821
1779
  */
1822
- function createSchemaParser(ctx) {
1823
- const document = ctx.document;
1824
- /**
1825
- * Tracks `$ref` paths that are currently being resolved to prevent infinite
1826
- * recursion when schemas contain circular references (e.g. `Pet → parent → Pet`).
1827
- */
1780
+ function resolveRef(document, $ref) {
1781
+ const { applicable, value } = walkPointer(document, $ref);
1782
+ if (!applicable) return null;
1783
+ if (value) return value;
1784
+ const diagnostic = {
1785
+ code: _kubb_core.Diagnostics.code.refNotFound,
1786
+ severity: "error",
1787
+ message: `Could not find a definition for ${$ref}.`,
1788
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
1789
+ location: {
1790
+ kind: "schema",
1791
+ pointer: $ref,
1792
+ ref: $ref
1793
+ }
1794
+ };
1795
+ if (!_kubb_core.Diagnostics.report(diagnostic)) throw new _kubb_core.Diagnostics.Error(diagnostic);
1796
+ return null;
1797
+ }
1798
+ /**
1799
+ * Resolves a `$ref` object while preserving the original `$ref` field on the result.
1800
+ *
1801
+ * Useful for parser flows that need both dereferenced fields and pointer
1802
+ * identity (for naming/import purposes). Non-reference values are returned as-is.
1803
+ *
1804
+ * @example
1805
+ * ```ts
1806
+ * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
1807
+ * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
1808
+ * ```
1809
+ */
1810
+ function dereferenceWithRef(document, schema) {
1811
+ if (isReference(schema)) return {
1812
+ ...schema,
1813
+ ...resolveRef(document, schema.$ref),
1814
+ $ref: schema.$ref
1815
+ };
1816
+ return schema;
1817
+ }
1818
+ /**
1819
+ * Creates the `$ref` resolution service for one document.
1820
+ *
1821
+ * Replaces what used to be six overlapping resolvers (a reporting walk, a silent walk, an
1822
+ * existence check, and a resolve-then-parse-into-a-node step, each with its own cache) with one
1823
+ * pointer walk and one explicit `report` contract for a missing ref: `report: true` (the default)
1824
+ * reports a `refNotFound` diagnostic (or throws outside a build), `report: false` resolves to
1825
+ * `null` silently for a speculative lookup.
1826
+ *
1827
+ * @example
1828
+ * ```ts
1829
+ * const refs = createRefs(document)
1830
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet')
1831
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
1832
+ * refs.exists('#/components/schemas/Pet')
1833
+ * refs.resolveNode('#/components/schemas/Pet', parseSchema)
1834
+ * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
1835
+ * ```
1836
+ */
1837
+ function createRefs(document) {
1838
+ const resolvedNodeCache = /* @__PURE__ */ new Map();
1839
+ const existenceCache = /* @__PURE__ */ new Map();
1828
1840
  const resolvingRefs = /* @__PURE__ */ new Set();
1829
1841
  /**
1830
- * Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
1831
- *
1832
- * Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
1833
- * it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
1834
- * since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
1835
- * Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
1842
+ * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
1843
+ * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
1844
+ * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
1845
+ * for a speculative lookup where a missing ref is not an error.
1836
1846
  */
1837
- const resolvedRefCache = /* @__PURE__ */ new Map();
1847
+ function resolve(refPath, options) {
1848
+ if (options?.report === false) {
1849
+ const { applicable, value } = walkPointer(document, refPath);
1850
+ return applicable ? value : null;
1851
+ }
1852
+ return resolveRef(document, refPath);
1853
+ }
1838
1854
  /**
1839
- * Memoized record of whether a `$ref` path resolves to a node the document actually defines.
1855
+ * Returns `true` when a `$ref` path resolves to a component the document actually defines.
1840
1856
  * A circular ref still resolves to an existing target, so this stays `true` for cycles and only
1841
- * goes `false` for a `$ref` that points at a component the spec never declares.
1857
+ * goes `false` for a `$ref` that points at a component the spec never declares. Memoized.
1842
1858
  */
1843
- const refExistence = /* @__PURE__ */ new Map();
1844
- function refExists(refPath) {
1845
- if (!refExistence.has(refPath)) {
1846
- let exists = false;
1847
- try {
1848
- exists = !!resolveRef(document, refPath);
1849
- } catch {
1850
- exists = false;
1851
- }
1852
- refExistence.set(refPath, exists);
1853
- }
1854
- return refExistence.get(refPath) ?? false;
1859
+ function exists(refPath) {
1860
+ if (!existenceCache.has(refPath)) existenceCache.set(refPath, !!resolve(refPath, { report: false }));
1861
+ return existenceCache.get(refPath) ?? false;
1855
1862
  }
1856
1863
  /**
1857
- * Resolves a `$ref` to its parsed node, guarding against cycles and memoizing per instance.
1858
- * Returns `null` when the ref is currently being resolved (a cycle) or cannot be resolved
1859
- * (e.g. a minimal document in a unit test).
1864
+ * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
1865
+ * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
1866
+ * resolved (e.g. a minimal document in a unit test).
1860
1867
  */
1861
- function resolveRefNode(refPath, rawOptions) {
1868
+ function resolveNode(refPath, parse, rawOptions) {
1862
1869
  if (resolvingRefs.has(refPath)) return null;
1863
- if (!resolvedRefCache.has(refPath)) {
1870
+ if (!resolvedNodeCache.has(refPath)) {
1864
1871
  let resolved = null;
1865
1872
  try {
1866
- const referenced = resolveRef(document, refPath);
1873
+ const referenced = resolve(refPath);
1867
1874
  if (referenced) {
1868
1875
  resolvingRefs.add(refPath);
1869
- resolved = parseSchema({ schema: referenced }, rawOptions);
1876
+ resolved = parse({ schema: referenced }, rawOptions);
1870
1877
  resolvingRefs.delete(refPath);
1871
1878
  }
1872
1879
  } catch {}
1873
- resolvedRefCache.set(refPath, resolved);
1880
+ resolvedNodeCache.set(refPath, resolved);
1874
1881
  }
1875
- return resolvedRefCache.get(refPath) ?? null;
1882
+ return resolvedNodeCache.get(refPath) ?? null;
1883
+ }
1884
+ /**
1885
+ * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
1886
+ * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
1887
+ * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
1888
+ * returned as-is.
1889
+ *
1890
+ * @example
1891
+ * ```ts
1892
+ * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
1893
+ * ```
1894
+ */
1895
+ function deref(value) {
1896
+ if (!isReference(value)) return value ? value : null;
1897
+ const resolved = resolve(value.$ref);
1898
+ return resolved && !isReference(resolved) ? resolved : null;
1876
1899
  }
1900
+ return {
1901
+ resolve,
1902
+ exists,
1903
+ resolveNode,
1904
+ deref
1905
+ };
1906
+ }
1907
+ //#endregion
1908
+ //#region src/model/operations.ts
1909
+ /**
1910
+ * Returns all parameters for an operation, merging path-level and operation-level entries.
1911
+ * Operation-level parameters override path-level ones with the same `in:name` key.
1912
+ * Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
1913
+ *
1914
+ * @example
1915
+ * ```ts
1916
+ * getParameters({ document, operation })
1917
+ * // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
1918
+ * ```
1919
+ */
1920
+ function getParameters({ document, operation }) {
1921
+ const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
1922
+ const operationParams = resolveParams(operation.schema?.parameters || []);
1923
+ const pathLevelParams = resolveParams(operation.pathItem.parameters ?? []);
1924
+ const paramMap = /* @__PURE__ */ new Map();
1925
+ for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
1926
+ for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
1927
+ return Array.from(paramMap.values());
1928
+ }
1929
+ function getResponseBody(responseBody, contentType) {
1930
+ if (!responseBody) return false;
1931
+ if (isReference(responseBody)) return false;
1932
+ const body = responseBody;
1933
+ if (!body.content) return false;
1934
+ if (contentType) return contentType in body.content ? body.content[contentType] : false;
1935
+ const picked = pickContentEntry(body.content);
1936
+ return picked ? picked[1] : false;
1937
+ }
1938
+ /**
1939
+ * Returns the response schema for a given operation and HTTP status code.
1940
+ *
1941
+ * Returns an empty object `{}` when no response body schema is available.
1942
+ *
1943
+ * @example
1944
+ * ```ts
1945
+ * getResponseSchema({ document, operation, refs, statusCode: 200 }) // SchemaObject
1946
+ * getResponseSchema({ document, operation, refs, statusCode: '4XX' }) // {}
1947
+ * ```
1948
+ */
1949
+ function getResponseSchema({ document, operation, refs, statusCode, options = {} }) {
1950
+ const responseBody = getResponseBody(getResponseByStatusCode({
1951
+ operation,
1952
+ refs,
1953
+ statusCode
1954
+ }), options.contentType);
1955
+ if (responseBody === false) return {};
1956
+ const schema = responseBody.schema;
1957
+ if (!schema) return {};
1958
+ return dereferenceWithRef(document, schema);
1959
+ }
1960
+ /**
1961
+ * Returns the request body schema for an operation, or `null` when absent.
1962
+ *
1963
+ * @example
1964
+ * ```ts
1965
+ * getRequestSchema({ document, operation, refs }) // SchemaObject | null
1966
+ * ```
1967
+ */
1968
+ function getRequestSchema({ document, operation, refs, options = {} }) {
1969
+ const requestBody = getRequestContent({
1970
+ operation,
1971
+ refs,
1972
+ mediaType: options.contentType
1973
+ });
1974
+ if (requestBody === false) return null;
1975
+ const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
1976
+ const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
1977
+ if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
1978
+ type: "string",
1979
+ contentMediaType: "application/octet-stream"
1980
+ };
1981
+ if (!schema) return null;
1982
+ return dereferenceWithRef(document, schema);
1983
+ }
1984
+ /**
1985
+ * Returns all request body content type keys for an operation, resolving a `$ref` requestBody
1986
+ * through `refs`.
1987
+ *
1988
+ * @example
1989
+ * ```ts
1990
+ * getRequestBodyContentTypes(operation, refs)
1991
+ * // ['application/json', 'multipart/form-data']
1992
+ * ```
1993
+ */
1994
+ function getRequestBodyContentTypes(operation, refs) {
1995
+ const body = getRequestBody({
1996
+ operation,
1997
+ refs
1998
+ });
1999
+ return body?.content ? Object.keys(body.content) : [];
2000
+ }
2001
+ /**
2002
+ * Returns all response content type keys for an operation at a given status code, resolving the
2003
+ * response `$ref` through `refs`.
2004
+ *
2005
+ * @example
2006
+ * ```ts
2007
+ * getResponseBodyContentTypes(operation, refs, 200)
2008
+ * // ['application/json', 'application/xml']
2009
+ * ```
2010
+ */
2011
+ function getResponseBodyContentTypes(operation, refs, statusCode) {
2012
+ const responseObj = getResponseByStatusCode({
2013
+ operation,
2014
+ refs,
2015
+ statusCode
2016
+ });
2017
+ if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
2018
+ const body = responseObj;
2019
+ return body.content ? Object.keys(body.content) : [];
2020
+ }
2021
+ //#endregion
2022
+ //#region src/parser.ts
2023
+ /**
2024
+ * Creates the schema and operation converters bound to one OpenAPI document.
2025
+ *
2026
+ * Takes the `$ref` service for this document (shared with the rest of the pipeline, see
2027
+ * `adapter.ts`) and owns the `parseSchema` recursion seam, then dispatches each schema through
2028
+ * the ordered `schemaRules` table from `emit/parseSchema.ts`. Every converter is a standalone
2029
+ * function that recurses through the `parse` function passed to it, so this file only wires
2030
+ * state to the converters.
2031
+ *
2032
+ * @internal
2033
+ */
2034
+ function createSchemaParser(ctx) {
2035
+ const document = ctx.document;
2036
+ const refs = ctx.refs;
1877
2037
  /**
1878
2038
  * Converts an OAS `SchemaObject` into a `SchemaNode`.
1879
2039
  *
@@ -1902,15 +2062,10 @@ function createSchemaParser(ctx) {
1902
2062
  options,
1903
2063
  parse: parseSchema,
1904
2064
  document,
1905
- resolveRefNode,
1906
- refExists,
2065
+ refs,
1907
2066
  renames: ctx.renames
1908
2067
  };
1909
- for (const rule of schemaRules) {
1910
- if (!rule.match(context)) continue;
1911
- const node = rule.convert(context);
1912
- if (node) return node;
1913
- }
2068
+ for (const rule of schemaRules) if (rule.match(context)) return rule.convert(context);
1914
2069
  const emptyType = options.emptySchemaType;
1915
2070
  return _kubb_ast.ast.factory.createSchema({
1916
2071
  type: emptyType,
@@ -1947,10 +2102,14 @@ function createSchemaParser(ctx) {
1947
2102
  }
1948
2103
  /**
1949
2104
  * Reads the inline `requestBody` metadata (description / required) that OAS exposes
1950
- * outside the schema itself. Returns an empty object when the request body is missing or a `$ref`.
2105
+ * outside the schema itself, resolving a `$ref` requestBody through `refs`. Returns an
2106
+ * empty object when the request body is missing or cannot be resolved.
1951
2107
  */
1952
2108
  function getRequestBodyMeta(operation) {
1953
- const body = operation.schema.requestBody;
2109
+ const body = getRequestBody({
2110
+ operation,
2111
+ refs
2112
+ });
1954
2113
  if (!body) return { required: false };
1955
2114
  return {
1956
2115
  description: body.description,
@@ -1976,12 +2135,20 @@ function createSchemaParser(ctx) {
1976
2135
  function parseOperation(options, operation) {
1977
2136
  const operationId = getOperationId(operation);
1978
2137
  const operationName = operationId ? pascalCase(operationId) : void 0;
1979
- const parameters = getParameters(document, operation).map((param) => parseParameter(options, param, operationName));
1980
- const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
2138
+ const parameters = getParameters({
2139
+ document,
2140
+ operation
2141
+ }).map((param) => parseParameter(options, param, operationName));
2142
+ const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(operation, refs);
1981
2143
  const requestBodyMeta = getRequestBodyMeta(operation);
1982
2144
  const requestBodyName = operationName ? `${operationName}Request` : void 0;
1983
2145
  const content = allContentTypes.flatMap((ct) => {
1984
- const schema = getRequestSchema(document, operation, { contentType: ct });
2146
+ const schema = getRequestSchema({
2147
+ document,
2148
+ operation,
2149
+ refs,
2150
+ options: { contentType: ct }
2151
+ });
1985
2152
  if (!schema) return [];
1986
2153
  return [_kubb_ast.ast.factory.createContent({
1987
2154
  contentType: ct,
@@ -1999,14 +2166,20 @@ function createSchemaParser(ctx) {
1999
2166
  } : void 0;
2000
2167
  const responses = getResponseStatusCodes(operation).map((statusCode) => {
2001
2168
  const responseObj = getResponseByStatusCode({
2002
- document,
2003
2169
  operation,
2170
+ refs,
2004
2171
  statusCode
2005
2172
  });
2006
2173
  const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
2007
2174
  const description = typeof responseObj === "object" && responseObj !== null ? responseObj.description : void 0;
2008
2175
  const parseEntrySchema = (contentType) => {
2009
- const raw = getResponseSchema(document, operation, statusCode, { contentType });
2176
+ const raw = getResponseSchema({
2177
+ document,
2178
+ operation,
2179
+ refs,
2180
+ statusCode,
2181
+ options: { contentType }
2182
+ });
2010
2183
  return {
2011
2184
  schema: raw && Object.keys(raw).length > 0 ? parseSchema({
2012
2185
  schema: raw,
@@ -2015,14 +2188,14 @@ function createSchemaParser(ctx) {
2015
2188
  keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
2016
2189
  };
2017
2190
  };
2018
- const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)).map((contentType) => _kubb_ast.ast.factory.createContent({
2191
+ const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(operation, refs, statusCode)).map((contentType) => _kubb_ast.ast.factory.createContent({
2019
2192
  contentType,
2020
2193
  ...parseEntrySchema(contentType)
2021
2194
  }));
2022
2195
  if (content.length === 0) content.push(_kubb_ast.ast.factory.createContent({
2023
2196
  contentType: getRequestContentType({
2024
- document,
2025
- operation
2197
+ operation,
2198
+ refs
2026
2199
  }) || "application/json",
2027
2200
  ...parseEntrySchema(ctx.contentType)
2028
2201
  }));
@@ -2032,12 +2205,10 @@ function createSchemaParser(ctx) {
2032
2205
  content
2033
2206
  });
2034
2207
  });
2035
- const pathItem = document.paths?.[operation.path];
2036
- const pathItemDoc = pathItem && !isReference(pathItem) ? pathItem : void 0;
2037
2208
  const pickDoc = (key) => {
2038
2209
  const own = operation.schema[key];
2039
2210
  if (typeof own === "string") return own;
2040
- const fallback = pathItemDoc?.[key];
2211
+ const fallback = operation.pathItem[key];
2041
2212
  return typeof fallback === "string" ? fallback : void 0;
2042
2213
  };
2043
2214
  return _kubb_ast.ast.factory.createOperation({
@@ -2206,40 +2377,8 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2206
2377
  enumSuffix
2207
2378
  };
2208
2379
  let parsedDocument = null;
2209
- const documentCache = /* @__PURE__ */ new WeakMap();
2210
- const schemasCache = /* @__PURE__ */ new WeakMap();
2211
- const schemaParserCache = /* @__PURE__ */ new WeakMap();
2212
- function ensureDocument(source) {
2213
- const cached = documentCache.get(source);
2214
- if (cached) return cached;
2215
- const promise = (async () => {
2216
- const fresh = await parseFromConfig(source);
2217
- if (validate) await validateDocument(fresh);
2218
- parsedDocument = fresh;
2219
- return fresh;
2220
- })();
2221
- documentCache.set(source, promise);
2222
- return promise;
2223
- }
2224
- function ensureSchemas(document) {
2225
- const cached = schemasCache.get(document);
2226
- if (cached) return cached;
2227
- const result = getSchemas(document, { contentType });
2228
- schemasCache.set(document, result);
2229
- return result;
2230
- }
2231
- function ensureSchemaParser({ document, renames }) {
2232
- const cached = schemaParserCache.get(document);
2233
- if (cached) return cached;
2234
- const parser = createSchemaParser({
2235
- document,
2236
- contentType,
2237
- renames
2238
- });
2239
- schemaParserCache.set(document, parser);
2240
- return parser;
2241
- }
2242
- function parseInput({ document, schemas, parser }) {
2380
+ const inputCache = /* @__PURE__ */ new WeakMap();
2381
+ function parseInput({ document, refs, schemas, parser }) {
2243
2382
  const { parseSchema, parseOperation } = parser;
2244
2383
  const parsedByName = /* @__PURE__ */ new Map();
2245
2384
  const refAliasMap = /* @__PURE__ */ new Map();
@@ -2262,7 +2401,7 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2262
2401
  const circularNames = [...(0, _kubb_ast.findCircularSchemas)([...parsedByName.values()])];
2263
2402
  const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
2264
2403
  const operationNodes = [];
2265
- for (const operation of getOperations(document)) {
2404
+ for (const operation of getOperations(document, refs)) {
2266
2405
  const operationNode = parseOperation(parserOptions, operation);
2267
2406
  if (operationNode) operationNodes.push(operationNode);
2268
2407
  }
@@ -2327,16 +2466,28 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
2327
2466
  await validateDocument(await parseDocument(input), options);
2328
2467
  },
2329
2468
  async parse(source) {
2330
- const document = await ensureDocument(source);
2331
- const { schemas, renames } = ensureSchemas(document);
2332
- return parseInput({
2333
- document,
2334
- schemas,
2335
- parser: ensureSchemaParser({
2469
+ const cached = inputCache.get(source);
2470
+ if (cached) return cached;
2471
+ const promise = (async () => {
2472
+ const document = await parseFromConfig(source);
2473
+ if (validate) await validateDocument(document);
2474
+ parsedDocument = document;
2475
+ const refs = createRefs(document);
2476
+ const { schemas, renames } = getSchemas(document, { contentType }, refs);
2477
+ return parseInput({
2336
2478
  document,
2337
- renames
2338
- })
2339
- });
2479
+ refs,
2480
+ schemas,
2481
+ parser: createSchemaParser({
2482
+ document,
2483
+ refs,
2484
+ contentType,
2485
+ renames
2486
+ })
2487
+ });
2488
+ })();
2489
+ inputCache.set(source, promise);
2490
+ return promise;
2340
2491
  }
2341
2492
  };
2342
2493
  });