@kubb/adapter-oas 5.0.0-beta.5 → 5.0.0-beta.51

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,10 +1,10 @@
1
- import "./chunk--u3MIqq1.js";
2
- import { ast, createAdapter } from "@kubb/core";
1
+ import "./chunk-C0LytTxp.js";
2
+ import { Diagnostics, ast, createAdapter } from "@kubb/core";
3
+ import BaseOas from "oas";
3
4
  import path from "node:path";
4
- import { bundle, loadConfig } from "@redocly/openapi-core";
5
+ import { access } from "node:fs/promises";
5
6
  import OASNormalize from "oas-normalize";
6
- import swagger2openapi from "swagger2openapi";
7
- import BaseOas from "oas";
7
+ import { childName, enumPropName, extractRefName, findDiscriminator } from "@kubb/ast/utils";
8
8
  import { isRef } from "oas/types";
9
9
  import { matchesMimeType } from "oas/utils";
10
10
  //#region src/constants.ts
@@ -89,6 +89,18 @@ const structuralKeys = new Set([
89
89
  * formatMap['float'] // 'number'
90
90
  * ```
91
91
  */
92
+ /**
93
+ * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
94
+ * `int64` and the date/time family. Keep this in sync with the `convertFormat`
95
+ * special-cases in `parser.ts`. `isHandledFormat` reads it so the
96
+ * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
97
+ */
98
+ const specialCasedFormats = new Set([
99
+ "int64",
100
+ "date-time",
101
+ "date",
102
+ "time"
103
+ ]);
92
104
  const formatMap = {
93
105
  uuid: "uuid",
94
106
  email: "email",
@@ -127,87 +139,6 @@ const typeOptionMap = new Map([
127
139
  ["void", ast.schemaTypes.void]
128
140
  ]);
129
141
  //#endregion
130
- //#region src/discriminator.ts
131
- /**
132
- * Injects discriminator enum values into child schemas so they know which value identifies them.
133
- *
134
- * Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
135
- * enum value each union member is mapped to, then adds (or replaces) that property on the matching
136
- * child object schema.
137
- *
138
- * Returns a new `InputNode` — the original is never mutated.
139
- *
140
- * @example
141
- * ```ts
142
- * const { root } = parseOas(document, options)
143
- * const next = applyDiscriminatorInheritance(root)
144
- * ```
145
- */
146
- function applyDiscriminatorInheritance(root) {
147
- const childMap = /* @__PURE__ */ new Map();
148
- for (const schema of root.schemas) {
149
- let unionNode = ast.narrowSchema(schema, "union");
150
- if (!unionNode) {
151
- const intersectionMembers = ast.narrowSchema(schema, "intersection")?.members;
152
- if (intersectionMembers) for (const m of intersectionMembers) {
153
- const u = ast.narrowSchema(m, "union");
154
- if (u) {
155
- unionNode = u;
156
- break;
157
- }
158
- }
159
- }
160
- if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue;
161
- const { discriminatorPropertyName, members } = unionNode;
162
- for (const member of members) {
163
- const intersectionNode = ast.narrowSchema(member, "intersection");
164
- if (!intersectionNode?.members) continue;
165
- let refNode;
166
- let objNode;
167
- for (const m of intersectionNode.members) {
168
- refNode ??= ast.narrowSchema(m, "ref");
169
- objNode ??= ast.narrowSchema(m, "object");
170
- }
171
- if (!refNode?.name || !objNode) continue;
172
- const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName);
173
- const enumNode = prop ? ast.narrowSchema(prop.schema, "enum") : void 0;
174
- if (!enumNode?.enumValues?.length) continue;
175
- const enumValues = enumNode.enumValues.filter((v) => v !== null);
176
- if (!enumValues.length) continue;
177
- const existing = childMap.get(refNode.name);
178
- if (existing) existing.enumValues.push(...enumValues);
179
- else childMap.set(refNode.name, {
180
- propertyName: discriminatorPropertyName,
181
- enumValues: [...enumValues]
182
- });
183
- }
184
- }
185
- if (childMap.size === 0) return root;
186
- return ast.transform(root, { schema(node, { parent }) {
187
- if (parent?.kind !== "Input" || !node.name) return;
188
- const entry = childMap.get(node.name);
189
- if (!entry) return;
190
- const objectNode = ast.narrowSchema(node, "object");
191
- if (!objectNode) return;
192
- const { propertyName, enumValues } = entry;
193
- const enumSchema = ast.createSchema({
194
- type: "enum",
195
- enumValues
196
- });
197
- const newProp = ast.createProperty({
198
- name: propertyName,
199
- required: true,
200
- schema: enumSchema
201
- });
202
- const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
203
- const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
204
- return {
205
- ...objectNode,
206
- properties: newProperties
207
- };
208
- } });
209
- }
210
- //#endregion
211
142
  //#region ../../internals/utils/src/casing.ts
212
143
  /**
213
144
  * Shared implementation for camelCase and PascalCase conversion.
@@ -272,6 +203,42 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
272
203
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
273
204
  }
274
205
  //#endregion
206
+ //#region ../../internals/utils/src/runtime.ts
207
+ /**
208
+ * Returns `true` when the current process is running under Bun.
209
+ *
210
+ * Detection keys off the global `Bun` object rather than `process.versions`,
211
+ * because Bun polyfills `process.versions.node` for Node compatibility and would
212
+ * otherwise look like Node.
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * if (isBun()) {
217
+ * await Bun.write(path, data)
218
+ * }
219
+ * ```
220
+ */
221
+ function isBun() {
222
+ return typeof Bun !== "undefined";
223
+ }
224
+ //#endregion
225
+ //#region ../../internals/utils/src/fs.ts
226
+ /**
227
+ * Resolves to `true` when the file or directory at `path` exists.
228
+ * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * if (await exists('./kubb.config.ts')) {
233
+ * const content = await read('./kubb.config.ts')
234
+ * }
235
+ * ```
236
+ */
237
+ async function exists(path) {
238
+ if (isBun()) return Bun.file(path).exists();
239
+ return access(path).then(() => true, () => false);
240
+ }
241
+ //#endregion
275
242
  //#region ../../internals/utils/src/object.ts
276
243
  /**
277
244
  * Returns `true` when `value` is a plain (non-null, non-array) object.
@@ -406,6 +373,22 @@ const reservedWords = new Set([
406
373
  */
407
374
  function isValidVarName(name) {
408
375
  if (!name || reservedWords.has(name)) return false;
376
+ return isIdentifier(name);
377
+ }
378
+ /**
379
+ * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
380
+ *
381
+ * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
382
+ * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
383
+ * deciding whether an object key needs quoting.
384
+ *
385
+ * @example
386
+ * ```ts
387
+ * isIdentifier('name') // true
388
+ * isIdentifier('x-total')// false
389
+ * ```
390
+ */
391
+ function isIdentifier(name) {
409
392
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
410
393
  }
411
394
  //#endregion
@@ -517,12 +500,13 @@ var URLPath = class {
517
500
  * @example
518
501
  * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
519
502
  */
520
- toTemplateString({ prefix = "", replacer } = {}) {
521
- return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
503
+ toTemplateString({ prefix, replacer } = {}) {
504
+ const result = this.path.split(/\{([^}]+)\}/).map((part, i) => {
522
505
  if (i % 2 === 0) return part;
523
506
  const param = this.#transformParam(part);
524
507
  return `\${${replacer ? replacer(param) : param}}`;
525
- }).join("")}\``;
508
+ }).join("");
509
+ return `\`${prefix ?? ""}${result}\``;
526
510
  }
527
511
  /**
528
512
  * Extracts all `{param}` segments from the path and returns them as a key-value map.
@@ -620,8 +604,8 @@ function isDiscriminator(obj) {
620
604
  * Loads and dereferences an OpenAPI document, returning the raw `Document`.
621
605
  *
622
606
  * Accepts a file path string or an already-parsed document object. File paths are bundled via
623
- * Redocly to resolve external `$ref`s. Swagger 2.0 documents are automatically up-converted
624
- * to OpenAPI 3.0 via `swagger2openapi`.
607
+ * `@apidevtools/json-schema-ref-parser` to resolve external `$ref`s. Swagger 2.0 documents are
608
+ * automatically up-converted to OpenAPI 3.0 via `swagger2openapi`.
625
609
  *
626
610
  * @example
627
611
  * ```ts
@@ -630,19 +614,19 @@ function isDiscriminator(obj) {
630
614
  * ```
631
615
  */
