@kubb/adapter-oas 5.0.0-beta.99 → 5.0.0

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