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