632
616
  async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true } = {}) {
633
- if (typeof pathOrApi === "string" && canBundle) return parseDocument((await bundle({
634
- ref: pathOrApi,
635
- config: await loadConfig(),
636
- base: pathOrApi
637
- })).bundle.parsed, {
638
- canBundle,
639
- enablePaths
640
- });
617
+ if (typeof pathOrApi === "string" && canBundle) {
618
+ const { $RefParser } = await import("@apidevtools/json-schema-ref-parser");
619
+ return parseDocument(await $RefParser.bundle(pathOrApi), {
620
+ canBundle: false,
621
+ enablePaths
622
+ });
623
+ }
641
624
  const document = await new OASNormalize(pathOrApi, {
642
625
  enablePaths,
643
626
  colorizeErrors: true
644
627
  }).load();
645
628
  if (isOpenApiV2Document(document)) {
629
+ const { default: swagger2openapi } = await import("swagger2openapi");
646
630
  const { openapi } = await swagger2openapi.convertObj(document, { anchors: true });
647
631
  return openapi;
648
632
  }
@@ -660,12 +644,17 @@ async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true }
660
644
  * ```
661
645
  */
662
646
  async function mergeDocuments(pathOrApi) {
663
- const documents = [];
664
- for (const p of pathOrApi) documents.push(await parseDocument(p, {
647
+ const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, {
665
648
  enablePaths: false,
666
649
  canBundle: false
667
- }));
668
- if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
650
+ })));
651
+ if (documents.length === 0) throw new Diagnostics.Error({
652
+ code: Diagnostics.code.inputRequired,
653
+ severity: "error",
654
+ message: "No OAS documents were provided for merging.",
655
+ help: "Pass at least one path or document to `input.path`.",
656
+ location: { kind: "config" }
657
+ });
669
658
  const seed = {
670
659
  openapi: MERGE_OPENAPI_VERSION,
671
660
  info: {
@@ -681,9 +670,9 @@ async function mergeDocuments(pathOrApi) {
681
670
  * Creates a `Document` from an `AdapterSource`.
682
671
  *
683
672
  * Handles all three source types:
684
- * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
685
- * - `{ type: 'paths' }` merges multiple file paths into a single document.
686
- * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
673
+ * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
674
+ * - `{ type: 'paths' }` merges multiple file paths into a single document.
675
+ * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
687
676
  *
688
677
  * @example
689
678
  * ```ts
@@ -691,14 +680,31 @@ async function mergeDocuments(pathOrApi) {
691
680
  * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
692
681
  * ```
693
682
  */
694
- function parseFromConfig(source) {
683
+ async function parseFromConfig(source) {
695
684
  if (source.type === "data") {
696
685
  if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
697
686
  return parseDocument(source.data, { canBundle: false });
698
687
  }
699
688
  if (source.type === "paths") return mergeDocuments(source.paths);
700
689
  if (new URLPath(source.path).isURL) return parseDocument(source.path);
701
- return parseDocument(path.resolve(path.dirname(source.path), source.path));
690
+ const resolved = path.resolve(path.dirname(source.path), source.path);
691
+ await assertInputExists(resolved);
692
+ return parseDocument(resolved);
693
+ }
694
+ /**
695
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
696
+ * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
697
+ * its parse error instead.
698
+ */
699
+ async function assertInputExists(input) {
700
+ if (new URLPath(input).isURL) return;
701
+ if (!await exists(input)) throw new Diagnostics.Error({
702
+ code: Diagnostics.code.inputNotFound,
703
+ severity: "error",
704
+ message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
705
+ help: "Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.",
706
+ location: { kind: "config" }
707
+ });
702
708
  }
703
709
  /**
704
710
  * Validates an OpenAPI document using `oas-normalize` with colorized error output.
@@ -720,6 +726,7 @@ async function validateDocument(document, { throwOnError = false } = {}) {
720
726
  }
721
727
  //#endregion
722
728
  //#region src/refs.ts
729
+ const _refCache = /* @__PURE__ */ new WeakMap();
723
730
  /**
724
731
  * Resolves a local JSON pointer reference from a document.
725
732
  *
@@ -735,10 +742,31 @@ function resolveRef(document, $ref) {
735
742
  const origRef = $ref;
736
743
  $ref = $ref.trim();
737
744
  if ($ref === "") return null;
738
- if ($ref.startsWith("#")) $ref = globalThis.decodeURIComponent($ref.substring(1));
739
- else return null;
745
+ if (!$ref.startsWith("#")) return null;
746
+ $ref = globalThis.decodeURIComponent($ref.substring(1));
747
+ let docCache = _refCache.get(document);
748
+ if (!docCache) {
749
+ docCache = /* @__PURE__ */ new Map();
750
+ _refCache.set(document, docCache);
751
+ }
752
+ if (docCache.has($ref)) return docCache.get($ref);
740
753
  const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
741
- if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
754
+ if (!current) {
755
+ const diagnostic = {
756
+ code: Diagnostics.code.refNotFound,
757
+ severity: "error",
758
+ message: `Could not find a definition for ${origRef}.`,
759
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
760
+ location: {
761
+ kind: "schema",
762
+ pointer: origRef,
763
+ ref: origRef
764
+ }
765
+ };
766
+ if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
767
+ return null;
768
+ }
769
+ docCache.set($ref, current);
742
770
  return current;
743
771
  }
744
772
  /**
@@ -762,6 +790,35 @@ function dereferenceWithRef(document, schema) {
762
790
  return schema;
763
791
  }
764
792
  //#endregion
793
+ //#region src/dialect.ts
794
+ /**
795
+ * The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
796
+ *
797
+ * Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
798
+ * decisions that differ between specs (nullability, `$ref`, discriminator, binary,
799
+ * ref resolution) so the converter pipeline and dispatch rules stay shared. A
800
+ * future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
801
+ * nullability, no discriminator object, binary via `contentEncoding` and reuses
802
+ * the rest unchanged.
803
+ *
804
+ * Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
805
+ * JSON Schema vocabulary, so the converters keep that common case.
806
+ *
807
+ * @example
808
+ * ```ts
809
+ * const parser = createSchemaParser(context) // uses oasDialect
810
+ * const parser = createSchemaParser(context, oasDialect) // explicit
811
+ * ```
812
+ */
813
+ const oasDialect = ast.defineSchemaDialect({
814
+ name: "oas",
815
+ isNullable,
816
+ isReference,
817
+ isDiscriminator,
818
+ isBinary: (schema) => schema.type === "string" && schema.contentMediaType === "application/octet-stream",
819
+ resolveRef
820
+ });
821
+ //#endregion
765
822
  //#region src/resolvers.ts
766
823
  /**
767
824
  * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
@@ -783,7 +840,16 @@ function resolveServerUrl(server, overrides) {
783
840
  for (const [key, variable] of Object.entries(server.variables)) {
784
841
  const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
785
842
  if (value === void 0) continue;
786
- if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Error(`Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`);
843
+ if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Diagnostics.Error({
844
+ code: Diagnostics.code.invalidServerVariable,
845
+ severity: "error",
846
+ message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
847
+ help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
848
+ location: {
849
+ kind: "document",
850
+ pointer: "#/servers"
851
+ }
852
+ });
787
853
  url = url.replaceAll(`{${key}}`, value);
788
854
  }
789
855
  return url;
@@ -796,6 +862,15 @@ function getSchemaType(format) {
796
862
  return formatMap[format] ?? null;
797
863
  }
798
864
  /**
865
+ * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
866
+ * `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
867
+ * base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
868
+ * diagnostic in step with the parser as `formatMap` grows.
869
+ */
870
+ function isHandledFormat(format) {
871
+ return getSchemaType(format) !== null || specialCasedFormats.has(format);
872
+ }
873
+ /**
799
874
  * Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
800
875
  * Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
801
876
  */
@@ -805,12 +880,6 @@ function getPrimitiveType(type) {
805
880
  return "string";
806
881
  }
807
882
  /**
808
- * Narrows a content-type string to the `MediaType` union Kubb recognizes, or returns `null`.
809
- */
810
- function getMediaType(contentType) {
811
- return Object.values(ast.mediaTypes).includes(contentType) ? contentType : null;
812
- }
813
- /**
814
883
  * Returns all parameters for an operation, merging path-level and operation-level entries.
815
884
  * Operation-level parameters override path-level ones with the same `in:name` key.
816
885
  * `$ref` parameters resolve via `dereferenceWithRef` for backward compatibility.
@@ -898,7 +967,7 @@ function getRequestSchema(document, operation, options = {}) {
898
967
  /**
899
968
  * Flattens a keyword-only `allOf` into its parent schema.
900
969
  *
901
- * Only flattens when every member is a plain fragment no `$ref` and no structural keywords
970
+ * Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
902
971
  * (see `structuralKeys`). Outer schema values take precedence over fragment values.
903
972
  * Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
904
973
  *
@@ -908,7 +977,7 @@ function getRequestSchema(document, operation, options = {}) {
908
977
  * // { type: 'object', properties: {}, description: 'A pet' }
909
978
  *
910
979
  * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
911
- * // returned unchanged contains a $ref
980
+ * // returned unchanged, contains a $ref
912
981
  * ```
913
982
  */
914
983
  /**
@@ -934,7 +1003,7 @@ function flattenSchema(schema) {
934
1003
  /**
935
1004
  * Extracts the inline schema from a media-type `content` map.
936
1005
  *
937
- * Prefers `preferredContentType` when given; otherwise uses the first key in the map.
1006
+ * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
938
1007
  * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
939
1008
  *
940
1009
  * @example
@@ -953,21 +1022,22 @@ function extractSchemaFromContent(content, preferredContentType) {
953
1022
  /**
954
1023
  * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
955
1024
  */
956
- function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
1025
+ function* collectRefs(schema) {
957
1026
  if (Array.isArray(schema)) {
958
- for (const item of schema) collectRefs(item, refs);
959
- return refs;
1027
+ for (const item of schema) yield* collectRefs(item);
1028
+ return;
960
1029
  }
961
1030
  if (schema && typeof schema === "object") for (const key in schema) {
962
1031
  const value = schema[key];
963
- if (key === "$ref" && typeof value === "string") {
964
- if (value.startsWith("#/components/schemas/")) {
965
- const name = value.slice(21);
966
- if (name) refs.add(name);
967
- }
968
- } else collectRefs(value, refs);
1032
+ if (!(key === "$ref" && typeof value === "string")) {
1033
+ yield* collectRefs(value);
1034
+ continue;
1035
+ }
1036
+ if (value.startsWith("#/components/schemas/")) {
1037
+ const name = value.slice(21);
1038
+ if (name) yield name;
1039
+ }
969
1040
  }
970
- return refs;
971
1041
  }
972
1042
  /**
973
1043
  * Returns a copy of `schemas` topologically sorted by `$ref` dependency.
@@ -983,7 +1053,7 @@ function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
983
1053
  */
984
1054
  function sortSchemas(schemas) {
985
1055
  const deps = /* @__PURE__ */ new Map();
986
- for (const [name, schema] of Object.entries(schemas)) deps.set(name, Array.from(collectRefs(schema)));
1056
+ for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
987
1057
  const sorted = [];
988
1058
  const visited = /* @__PURE__ */ new Set();
989
1059
  function visit(name, stack) {
@@ -1118,15 +1188,16 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
1118
1188
  readOnly: schema.readOnly,
1119
1189
  writeOnly: schema.writeOnly,
1120
1190
  default: defaultValue,
1121
- example: schema.example
1191
+ example: schema.example,
1192
+ format: schema.format
1122
1193
  };
1123
1194
  }
1124
1195
  /**
1125
1196
  * Returns all request body content type keys for an operation.
1126
1197
  *
1127
- * The requestBody is dereferenced **in-place** when it is a `$ref` the same mutation
1128
- * that `getRequestSchema` already performs so that the returned list accurately reflects
1129
- * the available content types even for referenced bodies.
1198
+ * The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
1199
+ * `getRequestSchema` already performs), so the returned list accurately reflects the
1200
+ * available content types even for referenced bodies.
1130
1201
  *
1131
1202
  * @example
1132
1203
  * ```ts
@@ -1140,6 +1211,31 @@ function getRequestBodyContentTypes(document, operation) {
1140
1211
  if (!body) return [];
1141
1212
  return body.content ? Object.keys(body.content) : [];
1142
1213
  }
1214
+ /**
1215
+ * Returns all response content type keys for an operation at a given status code.
1216
+ *
1217
+ * Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
1218
+ * so the returned list reflects the available content types even for referenced responses.
1219
+ *
1220
+ * @example
1221
+ * ```ts
1222
+ * getResponseBodyContentTypes(document, operation, 200)
1223
+ * // ['application/json', 'application/xml']
1224
+ * ```
1225
+ */
1226
+ function getResponseBodyContentTypes(document, operation, statusCode) {
1227
+ if (operation.schema.responses) {
1228
+ const responses = operation.schema.responses;
1229
+ for (const key in responses) {
1230
+ const schema = responses[key];
1231
+ if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
1232
+ }
1233
+ }
1234
+ const responseObj = operation.getResponseByStatusCode(statusCode);
1235
+ if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
1236
+ const body = responseObj;
1237
+ return body.content ? Object.keys(body.content) : [];
1238
+ }
1143
1239
  //#endregion
1144
1240
  //#region src/parser.ts
1145
1241
  /**
@@ -1168,9 +1264,9 @@ function normalizeArrayEnum(schema) {
1168
1264
  * Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
1169
1265
  * made possible by hoisting of function declarations.
1170
1266
  *
1171
- * @note Not exported; called internally by `parseOas()` and `parseSchema()`.
1267
+ * @internal
1172
1268
  */
1173
- function createSchemaParser(ctx) {
1269
+ function createSchemaParser(ctx, dialect = oasDialect) {
1174
1270
  const document = ctx.document;
1175
1271
  /**
1176
1272
  * Tracks `$ref` paths that are currently being resolved to prevent infinite
@@ -1178,6 +1274,15 @@ function createSchemaParser(ctx) {
1178
1274
  */
1179
1275
  const resolvingRefs = /* @__PURE__ */ new Set();
1180
1276
  /**
1277
+ * Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
1278
+ *
1279
+ * Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
1280
+ * it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
1281
+ * since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
1282
+ * Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
1283
+ */
1284
+ const resolvedRefCache = /* @__PURE__ */ new Map();
1285
+ /**
1181
1286
  * Converts a `$ref` schema into a `RefSchemaNode`.
1182
1287
  *
1183
1288
  * The resolved schema is stored in `node.schema`. Usage-site sibling fields
@@ -1186,20 +1291,26 @@ function createSchemaParser(ctx) {
1186
1291
  * Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
1187
1292
  */
1188
1293
  function convertRef({ schema, name, nullable, defaultValue, rawOptions }) {
1189
- let resolvedSchema;
1294
+ let resolvedSchema = null;
1190
1295
  const refPath = schema.$ref;
1191
- if (refPath && !resolvingRefs.has(refPath)) try {
1192
- const referenced = resolveRef(document, refPath);
1193
- if (referenced) {
1194
- resolvingRefs.add(refPath);
1195
- resolvedSchema = parseSchema({ schema: referenced }, rawOptions);
1196
- resolvingRefs.delete(refPath);
1296
+ if (refPath && !resolvingRefs.has(refPath)) {
1297
+ if (!resolvedRefCache.has(refPath)) {
1298
+ try {
1299
+ const referenced = dialect.resolveRef(document, refPath);
1300
+ if (referenced) {
1301
+ resolvingRefs.add(refPath);
1302
+ resolvedSchema = parseSchema({ schema: referenced }, rawOptions);
1303
+ resolvingRefs.delete(refPath);
1304
+ }
1305
+ } catch {}
1306
+ resolvedRefCache.set(refPath, resolvedSchema);
1197
1307
  }
1198
- } catch {}
1308
+ resolvedSchema = resolvedRefCache.get(refPath) ?? null;
1309
+ }
1199
1310
  return ast.createSchema({
1200
1311
  ...buildSchemaNode(schema, name, nullable, defaultValue),
1201
1312
  type: "ref",
1202
- name: ast.extractRefName(schema.$ref),
1313
+ name: extractRefName(schema.$ref),
1203
1314
  ref: schema.$ref,
1204
1315
  schema: resolvedSchema
1205
1316
  });
@@ -1212,7 +1323,7 @@ function createSchemaParser(ctx) {
1212
1323
  const [memberSchema] = schema.allOf;
1213
1324
  const memberNode = parseSchema({
1214
1325
  schema: memberSchema,
1215
- name: null
1326
+ name
1216
1327
  }, rawOptions);
1217
1328
  const { kind: _kind, ...memberNodeProps } = memberNode;
1218
1329
  const mergedNullable = nullable || memberNode.nullable || void 0;
@@ -1228,21 +1339,22 @@ function createSchemaParser(ctx) {
1228
1339
  writeOnly: schema.writeOnly ?? memberNode.writeOnly,
1229
1340
  default: mergedDefault,
1230
1341
  example: schema.example ?? memberNode.example,
1231
- pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0)
1342
+ pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0),
1343
+ format: schema.format ?? memberNode.format
1232
1344
  });
1233
1345
  }
1234
1346
  const filteredDiscriminantValues = [];
1235
1347
  const allOfMembers = schema.allOf.filter((item) => {
1236
- if (!isReference(item) || !name) return true;
1237
- const deref = resolveRef(document, item.$ref);
1238
- if (!deref || !isDiscriminator(deref)) return true;
1348
+ if (!dialect.isReference(item) || !name) return true;
1349
+ const deref = dialect.resolveRef(document, item.$ref);
1350
+ if (!deref || !dialect.isDiscriminator(deref)) return true;
1239
1351
  const parentUnion = deref.oneOf ?? deref.anyOf;
1240
1352
  if (!parentUnion) return true;
1241
1353
  const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1242
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1354
+ const inOneOf = parentUnion.some((oneOfItem) => dialect.isReference(oneOfItem) && oneOfItem.$ref === childRef);
1243
1355
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1244
1356
  if (inOneOf || inMapping) {
1245
- const discriminatorValue = ast.findDiscriminator(deref.discriminator.mapping, childRef);
1357
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
1246
1358
  if (discriminatorValue) filteredDiscriminantValues.push({
1247
1359
  propertyName: deref.discriminator.propertyName,
1248
1360
  value: discriminatorValue
@@ -1250,22 +1362,28 @@ function createSchemaParser(ctx) {
1250
1362
  return false;
1251
1363
  }
1252
1364
  return true;
1253
- }).map((s) => parseSchema({ schema: s }, rawOptions));
1365
+ }).map((s) => parseSchema({
1366
+ schema: s,
1367
+ name
1368
+ }, rawOptions));
1254
1369
  const syntheticStart = allOfMembers.length;
1255
1370
  if (Array.isArray(schema.required) && schema.required.length) {
1256
1371
  const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
1257
1372
  const missingRequired = schema.required.filter((key) => !outerKeys.has(key));
1258
1373
  if (missingRequired.length) {
1259
1374
  const resolvedMembers = schema.allOf.flatMap((item) => {
1260
- if (!isReference(item)) return [item];
1261
- const deref = resolveRef(document, item.$ref);
1262
- return deref && !isReference(deref) ? [deref] : [];
1375
+ if (!dialect.isReference(item)) return [item];
1376
+ const deref = dialect.resolveRef(document, item.$ref);
1377
+ return deref && !dialect.isReference(deref) ? [deref] : [];
1263
1378
  });
1264
1379
  for (const key of missingRequired) for (const resolved of resolvedMembers) if (resolved.properties?.[key]) {
1265
- allOfMembers.push(parseSchema({ schema: {
1266
- properties: { [key]: resolved.properties[key] },
1267
- required: [key]
1268
- } }, rawOptions));
1380
+ allOfMembers.push(parseSchema({
1381
+ schema: {
1382
+ properties: { [key]: resolved.properties[key] },
1383
+ required: [key]
1384
+ },
1385
+ name
1386
+ }, rawOptions));
1269
1387
  break;
1270
1388
  }
1271
1389
  }
@@ -1280,7 +1398,7 @@ function createSchemaParser(ctx) {
1280
1398
  }));
1281
1399
  return ast.createSchema({
1282
1400
  type: "intersection",
1283
- members: [...ast.mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
1401
+ members: [...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
1284
1402
  ...buildSchemaNode(schema, name, nullable, defaultValue)
1285
1403
  });
1286
1404
  }
@@ -1301,10 +1419,10 @@ function createSchemaParser(ctx) {
1301
1419
  const strategy = schema.oneOf ? "one" : "any";
1302
1420
  const unionBase = {
1303
1421
  ...buildSchemaNode(schema, name, nullable, defaultValue),
1304
- discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1422
+ discriminatorPropertyName: dialect.isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1305
1423
  strategy
1306
1424
  };
1307
- const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
1425
+ const discriminator = dialect.isDiscriminator(schema) ? schema.discriminator : void 0;
1308
1426
  const sharedPropertiesNode = schema.properties ? (() => {
1309
1427
  const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema;
1310
1428
  return parseSchema({
@@ -1314,9 +1432,12 @@ function createSchemaParser(ctx) {
1314
1432
  })() : void 0;
1315
1433
  if (sharedPropertiesNode || discriminator?.mapping) {
1316
1434
  const members = unionMembers.map((s) => {
1317
- const ref = isReference(s) ? s.$ref : void 0;
1318
- const discriminatorValue = ast.findDiscriminator(discriminator?.mapping, ref);
1319
- const memberNode = parseSchema({ schema: s }, rawOptions);
1435
+ const ref = dialect.isReference(s) ? s.$ref : void 0;
1436
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref);
1437
+ const memberNode = parseSchema({
1438
+ schema: s,
1439
+ name
1440
+ }, rawOptions);
1320
1441
  if (!discriminatorValue || !discriminator) return memberNode;
1321
1442
  const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.setDiscriminatorEnum({
1322
1443
  node: sharedPropertiesNode,
@@ -1346,7 +1467,10 @@ function createSchemaParser(ctx) {
1346
1467
  return ast.createSchema({
1347
1468
  type: "union",
1348
1469
  ...unionBase,
1349
- members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s }, rawOptions)))
1470
+ members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({
1471
+ schema: s,
1472
+ name
1473
+ }, rawOptions)))
1350
1474
  });
1351
1475
  }
1352
1476
  /**
@@ -1360,7 +1484,8 @@ function createSchemaParser(ctx) {
1360
1484
  name,
1361
1485
  title: schema.title,
1362
1486
  description: schema.description,
1363
- deprecated: schema.deprecated
1487
+ deprecated: schema.deprecated,
1488
+ format: schema.format
1364
1489
  });
1365
1490
  const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
1366
1491
  return ast.createSchema({
@@ -1450,6 +1575,15 @@ function createSchemaParser(ctx) {
1450
1575
  }, rawOptions);
1451
1576
  const nullInEnum = schema.enum.includes(null);
1452
1577
  const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
1578
+ if (nullInEnum && filteredValues.length === 0) return ast.createSchema({
1579
+ type: "null",
1580
+ primitive: "null",
1581
+ name,
1582
+ title: schema.title,
1583
+ description: schema.description,
1584
+ deprecated: schema.deprecated,
1585
+ format: schema.format
1586
+ });
1453
1587
  const enumNullable = nullable || nullInEnum || void 0;
1454
1588
  const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1455
1589
  const enumPrimitive = getPrimitiveType(type);
@@ -1464,7 +1598,8 @@ function createSchemaParser(ctx) {
1464
1598
  readOnly: schema.readOnly,
1465
1599
  writeOnly: schema.writeOnly,
1466
1600
  default: enumDefault,
1467
- example: schema.example
1601
+ example: schema.example,
1602
+ format: schema.format
1468
1603
  };
1469
1604
  const extensionKey = enumExtensionKeys.find((key) => key in schema);
1470
1605
  if (extensionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
@@ -1498,20 +1633,23 @@ function createSchemaParser(ctx) {
1498
1633
  const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
1499
1634
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
1500
1635
  const resolvedPropSchema = propSchema;
1501
- const propNullable = isNullable(resolvedPropSchema);
1636
+ const propNullable = dialect.isNullable(resolvedPropSchema);
1502
1637
  const propNode = parseSchema({
1503
1638
  schema: resolvedPropSchema,
1504
- name: ast.childName(name, propName)
1639
+ name: childName(name, propName)
1505
1640
  }, rawOptions);
1506
- let schemaNode = ast.setEnumName(propNode, name, propName, options.enumSuffix);
1507
- const tupleNode = ast.narrowSchema(schemaNode, "tuple");
1508
- if (tupleNode?.items) {
1509
- const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix));
1510
- if (namedItems.some((item, i) => item !== tupleNode.items[i])) schemaNode = {
1511
- ...tupleNode,
1512
- items: namedItems
1513
- };
1514
- }
1641
+ const schemaNode = (() => {
1642
+ const node = ast.setEnumName(propNode, name, propName, options.enumSuffix);
1643
+ const tupleNode = ast.narrowSchema(node, "tuple");
1644
+ if (tupleNode?.items) {
1645
+ const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix));
1646
+ if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
1647
+ ...tupleNode,
1648
+ items: namedItems
1649
+ };
1650
+ }
1651
+ return node;
1652
+ })();
1515
1653
  return ast.createProperty({
1516
1654
  name: propName,
1517
1655
  schema: {
@@ -1522,11 +1660,12 @@ function createSchemaParser(ctx) {
1522
1660
  });
1523
1661
  }) : [];
1524
1662
  const additionalProperties = schema.additionalProperties;
1525
- let additionalPropertiesNode;
1526
- if (additionalProperties === true) additionalPropertiesNode = true;
1527
- else if (additionalProperties && Object.keys(additionalProperties).length > 0) additionalPropertiesNode = parseSchema({ schema: additionalProperties }, rawOptions);
1528
- else if (additionalProperties === false) additionalPropertiesNode = false;
1529
- else if (additionalProperties) additionalPropertiesNode = ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1663
+ const additionalPropertiesNode = (() => {
1664
+ if (additionalProperties === true) return true;
1665
+ if (additionalProperties === false) return false;
1666
+ if (additionalProperties && Object.keys(additionalProperties).length > 0) return parseSchema({ schema: additionalProperties }, rawOptions);
1667
+ if (additionalProperties) return ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1668
+ })();
1530
1669
  const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
1531
1670
  const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? ast.createSchema({ type: typeOptionMap.get(options.unknownType) }) : parseSchema({ schema: patternSchema }, rawOptions)])) : void 0;
1532
1671
  const objectNode = ast.createSchema({
@@ -1539,10 +1678,10 @@ function createSchemaParser(ctx) {
1539
1678
  maxProperties: schema.maxProperties,
1540
1679
  ...buildSchemaNode(schema, name, nullable, defaultValue)
1541
1680
  });
1542
- if (isDiscriminator(schema) && schema.discriminator.mapping) {
1681
+ if (dialect.isDiscriminator(schema) && schema.discriminator.mapping) {
1543
1682
  const discPropName = schema.discriminator.propertyName;
1544
1683
  const values = Object.keys(schema.discriminator.mapping);
1545
- const enumName = name ? ast.enumPropName(name, discPropName, options.enumSuffix) : void 0;
1684
+ const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
1546
1685
  return ast.setDiscriminatorEnum({
1547
1686
  node: objectNode,
1548
1687
  propertyName: discPropName,
@@ -1573,7 +1712,7 @@ function createSchemaParser(ctx) {
1573
1712
  */
1574
1713
  function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }) {
1575
1714
  const rawItems = schema.items;
1576
- const itemName = rawItems?.enum?.length && name ? ast.enumPropName(void 0, name, options.enumSuffix) : void 0;
1715
+ const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name;
1577
1716
  const items = rawItems ? [parseSchema({
1578
1717
  schema: rawItems,
1579
1718
  name: itemName
@@ -1637,15 +1776,148 @@ function createSchemaParser(ctx) {
1637
1776
  title: schema.title,
1638
1777
  description: schema.description,
1639
1778
  deprecated: schema.deprecated,
1640
- nullable
1779
+ nullable,
1780
+ format: schema.format
1781
+ });
1782
+ }
1783
+ /**
1784
+ * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
1785
+ * into a `blob` node.
1786
+ */
1787
+ function convertBlob({ schema, name, nullable, defaultValue }) {
1788
+ return ast.createSchema({
1789
+ type: "blob",
1790
+ primitive: "string",
1791
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1641
1792
  });
1642
1793
  }
1643
1794
  /**
1795
+ * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
1796
+ *
1797
+ * Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parseSchema`
1798
+ * falls through and handles it as that single type with nullability already folded in.
1799
+ */
1800
+ function convertMultiType({ schema, name, nullable, defaultValue, rawOptions }) {
1801
+ const types = schema.type;
1802
+ const nonNullTypes = types.filter((t) => t !== "null");
1803
+ if (nonNullTypes.length <= 1) return null;
1804
+ const arrayNullable = types.includes("null") || nullable || void 0;
1805
+ return ast.createSchema({
1806
+ type: "union",
1807
+ members: nonNullTypes.map((t) => parseSchema({
1808
+ schema: {
1809
+ ...schema,
1810
+ type: t
1811
+ },
1812
+ name
1813
+ }, rawOptions)),
1814
+ ...buildSchemaNode(schema, name, arrayNullable, defaultValue)
1815
+ });
1816
+ }
1817
+ /**
1818
+ * Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
1819
+ * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
1820
+ * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
1821
+ * match/convert/fall-through contract.
1822
+ */
1823
+ const schemaRules = [
1824
+ {
1825
+ name: "ref",
1826
+ match: ({ schema }) => dialect.isReference(schema),
1827
+ convert: convertRef
1828
+ },
1829
+ {
1830
+ name: "allOf",
1831
+ match: ({ schema }) => !!schema.allOf?.length,
1832
+ convert: convertAllOf
1833
+ },
1834
+ {
1835
+ name: "union",
1836
+ match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length),
1837
+ convert: convertUnion
1838
+ },
1839
+ {
1840
+ name: "const",
1841
+ match: ({ schema }) => "const" in schema && schema.const !== void 0,
1842
+ convert: convertConst
1843
+ },
1844
+ {
1845
+ name: "format",
1846
+ match: ({ schema }) => !!schema.format,
1847
+ convert: convertFormat
1848
+ },
1849
+ {
1850
+ name: "blob",
1851
+ match: ({ schema }) => dialect.isBinary(schema),
1852
+ convert: convertBlob
1853
+ },
1854
+ {
1855
+ name: "multi-type",
1856
+ match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
1857
+ convert: convertMultiType
1858
+ },
1859
+ {
1860
+ name: "constrained-string",
1861
+ match: ({ schema, type }) => !type && (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0),
1862
+ convert: convertString
1863
+ },
1864
+ {
1865
+ name: "constrained-number",
1866
+ match: ({ schema, type }) => !type && (schema.minimum !== void 0 || schema.maximum !== void 0),
1867
+ convert: (ctx) => convertNumeric(ctx, "number")
1868
+ },
1869
+ {
1870
+ name: "enum",
1871
+ match: ({ schema }) => !!schema.enum?.length,
1872
+ convert: convertEnum
1873
+ },
1874
+ {
1875
+ name: "object",
1876
+ match: ({ schema, type }) => type === "object" || !!schema.properties || !!schema.additionalProperties || "patternProperties" in schema,
1877
+ convert: convertObject
1878
+ },
1879
+ {
1880
+ name: "tuple",
1881
+ match: ({ schema }) => "prefixItems" in schema,
1882
+ convert: convertTuple
1883
+ },
1884
+ {
1885
+ name: "array",
1886
+ match: ({ schema, type }) => type === "array" || "items" in schema,
1887
+ convert: convertArray
1888
+ },
1889
+ {
1890
+ name: "string",
1891
+ match: ({ type }) => type === "string",
1892
+ convert: convertString
1893
+ },
1894
+ {
1895
+ name: "number",
1896
+ match: ({ type }) => type === "number",
1897
+ convert: (ctx) => convertNumeric(ctx, "number")
1898
+ },
1899
+ {
1900
+ name: "integer",
1901
+ match: ({ type }) => type === "integer",
1902
+ convert: (ctx) => convertNumeric(ctx, "integer")
1903
+ },
1904
+ {
1905
+ name: "boolean",
1906
+ match: ({ type }) => type === "boolean",
1907
+ convert: convertBoolean
1908
+ },
1909
+ {
1910
+ name: "null",
1911
+ match: ({ type }) => type === "null",
1912
+ convert: convertNull
1913
+ }
1914
+ ];
1915
+ /**
1644
1916
  * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
1645
1917
  *
1646
- * Dispatch order (first match wins): `$ref` `allOf` `oneOf`/`anyOf` `const` → `format`
1647
- * octet-stream blob multi-type array constraint-inferred type `enum` object/array/tuple/scalar
1648
- * → empty-schema fallback (`emptySchemaType` option).
1918
+ * Builds the per-schema context, then runs it through the ordered {@link schemaRules} table
1919
+ * via {@link ast.dispatch}. When no rule produces a node, falls back to the configured
1920
+ * `emptySchemaType`.
1649
1921
  */
1650
1922
  function parseSchema({ schema, name }, rawOptions) {
1651
1923
  const options = {
@@ -1657,75 +1929,40 @@ function createSchemaParser(ctx) {
1657
1929
  schema: flattenedSchema,
1658
1930
  name
1659
1931
  }, rawOptions);
1660
- const nullable = isNullable(schema) || void 0;
1661
- const defaultValue = schema.default === null && nullable ? void 0 : schema.default;
1662
- const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
1663
- const ctx = {
1932
+ const nullable = dialect.isNullable(schema) || void 0;
1933
+ const schemaCtx = {
1664
1934
  schema,
1665
1935
  name,
1666
1936
  nullable,
1667
- defaultValue,
1668
- type,
1937
+ defaultValue: schema.default === null && nullable ? void 0 : schema.default,
1938
+ type: Array.isArray(schema.type) ? schema.type[0] : schema.type,
1669
1939
  rawOptions,
1670
1940
  options
1671
1941
  };
1672
- if (isReference(schema)) return convertRef(ctx);
1673
- if (schema.allOf?.length) return convertAllOf(ctx);
1674
- if ([...schema.oneOf ?? [], ...schema.anyOf ?? []].length) return convertUnion(ctx);
1675
- if ("const" in schema && schema.const !== void 0) return convertConst(ctx);
1676
- if (schema.format) {
1677
- const formatResult = convertFormat(ctx);
1678
- if (formatResult) return formatResult;
1679
- }
1680
- if (schema.type === "string" && schema.contentMediaType === "application/octet-stream") return ast.createSchema({
1681
- type: "blob",
1682
- primitive: "string",
1683
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1684
- });
1685
- if (Array.isArray(schema.type) && schema.type.length > 1) {
1686
- const nonNullTypes = schema.type.filter((t) => t !== "null");
1687
- const arrayNullable = schema.type.includes("null") || nullable || void 0;
1688
- if (nonNullTypes.length > 1) return ast.createSchema({
1689
- type: "union",
1690
- members: nonNullTypes.map((t) => parseSchema({
1691
- schema: {
1692
- ...schema,
1693
- type: t
1694
- },
1695
- name
1696
- }, rawOptions)),
1697
- ...buildSchemaNode(schema, name, arrayNullable, defaultValue)
1698
- });
1699
- }
1700
- if (!type) {
1701
- if (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0) return convertString(ctx);
1702
- if (schema.minimum !== void 0 || schema.maximum !== void 0) return convertNumeric(ctx, "number");
1703
- }
1704
- if (schema.enum?.length) return convertEnum(ctx);
1705
- if (type === "object" || schema.properties || schema.additionalProperties || "patternProperties" in schema) return convertObject(ctx);
1706
- if ("prefixItems" in schema) return convertTuple(ctx);
1707
- if (type === "array" || "items" in schema) return convertArray(ctx);
1708
- if (type === "string") return convertString(ctx);
1709
- if (type === "number") return convertNumeric(ctx, "number");
1710
- if (type === "integer") return convertNumeric(ctx, "integer");
1711
- if (type === "boolean") return convertBoolean(ctx);
1712
- if (type === "null") return convertNull(ctx);
1942
+ const node = ast.dispatch(schemaRules, schemaCtx);
1943
+ if (node) return node;
1713
1944
  const emptyType = typeOptionMap.get(options.emptySchemaType);
1714
1945
  return ast.createSchema({
1715
1946
  type: emptyType,
1716
1947
  name,
1717
1948
  title: schema.title,
1718
- description: schema.description
1949
+ description: schema.description,
1950
+ format: schema.format
1719
1951
  });
1720
1952
  }
1721
1953
  /**
1722
1954
  * Converts a dereferenced OAS parameter object into a `ParameterNode`.
1723
1955
  */
1724
- function parseParameter(options, param) {
1956
+ function parseParameter(options, param, parentName) {
1725
1957
  const required = param["required"] ?? false;
1726
- const schema = param["schema"] ? parseSchema({ schema: param["schema"] }, options) : ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1958
+ const paramName = param["name"];
1959
+ const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : void 0;
1960
+ const schema = param["schema"] ? parseSchema({
1961
+ schema: param["schema"],
1962
+ name: schemaName
1963
+ }, options) : ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1727
1964
  return ast.createParameter({
1728
- name: param["name"],
1965
+ name: paramName,
1729
1966
  in: param["in"],
1730
1967
  schema: {
1731
1968
  ...schema,
@@ -1762,27 +1999,33 @@ function createSchemaParser(ctx) {
1762
1999
  * `$ref` entries are skipped since their flags live on the dereferenced target.
1763
2000
  */
1764
2001
  function collectPropertyKeysByFlag(schema, flag) {
1765
- if (!schema?.properties) return void 0;
2002
+ if (!schema?.properties) return null;
1766
2003
  const keys = [];
1767
2004
  for (const key in schema.properties) {
1768
2005
  const prop = schema.properties[key];
1769
- if (prop && !isReference(prop) && prop[flag]) keys.push(key);
2006
+ if (prop && !dialect.isReference(prop) && prop[flag]) keys.push(key);
1770
2007
  }
1771
- return keys.length ? keys : void 0;
2008
+ return keys.length ? keys : null;
1772
2009
  }
1773
2010
  /**
1774
2011
  * Converts an OAS `Operation` into an `OperationNode`.
1775
2012
  */
1776
2013
  function parseOperation(options, operation) {
1777
- const parameters = getParameters(document, operation).map((param) => parseParameter(options, param));
2014
+ const operationId = operation.getOperationId();
2015
+ const operationName = operationId ? pascalCase(operationId) : void 0;
2016
+ const parameters = getParameters(document, operation).map((param) => parseParameter(options, param, operationName));
1778
2017
  const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
1779
2018
  const requestBodyMeta = getRequestBodyMeta(operation);
2019
+ const requestBodyName = operationName ? `${operationName}Request` : void 0;
1780
2020
  const content = allContentTypes.flatMap((ct) => {
1781
2021
  const schema = getRequestSchema(document, operation, { contentType: ct });
1782
2022
  if (!schema) return [];
1783
2023
  return [{
1784
2024
  contentType: ct,
1785
- schema: ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
2025
+ schema: ast.syncOptionality(parseSchema({
2026
+ schema,
2027
+ name: requestBodyName
2028
+ }, options), requestBodyMeta.required),
1786
2029
  keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
1787
2030
  }];
1788
2031
  });
@@ -1793,21 +2036,36 @@ function createSchemaParser(ctx) {
1793
2036
  } : void 0;
1794
2037
  const responses = operation.getResponseStatusCodes().map((statusCode) => {
1795
2038
  const responseObj = operation.getResponseByStatusCode(statusCode);
1796
- const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType });
1797
- const schema = responseSchema && Object.keys(responseSchema).length > 0 ? parseSchema({ schema: responseSchema }, options) : ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType) });
1798
- const { description, content } = getResponseMeta(responseObj);
1799
- const mediaType = content ? getMediaType(Object.keys(content)[0] ?? "") : getMediaType(operation.contentType ?? "");
2039
+ const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
2040
+ const { description } = getResponseMeta(responseObj);
2041
+ const parseEntrySchema = (contentType) => {
2042
+ const raw = getResponseSchema(document, operation, statusCode, { contentType });
2043
+ return {
2044
+ schema: raw && Object.keys(raw).length > 0 ? parseSchema({
2045
+ schema: raw,
2046
+ name: responseName
2047
+ }, options) : ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType) }),
2048
+ keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
2049
+ };
2050
+ };
2051
+ const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)).map((contentType) => ({
2052
+ contentType,
2053
+ ...parseEntrySchema(contentType)
2054
+ }));
2055
+ if (content.length === 0) content.push({
2056
+ contentType: operation.contentType || "application/json",
2057
+ ...parseEntrySchema(ctx.contentType)
2058
+ });
1800
2059
  return ast.createResponse({
1801
2060
  statusCode,
1802
2061
  description,
1803
- schema,
1804
- mediaType,
1805
- keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
2062
+ content
1806
2063
  });
1807
2064
  });
1808
2065
  const urlPath = new URLPath(operation.path);
1809
2066
  return ast.createOperation({
1810
- operationId: operation.getOperationId(),
2067
+ operationId,
2068
+ protocol: "http",
1811
2069
  method: operation.method.toUpperCase(),
1812
2070
  path: urlPath.path,
1813
2071
  tags: operation.getTags().map((tag) => tag.name),
@@ -1825,59 +2083,336 @@ function createSchemaParser(ctx) {
1825
2083
  parseParameter
1826
2084
  };
1827
2085
  }
2086
+ //#endregion
2087
+ //#region src/discriminator.ts
2088
+ /**
2089
+ * Builds a map of child schema names → discriminator patch data by scanning the given
2090
+ * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
2091
+ *
2092
+ * The streaming path calls this on a small pre-parsed subset of schemas (only the
2093
+ * discriminator parents) rather than on all schemas at once.
2094
+ */
2095
+ function buildDiscriminatorChildMap(schemas) {
2096
+ const childMap = /* @__PURE__ */ new Map();
2097
+ for (const schema of schemas) {
2098
+ let unionNode = ast.narrowSchema(schema, "union");
2099
+ if (!unionNode) {
2100
+ const intersectionMembers = ast.narrowSchema(schema, "intersection")?.members;
2101
+ if (intersectionMembers) for (const m of intersectionMembers) {
2102
+ const u = ast.narrowSchema(m, "union");
2103
+ if (u) {
2104
+ unionNode = u;
2105
+ break;
2106
+ }
2107
+ }
2108
+ }
2109
+ if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue;
2110
+ const { discriminatorPropertyName, members } = unionNode;
2111
+ for (const member of members) {
2112
+ const intersectionNode = ast.narrowSchema(member, "intersection");
2113
+ if (!intersectionNode?.members) continue;
2114
+ let refNode = null;
2115
+ let objNode = null;
2116
+ for (const m of intersectionNode.members) {
2117
+ refNode ??= ast.narrowSchema(m, "ref");
2118
+ objNode ??= ast.narrowSchema(m, "object");
2119
+ }
2120
+ if (!refNode?.name || !objNode) continue;
2121
+ const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName);
2122
+ const enumNode = prop ? ast.narrowSchema(prop.schema, "enum") : null;
2123
+ if (!enumNode?.enumValues?.length) continue;
2124
+ const enumValues = enumNode.enumValues.filter((v) => v !== null);
2125
+ if (!enumValues.length) continue;
2126
+ const existing = childMap.get(refNode.name);
2127
+ if (!existing) {
2128
+ childMap.set(refNode.name, {
2129
+ propertyName: discriminatorPropertyName,
2130
+ enumValues: [...enumValues]
2131
+ });
2132
+ continue;
2133
+ }
2134
+ existing.enumValues.push(...enumValues);
2135
+ }
2136
+ }
2137
+ return childMap;
2138
+ }
2139
+ /**
2140
+ * Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
2141
+ * the discriminant property). Used by the streaming path to apply patches inline per yield
2142
+ * without buffering all schemas.
2143
+ */
2144
+ function patchDiscriminatorNode(node, entry) {
2145
+ const objectNode = ast.narrowSchema(node, "object");
2146
+ if (!objectNode) return node;
2147
+ const { propertyName, enumValues } = entry;
2148
+ const enumSchema = ast.createSchema({
2149
+ type: "enum",
2150
+ enumValues
2151
+ });
2152
+ const newProp = ast.createProperty({
2153
+ name: propertyName,
2154
+ required: true,
2155
+ schema: enumSchema
2156
+ });
2157
+ const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
2158
+ const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
2159
+ return {
2160
+ ...objectNode,
2161
+ properties: newProperties
2162
+ };
2163
+ }
2164
+ //#endregion
2165
+ //#region src/schemaDiagnostics.ts
2166
+ /**
2167
+ * Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
2168
+ * top-level schema. Walks the node the parser produced during `preScan`, threading the RFC 6901
2169
+ * pointer as it descends so a nested field reports against its full path
2170
+ * (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
2171
+ * resolved schema is reported under its own walk. Reports land in the active build run, are a
2172
+ * no-op outside one, and repeats are deduped by the build.
2173
+ */
2174
+ function reportSchemaDiagnostics({ node, name }) {
2175
+ visit(node, `#/components/schemas/${escapePointerToken(name)}`);
2176
+ }
2177
+ /**
2178
+ * Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
2179
+ * property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
2180
+ */
2181
+ function escapePointerToken(token) {
2182
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
2183
+ }
2184
+ function visit(node, pointer) {
2185
+ if (node.deprecated) Diagnostics.report({
2186
+ code: Diagnostics.code.deprecated,
2187
+ severity: "info",
2188
+ message: "This schema is marked as deprecated.",
2189
+ location: {
2190
+ kind: "schema",
2191
+ pointer
2192
+ }
2193
+ });
2194
+ if (typeof node.format === "string" && !isHandledFormat(node.format)) Diagnostics.report({
2195
+ code: Diagnostics.code.unsupportedFormat,
2196
+ severity: "warning",
2197
+ message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
2198
+ help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
2199
+ location: {
2200
+ kind: "schema",
2201
+ pointer
2202
+ }
2203
+ });
2204
+ if (node.type === "object") {
2205
+ for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
2206
+ if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
2207
+ return;
2208
+ }
2209
+ if (node.type === "array") {
2210
+ for (const item of node.items ?? []) visit(item, `${pointer}/items`);
2211
+ return;
2212
+ }
2213
+ if (node.type === "tuple") {
2214
+ for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
2215
+ return;
2216
+ }
2217
+ if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
2218
+ }
2219
+ //#endregion
2220
+ //#region src/stream.ts
2221
+ /**
2222
+ * Builds the deduplication plan from the already-parsed top-level schema nodes plus a
2223
+ * single extra parse pass over operations (so duplicates in request/response bodies are seen).
2224
+ *
2225
+ * Only enums and objects are candidates, and object shapes that reference a circular schema are
2226
+ * rejected to avoid hoisting recursive structures. Inline shapes reuse their context-derived
2227
+ * name (collision-resolved against existing component names); shapes without a name stay inline.
2228
+ */
2229
+ function createDedupePlan({ schemaNodes, operationNodes, schemaNames, circularNames }) {
2230
+ const circularSchemas = new Set(circularNames);
2231
+ const usedNames = new Set(schemaNames);
2232
+ return ast.buildDedupePlan([...schemaNodes, ...operationNodes], {
2233
+ isCandidate: (node) => {
2234
+ if (node.type === "enum") return true;
2235
+ if (node.type !== "object") return false;
2236
+ if (node.name && circularSchemas.has(node.name)) return false;
2237
+ return !ast.containsCircularRef(node, { circularSchemas });
2238
+ },
2239
+ nameFor: (node) => {
2240
+ const base = node.name;
2241
+ if (!base) return null;
2242
+ let name = base;
2243
+ let counter = 2;
2244
+ while (usedNames.has(name)) name = `${base}${counter++}`;
2245
+ usedNames.add(name);
2246
+ return name;
2247
+ },
2248
+ refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`
2249
+ });
2250
+ }
1828
2251
  /**
1829
- * Parses an OpenAPI specification into Kubb's universal `InputNode` AST.
2252
+ * Reads the server URL from the document's `servers` array at `serverIndex`,
2253
+ * interpolating any `serverVariables` into the URL template.
1830
2254
  *
1831
- * This is the main entry point for `@kubb/adapter-oas`. It converts OpenAPI/Swagger specs into a spec-agnostic tree
1832
- * that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here —
1833
- * the tree is a pure data structure representing all schemas and operations.
2255
+ * Returns `null` when `serverIndex` is omitted or out of range.
1834
2256
  *
1835
- * Returns the AST root and a `nameMapping` for resolving schema references.
2257
+ * @example Resolve the first server
2258
+ * `resolveBaseUrl({ document, serverIndex: 0 })`
2259
+ *
2260
+ * @example Override a path variable
2261
+ * `resolveBaseUrl({ document, serverIndex: 0, serverVariables: { version: 'v2' } })`
2262
+ */
2263
+ function resolveBaseUrl({ document, serverIndex, serverVariables }) {
2264
+ const server = serverIndex !== void 0 ? document.servers?.at(serverIndex) : void 0;
2265
+ return server?.url ? resolveServerUrl(server, serverVariables) : null;
2266
+ }
2267
+ /**
2268
+ * Parses every schema once to build the lookup structures that streaming needs upfront.
2269
+ *
2270
+ * Three things happen in this single pass:
2271
+ * - `refAliasMap` records schemas that are pure `$ref` aliases so the streaming pass can inline them.
2272
+ * - `enumNames` collects the names of every enum schema so plugins skip re-scanning the stream.
2273
+ * - `circularNames` runs cycle detection, which requires all nodes in memory simultaneously.
2274
+ * The `allNodes` array is local and drops out of scope as soon as this function returns.
2275
+ *
2276
+ * After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
2277
+ * Both are proportional to the number of aliases or discriminator parents, not total schema count.
2278
+ *
2279
+ * Each schema is parsed again during the streaming pass. This is intentional.
2280
+ * Holding the parsed nodes in memory here would defeat the streaming memory benefit.
1836
2281
  *
1837
2282
  * @example
1838
2283
  * ```ts
1839
- * import { parseOas } from '@kubb/adapter-oas'
1840
- *
1841
- * const document = await parseFromConfig(config)
1842
- * const { root, nameMapping } = parseOas(document, { dateType: 'date', contentType: 'application/json' })
2284
+ * const { refAliasMap, enumNames, circularNames } = preScan({
2285
+ * schemas,
2286
+ * parseSchema,
2287
+ * parserOptions,
2288
+ * discriminator: 'strict',
2289
+ * })
1843
2290
  * ```
1844
2291
  */
1845
- function parseOas(document, options = {}) {
1846
- const { contentType, ...parserOptions } = options;
1847
- const mergedOptions = {
1848
- ...DEFAULT_PARSER_OPTIONS,
1849
- ...parserOptions
1850
- };
1851
- const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType });
1852
- const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({
1853
- document,
1854
- contentType
1855
- });
1856
- const schemas = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({
1857
- schema,
1858
- name
1859
- }, mergedOptions));
1860
- const paths = new BaseOas(document).getPaths();
1861
- const operations = Object.entries(paths).flatMap(([_path, methods]) => Object.entries(methods).map(([, operation]) => operation ? _parseOperation(mergedOptions, operation) : null).filter((op) => op !== null));
2292
+ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions, discriminator, dedupe }) {
2293
+ const allNodes = [];
2294
+ const refAliasMap = /* @__PURE__ */ new Map();
2295
+ const enumNames = [];
2296
+ const discriminatorParentNodes = [];
2297
+ for (const [name, schema] of Object.entries(schemas)) {
2298
+ const node = parseSchema({
2299
+ schema,
2300
+ name
2301
+ }, parserOptions);
2302
+ allNodes.push(node);
2303
+ reportSchemaDiagnostics({
2304
+ node,
2305
+ name
2306
+ });
2307
+ if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2308
+ if (ast.narrowSchema(node, ast.schemaTypes.enum) && node.name) enumNames.push(node.name);
2309
+ if (discriminator === "inherit" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2310
+ }
2311
+ const circularNames = [...ast.findCircularSchemas(allNodes)];
2312
+ const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
2313
+ let dedupePlan = null;
2314
+ if (dedupe) {
2315
+ const operationNodes = [];
2316
+ for (const methods of Object.values(baseOas.getPaths())) for (const operation of Object.values(methods)) {
2317
+ if (!operation) continue;
2318
+ const operationNode = parseOperation(parserOptions, operation);
2319
+ if (operationNode) operationNodes.push(operationNode);
2320
+ }
2321
+ dedupePlan = createDedupePlan({
2322
+ schemaNodes: allNodes,
2323
+ operationNodes,
2324
+ schemaNames: Object.keys(schemas),
2325
+ circularNames
2326
+ });
2327
+ for (const definition of dedupePlan.hoisted) if (definition.type === "enum" && definition.name) enumNames.push(definition.name);
2328
+ }
1862
2329
  return {
1863
- root: ast.createInput({
1864
- schemas,
1865
- operations
1866
- }),
1867
- nameMapping
2330
+ refAliasMap,
2331
+ enumNames,
2332
+ circularNames,
2333
+ discriminatorChildMap,
2334
+ dedupePlan
1868
2335
  };
1869
2336
  }
2337
+ /**
2338
+ * Creates a lazy `InputStreamNode` from already-resolved adapter state.
2339
+ *
2340
+ * The schema and operation iterables each start a fresh parse pass on every
2341
+ * `[Symbol.asyncIterator]()` call. This lets multiple plugins consume the same
2342
+ * stream object independently without sharing a cursor or holding all nodes in memory.
2343
+ *
2344
+ * Ref aliases in `refAliasMap` are inlined during iteration: an alias entry is replaced
2345
+ * with its target's parsed node (but keeps the alias name) so plugins never receive bare `ref` nodes.
2346
+ *
2347
+ * @example
2348
+ * ```ts
2349
+ * const streamNode = createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, meta })
2350
+ * for await (const schema of streamNode.schemas) {
2351
+ * // each call to for-await restarts from the first schema
2352
+ * }
2353
+ * ```
2354
+ */
2355
+ function createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, dedupePlan, meta }) {
2356
+ const rewriteTopLevelSchema = (node) => {
2357
+ if (!dedupePlan) return node;
2358
+ const canonical = dedupePlan.canonicalBySignature.get(ast.schemaSignature(node));
2359
+ if (canonical && canonical.name !== node.name) return ast.createSchema({
2360
+ type: "ref",
2361
+ name: node.name ?? null,
2362
+ ref: canonical.ref,
2363
+ description: node.description,
2364
+ deprecated: node.deprecated
2365
+ });
2366
+ return ast.applyDedupe(node, dedupePlan.canonicalBySignature, true);
2367
+ };
2368
+ const schemasIterable = { [Symbol.asyncIterator]() {
2369
+ return (async function* () {
2370
+ if (dedupePlan) for (const definition of dedupePlan.hoisted) yield definition;
2371
+ for (const [name, schema] of Object.entries(schemas)) {
2372
+ const alias = refAliasMap.get(name);
2373
+ if (alias?.name && schemas[alias.name]) {
2374
+ yield rewriteTopLevelSchema({
2375
+ ...parseSchema({
2376
+ schema: schemas[alias.name],
2377
+ name: alias.name
2378
+ }, parserOptions),
2379
+ name
2380
+ });
2381
+ continue;
2382
+ }
2383
+ const parsed = parseSchema({
2384
+ schema,
2385
+ name
2386
+ }, parserOptions);
2387
+ yield rewriteTopLevelSchema(discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)) : parsed);
2388
+ }
2389
+ })();
2390
+ } };
2391
+ const operationsIterable = { [Symbol.asyncIterator]() {
2392
+ return (async function* () {
2393
+ for (const methods of Object.values(baseOas.getPaths())) for (const operation of Object.values(methods)) {
2394
+ if (!operation) continue;
2395
+ const node = parseOperation(parserOptions, operation);
2396
+ if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan.canonicalBySignature) : node;
2397
+ }
2398
+ })();
2399
+ } };
2400
+ return ast.createStreamInput(schemasIterable, operationsIterable, meta);
2401
+ }
1870
2402
  //#endregion
1871
2403
  //#region src/adapter.ts
1872
2404
  /**
1873
- * Stable string identifier for the OAS adapter used in Kubb's adapter registry.
2405
+ * Canonical adapter name for `@kubb/adapter-oas`. Used for driver lookups.
1874
2406
  */
1875
2407
  const adapterOasName = "oas";
1876
2408
  /**
1877
- * Creates the default OpenAPI / Swagger adapter for Kubb.
2409
+ * Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.1 specifications. Reads the
2410
+ * file at `input.path`, validates it, resolves the base URL, and converts every
2411
+ * schema and operation into the universal AST that every downstream plugin
2412
+ * consumes.
1878
2413
  *
1879
- * Parses the spec, optionally validates it, resolves the base URL, and converts
1880
- * everything into an `InputNode` that downstream plugins consume.
2414
+ * Configure once on `defineConfig`. The adapter's choices (date representation,
2415
+ * integer width, server URL) apply to every plugin in the build.
1881
2416
  *
1882
2417
  * @example
1883
2418
  * ```ts
@@ -1886,17 +2421,118 @@ const adapterOasName = "oas";
1886
2421
  * import { pluginTs } from '@kubb/plugin-ts'
1887
2422
  *
1888
2423
  * export default defineConfig({
1889
- * adapter: adapterOas({ dateType: 'date', serverIndex: 0 }),
1890
- * input: { path: './openapi.yaml' },
2424
+ * input: { path: './petStore.yaml' },
2425
+ * output: { path: './src/gen' },
2426
+ * adapter: adapterOas({
2427
+ * serverIndex: 0,
2428
+ * discriminator: 'inherit',
2429
+ * dateType: 'date',
2430
+ * }),
1891
2431
  * plugins: [pluginTs()],
1892
2432
  * })
1893
2433
  * ```
1894
2434
  */
1895
2435
  const adapterOas = createAdapter((options) => {
1896
- const { validate = true, contentType, serverIndex, serverVariables, discriminator = "strict", dateType = DEFAULT_PARSER_OPTIONS.dateType, integerType = DEFAULT_PARSER_OPTIONS.integerType, unknownType = DEFAULT_PARSER_OPTIONS.unknownType, enumSuffix = DEFAULT_PARSER_OPTIONS.enumSuffix, emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType } = options;
2436
+ const { validate = true, contentType, serverIndex, serverVariables, discriminator = "strict", dedupe = true, dateType = DEFAULT_PARSER_OPTIONS.dateType, integerType = DEFAULT_PARSER_OPTIONS.integerType, unknownType = DEFAULT_PARSER_OPTIONS.unknownType, enumSuffix = DEFAULT_PARSER_OPTIONS.enumSuffix, emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType } = options;
2437
+ const parserOptions = {
2438
+ ...DEFAULT_PARSER_OPTIONS,
2439
+ dateType,
2440
+ integerType,
2441
+ unknownType,
2442
+ emptySchemaType,
2443
+ enumSuffix
2444
+ };
1897
2445
  let nameMapping = /* @__PURE__ */ new Map();
1898
- let parsedDocument;
1899
- let inputNode;
2446
+ let parsedDocument = null;
2447
+ const documentCache = /* @__PURE__ */ new WeakMap();
2448
+ const schemasCache = /* @__PURE__ */ new WeakMap();
2449
+ const baseOasCache = /* @__PURE__ */ new WeakMap();
2450
+ const schemaParserCache = /* @__PURE__ */ new WeakMap();
2451
+ const preScanCache = /* @__PURE__ */ new WeakMap();
2452
+ function ensureDocument(source) {
2453
+ const cached = documentCache.get(source);
2454
+ if (cached) return cached;
2455
+ const promise = (async () => {
2456
+ const fresh = await parseFromConfig(source);
2457
+ if (validate) await validateDocument(fresh);
2458
+ parsedDocument = fresh;
2459
+ return fresh;
2460
+ })();
2461
+ documentCache.set(source, promise);
2462
+ return promise;
2463
+ }
2464
+ function ensureSchemas(document) {
2465
+ const cached = schemasCache.get(document);
2466
+ if (cached) return cached;
2467
+ const promise = Promise.resolve().then(() => {
2468
+ const result = getSchemas(document, { contentType });
2469
+ nameMapping = result.nameMapping;
2470
+ return result.schemas;
2471
+ });
2472
+ schemasCache.set(document, promise);
2473
+ return promise;
2474
+ }
2475
+ function ensureBaseOas(document) {
2476
+ const cached = baseOasCache.get(document);
2477
+ if (cached) return cached;
2478
+ const baseOas = new BaseOas(document);
2479
+ baseOasCache.set(document, baseOas);
2480
+ return baseOas;
2481
+ }
2482
+ function ensureSchemaParser(document) {
2483
+ const cached = schemaParserCache.get(document);
2484
+ if (cached) return cached;
2485
+ const parser = createSchemaParser({
2486
+ document,
2487
+ contentType
2488
+ });
2489
+ schemaParserCache.set(document, parser);
2490
+ return parser;
2491
+ }
2492
+ function ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas) {
2493
+ const cached = preScanCache.get(document);
2494
+ if (cached) return cached;
2495
+ const result = preScan({
2496
+ schemas,
2497
+ parseSchema,
2498
+ parseOperation,
2499
+ baseOas,
2500
+ parserOptions,
2501
+ discriminator,
2502
+ dedupe
2503
+ });
2504
+ preScanCache.set(document, result);
2505
+ return result;
2506
+ }
2507
+ async function createStream(source) {
2508
+ const document = await ensureDocument(source);
2509
+ const schemas = await ensureSchemas(document);
2510
+ const { parseSchema, parseOperation } = ensureSchemaParser(document);
2511
+ const baseOas = ensureBaseOas(document);
2512
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas);
2513
+ return createInputStream({
2514
+ schemas,
2515
+ parseSchema,
2516
+ parseOperation,
2517
+ baseOas,
2518
+ parserOptions,
2519
+ refAliasMap,
2520
+ discriminatorChildMap,
2521
+ dedupePlan,
2522
+ meta: {
2523
+ title: document.info?.title,
2524
+ description: document.info?.description,
2525
+ version: document.info?.version,
2526
+ baseURL: resolveBaseUrl({
2527
+ document,
2528
+ serverIndex,
2529
+ serverVariables
2530
+ }),
2531
+ circularNames,
2532
+ enumNames
2533
+ }
2534
+ });
2535
+ }
1900
2536
  return {
1901
2537
  name: "oas",
1902
2538
  get options() {
@@ -1906,6 +2542,7 @@ const adapterOas = createAdapter((options) => {
1906
2542
  serverIndex,
1907
2543
  serverVariables,
1908
2544
  discriminator,
2545
+ dedupe,
1909
2546
  dateType,
1910
2547
  integerType,
1911
2548
  unknownType,
@@ -1917,8 +2554,9 @@ const adapterOas = createAdapter((options) => {
1917
2554
  get document() {
1918
2555
  return parsedDocument;
1919
2556
  },
1920
- get inputNode() {
1921
- return inputNode;
2557
+ async validate(input, options) {
2558
+ await assertInputExists(input);
2559
+ await validateDocument(await parseDocument(input), options);
1922
2560
  },
1923
2561
  getImports(node, resolve) {
1924
2562
  return ast.collectImports({
@@ -1926,7 +2564,7 @@ const adapterOas = createAdapter((options) => {
1926
2564
  nameMapping,
1927
2565
  resolve: (schemaName) => {
1928
2566
  const result = resolve(schemaName);
1929
- if (!result) return;
2567
+ if (!result) return null;
1930
2568
  return ast.createImport({
1931
2569
  name: [result.name],
1932
2570
  path: result.path
@@ -1935,47 +2573,18 @@ const adapterOas = createAdapter((options) => {
1935
2573
  });
1936
2574
  },
1937
2575
  async parse(source) {
1938
- const document = await parseFromConfig(source);
1939
- if (validate) await validateDocument(document);
1940
- const server = serverIndex !== void 0 ? document.servers?.at(serverIndex) : void 0;
1941
- const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : void 0;
1942
- const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
1943
- contentType,
1944
- dateType,
1945
- integerType,
1946
- unknownType,
1947
- emptySchemaType,
1948
- enumSuffix
2576
+ const streamNode = await createStream(source);
2577
+ const [schemas, operations] = await Promise.all([Array.fromAsync(streamNode.schemas), Array.fromAsync(streamNode.operations)]);
2578
+ return ast.createInput({
2579
+ schemas,
2580
+ operations,
2581
+ meta: streamNode.meta
1949
2582
  });
1950
- const node = discriminator === "inherit" ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot;
1951
- nameMapping = parsedNameMapping;
1952
- parsedDocument = document;
1953
- inputNode = ast.createInput({
1954
- ...node,
1955
- meta: {
1956
- title: document.info?.title,
1957
- description: document.info?.description,
1958
- version: document.info?.version,
1959
- baseURL
1960
- }
1961
- });
1962
- return inputNode;
1963
- }
2583
+ },
2584
+ stream: createStream
1964
2585
  };
1965
2586
  });
1966
2587
  //#endregion
1967
- //#region src/types.ts
1968
- /**
1969
- * Maps uppercase HTTP method names to lowercase for backwards compatibility.
1970
- *
1971
- * @example
1972
- * ```ts
1973
- * HttpMethods['GET'] // 'get'
1974
- * HttpMethods['POST'] // 'post'
1975
- * ```
1976
- */
1977
- const HttpMethods = Object.fromEntries(Object.entries(ast.httpMethods).map(([lower, upper]) => [upper, lower]));
1978
- //#endregion
1979
- export { HttpMethods, adapterOas, adapterOasName, mergeDocuments, parseDocument, parseFromConfig, validateDocument };
2588
+ export { adapterOas, adapterOasName };
1980
2589
 
1981
2590
  //# sourceMappingURL=index.js.map