@kubb/adapter-oas 5.0.0-beta.6 → 5.0.0-beta.61

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,13 @@
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
3
  import path from "node:path";
4
- import { bundle, loadConfig } from "@redocly/openapi-core";
5
- import OASNormalize from "oas-normalize";
6
- import swagger2openapi from "swagger2openapi";
7
- import BaseOas from "oas";
8
- import { isRef } from "oas/types";
9
- import { matchesMimeType } from "oas/utils";
4
+ import { access, readFile } from "node:fs/promises";
5
+ import { compileErrors, validate } from "@readme/openapi-parser";
6
+ import { parse } from "yaml";
7
+ import { bundle } from "api-ref-bundler";
8
+ import { macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion } from "@kubb/ast/macros";
9
+ import { childName, containsCircularRef, enumPropName, extractRefName, findCircularSchemas, mergeAdjacentObjectsLazy } from "@kubb/ast/utils";
10
+ import { collect, narrowSchema } from "@kubb/ast";
10
11
  //#region src/constants.ts
11
12
  /**
12
13
  * Default parser options applied when no explicit options are provided.
@@ -38,6 +39,20 @@ const DEFAULT_PARSER_OPTIONS = {
38
39
  */
39
40
  const SCHEMA_REF_PREFIX = "#/components/schemas/";
40
41
  /**
42
+ * HTTP methods that count as operations on an OpenAPI path item. Other keys
43
+ * (`parameters`, `summary`, `$ref`, vendor extensions) are skipped when iterating operations.
44
+ */
45
+ const SUPPORTED_METHODS = new Set([
46
+ "get",
47
+ "put",
48
+ "post",
49
+ "delete",
50
+ "options",
51
+ "head",
52
+ "patch",
53
+ "trace"
54
+ ]);
55
+ /**
41
56
  * OpenAPI version string written into the stub document created during multi-spec merges.
42
57
  */
43
58
  const MERGE_OPENAPI_VERSION = "3.0.0";
@@ -77,7 +92,7 @@ const structuralKeys = new Set([
77
92
  *
78
93
  * Only formats whose AST type differs from the OAS `type` field appear here.
79
94
  * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
80
- * in the parser. `ipv4` and `ipv6` map to their own dedicated schema types; `hostname` and
95
+ * in the parser. `ipv4` and `ipv6` map to their own dedicated schema types. `hostname` and
81
96
  * `idn-hostname` map to `'url'` as the closest generic string-format type.
82
97
  *
83
98
  * @example
@@ -89,6 +104,18 @@ const structuralKeys = new Set([
89
104
  * formatMap['float'] // 'number'
90
105
  * ```
91
106
  */
107
+ /**
108
+ * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
109
+ * `int64` and the date/time family. Keep this in sync with the `convertFormat`
110
+ * special-cases in `parser.ts`. `isHandledFormat` reads it so the
111
+ * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
112
+ */
113
+ const specialCasedFormats = new Set([
114
+ "int64",
115
+ "date-time",
116
+ "date",
117
+ "time"
118
+ ]);
92
119
  const formatMap = {
93
120
  uuid: "uuid",
94
121
  email: "email",
@@ -118,8 +145,8 @@ const formatMap = {
118
145
  */
119
146
  const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
120
147
  /**
121
- * Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
122
- * Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
148
+ * Maps the `unknownType`/`emptySchemaType` option string to its `ScalarSchemaType` constant.
149
+ * A `Map` (over a plain object) lets callers test key membership with `.has()`.
123
150
  */
124
151
  const typeOptionMap = new Map([
125
152
  ["any", ast.schemaTypes.any],
@@ -127,87 +154,6 @@ const typeOptionMap = new Map([
127
154
  ["void", ast.schemaTypes.void]
128
155
  ]);
129
156
  //#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
157
  //#region ../../internals/utils/src/casing.ts
212
158
  /**
213
159
  * Shared implementation for camelCase and PascalCase conversion.
@@ -219,57 +165,132 @@ function applyDiscriminatorInheritance(root) {
219
165
  function toCamelOrPascal(text, pascal) {
220
166
  return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
221
167
  if (word.length > 1 && word === word.toUpperCase()) return word;
222
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
223
- return word.charAt(0).toUpperCase() + word.slice(1);
168
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
224
169
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
225
170
  }
226
171
  /**
227
- * Splits `text` on `.` and applies `transformPart` to each segment.
228
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
229
- * Segments are joined with `/` to form a file path.
172
+ * Converts `text` to camelCase.
230
173
  *
231
- * Only splits on dots followed by a letter so that version numbers
232
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
174
+ * @example Word boundaries
175
+ * `camelCase('hello-world') // 'helloWorld'`
233
176
  *
234
- * Empty segments are filtered before joining. They arise when the text starts with
235
- * a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`
236
- * and `'..'` transforms to an empty string). Without this filter the join would produce
237
- * a leading `/`, which `path.resolve` would interpret as an absolute path, allowing
238
- * generated files to escape the configured output directory.
177
+ * @example With a prefix
178
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
239
179
  */
240
- function applyToFileParts(text, transformPart) {
241
- const parts = text.split(/\.(?=[a-zA-Z])/);
242
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/");
180
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
181
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
243
182
  }
244
183
  /**
245
- * Converts `text` to camelCase.
246
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
184
+ * Converts `text` to PascalCase.
185
+ *
186
+ * @example Word boundaries
187
+ * `pascalCase('hello-world') // 'HelloWorld'`
188
+ *
189
+ * @example With a suffix
190
+ * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
191
+ */
192
+ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
193
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
194
+ }
195
+ //#endregion
196
+ //#region ../../internals/utils/src/runtime.ts
197
+ /**
198
+ * Detects the JavaScript runtime executing the current process and exposes its name and version.
199
+ *
200
+ * Prefer the shared {@link runtime} instance over constructing your own.
201
+ */
202
+ var Runtime = class {
203
+ /**
204
+ * `true` when the current process is running under Bun.
205
+ *
206
+ * Detection keys off the global `Bun` object rather than `process.versions`,
207
+ * because Bun polyfills `process.versions.node` for Node compatibility and would
208
+ * otherwise look like Node.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * if (runtime.isBun) {
213
+ * await Bun.write(path, data)
214
+ * }
215
+ * ```
216
+ */
217
+ get isBun() {
218
+ return typeof Bun !== "undefined";
219
+ }
220
+ /**
221
+ * `true` when the current process is running under Deno.
222
+ */
223
+ get isDeno() {
224
+ return typeof globalThis.Deno !== "undefined";
225
+ }
226
+ /**
227
+ * `true` when the current process is running under Node.
228
+ *
229
+ * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
230
+ */
231
+ get isNode() {
232
+ return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
233
+ }
234
+ /**
235
+ * Name of the runtime executing the current process.
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
240
+ * ```
241
+ */
242
+ get name() {
243
+ if (this.isBun) return "bun";
244
+ if (this.isDeno) return "deno";
245
+ return "node";
246
+ }
247
+ /**
248
+ * Version of the active runtime, or an empty string when it cannot be read.
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * runtime.version // '1.3.11' under Bun, '22.22.2' under Node
253
+ * ```
254
+ */
255
+ get version() {
256
+ if (this.isBun) return process.versions.bun ?? "";
257
+ if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
258
+ return process.versions?.node ?? "";
259
+ }
260
+ };
261
+ /**
262
+ * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
263
+ */
264
+ const runtime = new Runtime();
265
+ //#endregion
266
+ //#region ../../internals/utils/src/fs.ts
267
+ /**
268
+ * Resolves to `true` when the file or directory at `path` exists.
269
+ * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
247
270
  *
248
271
  * @example
249
- * camelCase('hello-world') // 'helloWorld'
250
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
251
- */
252
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
253
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
254
- prefix,
255
- suffix
256
- } : {}));
257
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
272
+ * ```ts
273
+ * if (await exists('./kubb.config.ts')) {
274
+ * const content = await read('./kubb.config.ts')
275
+ * }
276
+ * ```
277
+ */
278
+ async function exists(path) {
279
+ if (runtime.isBun) return Bun.file(path).exists();
280
+ return access(path).then(() => true, () => false);
258
281
  }
259
282
  /**
260
- * Converts `text` to PascalCase.
261
- * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
283
+ * Reads the file at `path` as a UTF-8 string.
284
+ * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
262
285
  *
263
286
  * @example
264
- * pascalCase('hello-world') // 'HelloWorld'
265
- * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
266
- */
267
- function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
268
- if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
269
- prefix,
270
- suffix
271
- }) : camelCase(part));
272
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
287
+ * ```ts
288
+ * const source = await read('./src/Pet.ts')
289
+ * ```
290
+ */
291
+ async function read(path) {
292
+ if (runtime.isBun) return Bun.file(path).text();
293
+ return readFile(path, { encoding: "utf8" });
273
294
  }
274
295
  //#endregion
275
296
  //#region ../../internals/utils/src/object.ts
@@ -406,102 +427,99 @@ const reservedWords = new Set([
406
427
  */
407
428
  function isValidVarName(name) {
408
429
  if (!name || reservedWords.has(name)) return false;
409
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
430
+ return isIdentifier(name);
410
431
  }
411
- //#endregion
412
- //#region ../../internals/utils/src/urlPath.ts
413
432
  /**
414
- * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
433
+ * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
434
+ *
435
+ * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
436
+ * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
437
+ * deciding whether an object key needs quoting.
415
438
  *
416
439
  * @example
417
- * const p = new URLPath('/pet/{petId}')
418
- * p.URL // '/pet/:petId'
419
- * p.template // '`/pet/${petId}`'
440
+ * ```ts
441
+ * isIdentifier('name') // true
442
+ * isIdentifier('x-total')// false
443
+ * ```
420
444
  */
421
- var URLPath = class {
422
- /**
423
- * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
424
- */
425
- path;
426
- #options;
427
- constructor(path, options = {}) {
428
- this.path = path;
429
- this.#options = options;
445
+ function isIdentifier(name) {
446
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
447
+ }
448
+ //#endregion
449
+ //#region ../../internals/utils/src/url.ts
450
+ function transformParam(raw, casing) {
451
+ const param = isValidVarName(raw) ? raw : camelCase(raw);
452
+ return casing === "camelcase" ? camelCase(param) : param;
453
+ }
454
+ function toParamsObject(path, { replacer, casing } = {}) {
455
+ const params = {};
456
+ for (const match of path.matchAll(/\{([^}]+)\}/g)) {
457
+ const param = transformParam(match[1], casing);
458
+ const key = replacer ? replacer(param) : param;
459
+ params[key] = key;
430
460
  }
431
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
461
+ return Object.keys(params).length > 0 ? params : null;
462
+ }
463
+ /**
464
+ * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
465
+ */
466
+ var Url = class Url {
467
+ /**
468
+ * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
432
469
  *
433
470
  * @example
434
- * ```ts
435
- * new URLPath('/pet/{petId}').URL // '/pet/:petId'
436
- * ```
471
+ * Url.canParse('https://petstore.swagger.io/v2') // true
472
+ * Url.canParse('/pet/{petId}') // false
437
473
  */
438
- get URL() {
439
- return this.toURLPath();
474
+ static canParse(url, base) {
475
+ return URL.canParse(url, base);
440
476
  }
441
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
477
+ /**
478
+ * Converts an OpenAPI/Swagger path to Express-style colon syntax.
442
479
  *
443
480
  * @example
444
- * ```ts
445
- * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
446
- * new URLPath('/pet/{petId}').isURL // false
447
- * ```
481
+ * Url.toPath('/pet/{petId}') // '/pet/:petId'
448
482
  */
449
- get isURL() {
450
- try {
451
- return !!new URL(this.path).href;
452
- } catch {
453
- return false;
454
- }
483
+ static toPath(path) {
484
+ return path.replace(/\{([^}]+)\}/g, ":$1");
455
485
  }
456
486
  /**
457
- * Converts the OpenAPI path to a TypeScript template literal string.
487
+ * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
488
+ * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
489
+ * and `casing` controls parameter identifier casing.
458
490
  *
459
491
  * @example
460
- * new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
461
- * new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
462
- */
463
- get template() {
464
- return this.toTemplateString();
465
- }
466
- /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
492
+ * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
467
493
  *
468
494
  * @example
469
- * ```ts
470
- * new URLPath('/pet/{petId}').object
471
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
472
- * ```
495
+ * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
473
496
  */
474
- get object() {
475
- return this.toObject();
497
+ static toTemplateString(path, { prefix, replacer, casing } = {}) {
498
+ const result = path.split(/\{([^}]+)\}/).map((part, i) => {
499
+ if (i % 2 === 0) return part;
500
+ const param = transformParam(part, casing);
501
+ return `\${${replacer ? replacer(param) : param}}`;
502
+ }).join("");
503
+ return `\`${prefix ?? ""}${result}\``;
476
504
  }
477
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
505
+ /**
506
+ * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
507
+ * expression when `stringify` is set.
478
508
  *
479
509
  * @example
480
- * ```ts
481
- * new URLPath('/pet/{petId}').params // { petId: 'petId' }
482
- * new URLPath('/pet').params // undefined
483
- * ```
484
- */
485
- get params() {
486
- return this.getParams();
487
- }
488
- #transformParam(raw) {
489
- const param = isValidVarName(raw) ? raw : camelCase(raw);
490
- return this.#options.casing === "camelcase" ? camelCase(param) : param;
491
- }
492
- /**
493
- * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
510
+ * Url.toObject('/pet/{petId}')
511
+ * // { url: '/pet/:petId', params: { petId: 'petId' } }
494
512
  */
495
- #eachParam(fn) {
496
- for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
497
- const raw = match[1];
498
- fn(raw, this.#transformParam(raw));
499
- }
500
- }
501
- toObject({ type = "path", replacer, stringify } = {}) {
513
+ static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
502
514
  const object = {
503
- url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
504
- params: this.getParams()
515
+ url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
516
+ replacer,
517
+ casing
518
+ }),
519
+ params: toParamsObject(path, {
520
+ replacer,
521
+ casing
522
+ })
505
523
  };
506
524
  if (stringify) {
507
525
  if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
@@ -510,51 +528,52 @@ var URLPath = class {
510
528
  }
511
529
  return object;
512
530
  }
513
- /**
514
- * Converts the OpenAPI path to a TypeScript template literal string.
515
- * An optional `replacer` can transform each extracted parameter name before interpolation.
516
- *
517
- * @example
518
- * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
519
- */
520
- toTemplateString({ prefix = "", replacer } = {}) {
521
- return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
522
- if (i % 2 === 0) return part;
523
- const param = this.#transformParam(part);
524
- return `\${${replacer ? replacer(param) : param}}`;
525
- }).join("")}\``;
526
- }
527
- /**
528
- * Extracts all `{param}` segments from the path and returns them as a key-value map.
529
- * An optional `replacer` transforms each parameter name in both key and value positions.
530
- * Returns `undefined` when no path parameters are found.
531
- *
532
- * @example
533
- * ```ts
534
- * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
535
- * // { petId: 'petId', tagId: 'tagId' }
536
- * ```
537
- */
538
- getParams(replacer) {
539
- const params = {};
540
- this.#eachParam((_raw, param) => {
541
- const key = replacer ? replacer(param) : param;
542
- params[key] = key;
543
- });
544
- return Object.keys(params).length > 0 ? params : void 0;
545
- }
546
- /** Converts the OpenAPI path to Express-style colon syntax.
547
- *
548
- * @example
549
- * ```ts
550
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
551
- * ```
552
- */
553
- toURLPath() {
554
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
555
- }
556
531
  };
557
532
  //#endregion
533
+ //#region src/bundler.ts
534
+ const urlRegExp = /^https?:\/+/i;
535
+ async function readSource(sourcePath) {
536
+ if (urlRegExp.test(sourcePath)) {
537
+ const url = new URL(sourcePath);
538
+ const response = await fetch(url);
539
+ if (!response.ok) throw new Error(`Cannot fetch the OAS document at ${url.href} (HTTP ${response.status})`);
540
+ return response.text();
541
+ }
542
+ return read(sourcePath);
543
+ }
544
+ async function resolveSource(sourcePath) {
545
+ const data = await readSource(sourcePath);
546
+ if (sourcePath.toLowerCase().endsWith(".md")) return data;
547
+ return parse(data);
548
+ }
549
+ /**
550
+ * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
551
+ *
552
+ * External file schemas are hoisted into named `components.schemas` entries, so a property
553
+ * pointing at `./schemas/User.yaml` ends up referencing `#/components/schemas/User`. Generators
554
+ * can then emit a named type with an import instead of inlining the shape. Sources are read with
555
+ * the Bun-aware `read` util for local YAML and JSON files, and with `fetch` for HTTP(S) URLs.
556
+ *
557
+ * @example Local file
558
+ * `const document = await bundleDocument('./openapi.yaml')`
559
+ *
560
+ * @example Remote URL
561
+ * `const document = await bundleDocument('https://example.com/openapi.yaml')`
562
+ */
563
+ async function bundleDocument(pathOrUrl) {
564
+ const cache = /* @__PURE__ */ new Map();
565
+ const resolver = (sourcePath) => {
566
+ const key = urlRegExp.test(sourcePath) ? new URL(sourcePath).href : sourcePath;
567
+ const cached = cache.get(key);
568
+ if (cached) return cached;
569
+ const result = resolveSource(sourcePath);
570
+ cache.set(key, result);
571
+ return result;
572
+ };
573
+ await resolver(pathOrUrl);
574
+ return await bundle(pathOrUrl, resolver);
575
+ }
576
+ //#endregion
558
577
  //#region src/guards.ts
559
578
  /**
560
579
  * Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
@@ -619,9 +638,10 @@ function isDiscriminator(obj) {
619
638
  /**
620
639
  * Loads and dereferences an OpenAPI document, returning the raw `Document`.
621
640
  *
622
- * 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`.
641
+ * Accepts a file path string or an already-parsed document object. File paths and URLs are
642
+ * bundled via `api-ref-bundler`, hoisting external file schemas into named `components.schemas`
643
+ * entries so generators can emit named types and imports. Swagger 2.0 documents are
644
+ * automatically up-converted to OpenAPI 3.0 via `swagger2openapi`.
625
645
  *
626
646
  * @example
627
647
  * ```ts
@@ -629,20 +649,11 @@ function isDiscriminator(obj) {
629
649
  * const document = await parse(rawDocumentObject, { canBundle: false })
630
650
  * ```
631
651
  */
632
- 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
- });
641
- const document = await new OASNormalize(pathOrApi, {
642
- enablePaths,
643
- colorizeErrors: true
644
- }).load();
652
+ async function parseDocument(pathOrApi, { canBundle = true } = {}) {
653
+ if (typeof pathOrApi === "string" && canBundle) return parseDocument(await bundleDocument(pathOrApi), { canBundle: false });
654
+ const document = typeof pathOrApi === "string" ? parse(pathOrApi) : pathOrApi;
645
655
  if (isOpenApiV2Document(document)) {
656
+ const { default: swagger2openapi } = await import("swagger2openapi");
646
657
  const { openapi } = await swagger2openapi.convertObj(document, { anchors: true });
647
658
  return openapi;
648
659
  }
@@ -651,7 +662,7 @@ async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true }
651
662
  /**
652
663
  * Deep-merges multiple OpenAPI documents into a single `Document`.
653
664
  *
654
- * Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
665
+ * Each document is parsed independently, then deep-merged into one in array order.
655
666
  * Throws when the input array is empty.
656
667
  *
657
668
  * @example
@@ -660,12 +671,14 @@ async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true }
660
671
  * ```
661
672
  */
662
673
  async function mergeDocuments(pathOrApi) {
663
- const documents = [];
664
- for (const p of pathOrApi) documents.push(await parseDocument(p, {
665
- enablePaths: false,
666
- canBundle: false
667
- }));
668
- if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
674
+ const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { canBundle: false })));
675
+ if (documents.length === 0) throw new Diagnostics.Error({
676
+ code: Diagnostics.code.inputRequired,
677
+ severity: "error",
678
+ message: "No OAS documents were provided for merging.",
679
+ help: "Pass at least one path or document to `input.path`.",
680
+ location: { kind: "config" }
681
+ });
669
682
  const seed = {
670
683
  openapi: MERGE_OPENAPI_VERSION,
671
684
  info: {
@@ -681,9 +694,9 @@ async function mergeDocuments(pathOrApi) {
681
694
  * Creates a `Document` from an `AdapterSource`.
682
695
  *
683
696
  * 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.
697
+ * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
698
+ * - `{ type: 'paths' }` merges multiple file paths into a single document.
699
+ * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
687
700
  *
688
701
  * @example
689
702
  * ```ts
@@ -691,17 +704,34 @@ async function mergeDocuments(pathOrApi) {
691
704
  * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
692
705
  * ```
693
706
  */
694
- function parseFromConfig(source) {
707
+ async function parseFromConfig(source) {
695
708
  if (source.type === "data") {
696
709
  if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
697
710
  return parseDocument(source.data, { canBundle: false });
698
711
  }
699
712
  if (source.type === "paths") return mergeDocuments(source.paths);
700
- if (new URLPath(source.path).isURL) return parseDocument(source.path);
701
- return parseDocument(path.resolve(path.dirname(source.path), source.path));
713
+ if (Url.canParse(source.path)) return parseDocument(source.path);
714
+ const resolved = path.resolve(path.dirname(source.path), source.path);
715
+ await assertInputExists(resolved);
716
+ return parseDocument(resolved);
702
717
  }
703
718
  /**
704
- * Validates an OpenAPI document using `oas-normalize` with colorized error output.
719
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
720
+ * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
721
+ * its parse error instead.
722
+ */
723
+ async function assertInputExists(input) {
724
+ if (Url.canParse(input)) return;
725
+ if (!await exists(input)) throw new Diagnostics.Error({
726
+ code: Diagnostics.code.inputNotFound,
727
+ severity: "error",
728
+ message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
729
+ help: "Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.",
730
+ location: { kind: "config" }
731
+ });
732
+ }
733
+ /**
734
+ * Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
705
735
  *
706
736
  * @example
707
737
  * ```ts
@@ -710,56 +740,374 @@ function parseFromConfig(source) {
710
740
  */
711
741
  async function validateDocument(document, { throwOnError = false } = {}) {
712
742
  try {
713
- await new OASNormalize(document, {
714
- enablePaths: true,
715
- colorizeErrors: true
716
- }).validate({ parser: { validate: { errors: { colorize: true } } } });
743
+ const result = await validate(structuredClone(document), { validate: { errors: { colorize: true } } });
744
+ if (!result.valid) throw new Error(compileErrors(result));
717
745
  } catch (error) {
718
746
  if (throwOnError) throw error;
719
747
  }
720
748
  }
721
- //#endregion
722
- //#region src/refs.ts
749
+ //#endregion
750
+ //#region src/refs.ts
751
+ const _refCache = /* @__PURE__ */ new WeakMap();
752
+ /**
753
+ * Resolves a local JSON pointer reference from a document.
754
+ *
755
+ * Accepts `#/...` refs. Returns `null` for empty or non-local refs.
756
+ * Throws when the pointer cannot be resolved.
757
+ *
758
+ * @example
759
+ * ```ts
760
+ * resolveRef<SchemaObject>(document, '#/components/schemas/Pet') // SchemaObject | null
761
+ * ```
762
+ */
763
+ function resolveRef(document, $ref) {
764
+ const origRef = $ref;
765
+ $ref = $ref.trim();
766
+ if ($ref === "") return null;
767
+ if (!$ref.startsWith("#")) return null;
768
+ $ref = globalThis.decodeURIComponent($ref.substring(1));
769
+ let docCache = _refCache.get(document);
770
+ if (!docCache) {
771
+ docCache = /* @__PURE__ */ new Map();
772
+ _refCache.set(document, docCache);
773
+ }
774
+ if (docCache.has($ref)) return docCache.get($ref);
775
+ const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
776
+ if (!current) {
777
+ const diagnostic = {
778
+ code: Diagnostics.code.refNotFound,
779
+ severity: "error",
780
+ message: `Could not find a definition for ${origRef}.`,
781
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
782
+ location: {
783
+ kind: "schema",
784
+ pointer: origRef,
785
+ ref: origRef
786
+ }
787
+ };
788
+ if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
789
+ return null;
790
+ }
791
+ docCache.set($ref, current);
792
+ return current;
793
+ }
794
+ /**
795
+ * Resolves a `$ref` object while preserving the original `$ref` field on the result.
796
+ *
797
+ * Useful for parser flows that need both dereferenced fields and pointer
798
+ * identity (for naming/import purposes). Non-reference values are returned as-is.
799
+ *
800
+ * @example
801
+ * ```ts
802
+ * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
803
+ * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
804
+ * ```
805
+ */
806
+ function dereferenceWithRef(document, schema) {
807
+ if (isReference(schema)) return {
808
+ ...schema,
809
+ ...resolveRef(document, schema.$ref),
810
+ $ref: schema.$ref
811
+ };
812
+ return schema;
813
+ }
814
+ //#endregion
815
+ //#region src/dialect.ts
816
+ /**
817
+ * The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
818
+ *
819
+ * Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
820
+ * decisions that differ between specs (nullability, `$ref`, discriminator, binary,
821
+ * ref resolution) so the converter pipeline and dispatch rules stay shared. A
822
+ * future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
823
+ * nullability, no discriminator object, binary via `contentEncoding` and reuses
824
+ * the rest unchanged.
825
+ *
826
+ * Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
827
+ * JSON Schema vocabulary, so the converters keep that common case.
828
+ *
829
+ * @example
830
+ * ```ts
831
+ * const parser = createSchemaParser(context) // uses oasDialect
832
+ * const parser = createSchemaParser(context, oasDialect) // explicit
833
+ * ```
834
+ */
835
+ const oasDialect = ast.defineSchemaDialect({
836
+ name: "oas",
837
+ isNullable,
838
+ isReference,
839
+ isDiscriminator,
840
+ isBinary: (schema) => schema.type === "string" && schema.contentMediaType === "application/octet-stream",
841
+ resolveRef
842
+ });
843
+ //#endregion
844
+ //#region src/discriminator.ts
845
+ /**
846
+ * Builds a map of child schema names → discriminator patch data by scanning the given
847
+ * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
848
+ *
849
+ * The streaming path calls this on a small pre-parsed subset of schemas (only the
850
+ * discriminator parents) rather than on all schemas at once.
851
+ */
852
+ function buildDiscriminatorChildMap(schemas) {
853
+ const childMap = /* @__PURE__ */ new Map();
854
+ for (const schema of schemas) {
855
+ let unionNode = ast.narrowSchema(schema, "union");
856
+ if (!unionNode) {
857
+ const intersectionMembers = ast.narrowSchema(schema, "intersection")?.members;
858
+ if (intersectionMembers) for (const m of intersectionMembers) {
859
+ const u = ast.narrowSchema(m, "union");
860
+ if (u) {
861
+ unionNode = u;
862
+ break;
863
+ }
864
+ }
865
+ }
866
+ if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue;
867
+ const { discriminatorPropertyName, members } = unionNode;
868
+ for (const member of members) {
869
+ const intersectionNode = ast.narrowSchema(member, "intersection");
870
+ if (!intersectionNode?.members) continue;
871
+ let refNode = null;
872
+ let objNode = null;
873
+ for (const m of intersectionNode.members) {
874
+ refNode ??= ast.narrowSchema(m, "ref");
875
+ objNode ??= ast.narrowSchema(m, "object");
876
+ }
877
+ if (!refNode?.name || !objNode) continue;
878
+ const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName);
879
+ const enumNode = prop ? ast.narrowSchema(prop.schema, "enum") : null;
880
+ if (!enumNode?.enumValues?.length) continue;
881
+ const enumValues = enumNode.enumValues.filter((v) => v !== null);
882
+ if (!enumValues.length) continue;
883
+ const existing = childMap.get(refNode.name);
884
+ if (!existing) {
885
+ childMap.set(refNode.name, {
886
+ propertyName: discriminatorPropertyName,
887
+ enumValues: [...enumValues]
888
+ });
889
+ continue;
890
+ }
891
+ existing.enumValues.push(...enumValues);
892
+ }
893
+ }
894
+ return childMap;
895
+ }
896
+ /**
897
+ * Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
898
+ * the discriminant property). Used by the streaming path to apply patches inline per yield
899
+ * without buffering all schemas.
900
+ */
901
+ function patchDiscriminatorNode(node, entry) {
902
+ const objectNode = ast.narrowSchema(node, "object");
903
+ if (!objectNode) return node;
904
+ const { propertyName, enumValues } = entry;
905
+ const enumSchema = ast.factory.createSchema({
906
+ type: "enum",
907
+ enumValues
908
+ });
909
+ const newProp = ast.factory.createProperty({
910
+ name: propertyName,
911
+ required: true,
912
+ schema: enumSchema
913
+ });
914
+ const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
915
+ const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
916
+ return {
917
+ ...objectNode,
918
+ properties: newProperties
919
+ };
920
+ }
921
+ /**
922
+ * Creates a single-property object schema used as a discriminator literal.
923
+ *
924
+ * @example
925
+ * ```ts
926
+ * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
927
+ * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
928
+ * ```
929
+ */
930
+ function createDiscriminantNode({ propertyName, value }) {
931
+ return ast.factory.createSchema({
932
+ type: "object",
933
+ primitive: "object",
934
+ properties: [ast.factory.createProperty({
935
+ name: propertyName,
936
+ schema: ast.factory.createSchema({
937
+ type: "enum",
938
+ primitive: "string",
939
+ enumValues: [value]
940
+ }),
941
+ required: true
942
+ })]
943
+ });
944
+ }
945
+ /**
946
+ * Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
947
+ *
948
+ * @example
949
+ * ```ts
950
+ * findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
951
+ * ```
952
+ */
953
+ function findDiscriminator(mapping, ref) {
954
+ if (!mapping || !ref) return null;
955
+ return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
956
+ }
957
+ //#endregion
958
+ //#region src/mime.ts
959
+ /**
960
+ * MIME type fragments that mark a media type as JSON-like.
961
+ *
962
+ * Matches the substrings used by the `oas` library's `matchesMimeType.json`, so a content type is
963
+ * JSON when it contains any of these. The `+json` entry catches structured-syntax suffixes such as
964
+ * `application/vnd.api+json`.
965
+ */
966
+ const jsonMimeFragments = [
967
+ "application/json",
968
+ "application/x-json",
969
+ "text/json",
970
+ "text/x-json",
971
+ "+json"
972
+ ];
973
+ /**
974
+ * Returns `true` when a media type string is JSON-like.
975
+ *
976
+ * @example
977
+ * ```ts
978
+ * isJsonMimeType('application/json') // true
979
+ * isJsonMimeType('application/vnd.api+json') // true
980
+ * isJsonMimeType('multipart/form-data') // false
981
+ * ```
982
+ */
983
+ function isJsonMimeType(mimeType) {
984
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
985
+ }
986
+ //#endregion
987
+ //#region src/operation.ts
988
+ /**
989
+ * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
990
+ * with no leading or trailing dash.
991
+ */
992
+ function slugify(value) {
993
+ return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
994
+ }
995
+ /**
996
+ * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
997
+ */
998
+ function getOperationId({ path, method, schema }) {
999
+ const { operationId } = schema;
1000
+ if (typeof operationId === "string" && operationId.length > 0) return operationId;
1001
+ return `${method}_${slugify(path).toLowerCase()}`;
1002
+ }
1003
+ /**
1004
+ * Returns the declared response status codes, skipping `x-` extensions and non-object entries.
1005
+ */
1006
+ function getResponseStatusCodes({ schema }) {
1007
+ const responses = schema.responses;
1008
+ if (!responses || isReference(responses)) return [];
1009
+ return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
1010
+ }
1011
+ /**
1012
+ * Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
1013
+ */
1014
+ function getResponseByStatusCode({ document, operation, statusCode }) {
1015
+ const responses = operation.schema.responses;
1016
+ if (!responses || isReference(responses)) return false;
1017
+ const response = responses[statusCode];
1018
+ if (!response) return false;
1019
+ if (isReference(response)) {
1020
+ const resolved = resolveRef(document, response.$ref);
1021
+ responses[statusCode] = resolved;
1022
+ if (!resolved || isReference(resolved)) return false;
1023
+ return resolved;
1024
+ }
1025
+ return response;
1026
+ }
1027
+ /**
1028
+ * Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
1029
+ * `undefined` when the operation has no request body.
1030
+ */
1031
+ function getRequestBodyContent({ document, operation }) {
1032
+ const { schema } = operation;
1033
+ let requestBody = schema.requestBody;
1034
+ if (!requestBody) return;
1035
+ if (isReference(requestBody)) {
1036
+ const resolved = resolveRef(document, requestBody.$ref);
1037
+ schema.requestBody = resolved;
1038
+ if (!resolved || isReference(resolved)) return;
1039
+ requestBody = resolved;
1040
+ }
1041
+ return requestBody.content;
1042
+ }
723
1043
  /**
724
- * Resolves a local JSON pointer reference from a document.
725
- *
726
- * Accepts `#/...` refs. Returns `null` for empty or non-local refs.
727
- * Throws when the pointer cannot be resolved.
728
- *
729
- * @example
730
- * ```ts
731
- * resolveRef<SchemaObject>(document, '#/components/schemas/Pet') // SchemaObject | null
732
- * ```
1044
+ * Returns the request body media type. With `mediaType` set, returns that entry or `false`.
1045
+ * Otherwise picks the first JSON-like media type, then the first declared one, as a
1046
+ * `[mediaType, object]` tuple.
733
1047
  */
734
- function resolveRef(document, $ref) {
735
- const origRef = $ref;
736
- $ref = $ref.trim();
737
- if ($ref === "") return null;
738
- if ($ref.startsWith("#")) $ref = globalThis.decodeURIComponent($ref.substring(1));
739
- else return null;
740
- 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}.`);
742
- return current;
1048
+ function getRequestContent({ document, operation, mediaType }) {
1049
+ const content = getRequestBodyContent({
1050
+ document,
1051
+ operation
1052
+ });
1053
+ if (!content) return false;
1054
+ if (mediaType) return mediaType in content ? content[mediaType] : false;
1055
+ const mediaTypes = Object.keys(content);
1056
+ const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
1057
+ return available ? [available, content[available]] : false;
743
1058
  }
744
1059
  /**
745
- * Resolves a `$ref` object while preserving the original `$ref` field on the result.
746
- *
747
- * Useful for parser flows that need both dereferenced fields and pointer
748
- * identity (for naming/import purposes). Non-reference values are returned as-is.
1060
+ * Returns the primary request content type. Prefers a JSON-like media type (the last one wins,
1061
+ * matching the previous behavior), then the first declared one, defaulting to `'application/json'`.
1062
+ */
1063
+ function getRequestContentType({ document, operation }) {
1064
+ const content = getRequestBodyContent({
1065
+ document,
1066
+ operation
1067
+ });
1068
+ const mediaTypes = content ? Object.keys(content) : [];
1069
+ let result = mediaTypes[0] ?? "application/json";
1070
+ for (const mt of mediaTypes) if (isJsonMimeType(mt)) result = mt;
1071
+ return result;
1072
+ }
1073
+ /**
1074
+ * Builds an `Operation` for every supported HTTP method on every path, in document order.
1075
+ * `x-` path keys and unresolvable path-item `$ref`s are skipped.
749
1076
  *
750
1077
  * @example
751
1078
  * ```ts
752
- * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
753
- * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
1079
+ * for (const operation of getOperations(document)) {
1080
+ * parseOperation(options, operation)
1081
+ * }
754
1082
  * ```
755
1083
  */
756
- function dereferenceWithRef(document, schema) {
757
- if (isReference(schema)) return {
758
- ...schema,
759
- ...resolveRef(document, schema.$ref),
760
- $ref: schema.$ref
761
- };
762
- return schema;
1084
+ function getOperations(document) {
1085
+ const operations = [];
1086
+ const paths = document.paths;
1087
+ if (!paths) return operations;
1088
+ for (const path of Object.keys(paths)) {
1089
+ if (path.startsWith("x-")) continue;
1090
+ let pathItem = paths[path];
1091
+ if (!pathItem) continue;
1092
+ if (isReference(pathItem)) {
1093
+ const resolved = resolveRef(document, pathItem.$ref);
1094
+ paths[path] = resolved;
1095
+ if (!resolved || isReference(resolved)) continue;
1096
+ pathItem = resolved;
1097
+ }
1098
+ const item = pathItem;
1099
+ for (const method of Object.keys(item)) {
1100
+ if (!SUPPORTED_METHODS.has(method)) continue;
1101
+ const schema = item[method];
1102
+ if (!schema || typeof schema !== "object") continue;
1103
+ operations.push({
1104
+ path,
1105
+ method,
1106
+ schema
1107
+ });
1108
+ }
1109
+ }
1110
+ return operations;
763
1111
  }
764
1112
  //#endregion
765
1113
  //#region src/resolvers.ts
@@ -783,7 +1131,16 @@ function resolveServerUrl(server, overrides) {
783
1131
  for (const [key, variable] of Object.entries(server.variables)) {
784
1132
  const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
785
1133
  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(", ")}.`);
1134
+ if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Diagnostics.Error({
1135
+ code: Diagnostics.code.invalidServerVariable,
1136
+ severity: "error",
1137
+ message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
1138
+ help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
1139
+ location: {
1140
+ kind: "document",
1141
+ pointer: "#/servers"
1142
+ }
1143
+ });
787
1144
  url = url.replaceAll(`{${key}}`, value);
788
1145
  }
789
1146
  return url;
@@ -796,6 +1153,15 @@ function getSchemaType(format) {
796
1153
  return formatMap[format] ?? null;
797
1154
  }
798
1155
  /**
1156
+ * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
1157
+ * `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
1158
+ * base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
1159
+ * diagnostic in step with the parser as `formatMap` grows.
1160
+ */
1161
+ function isHandledFormat(format) {
1162
+ return getSchemaType(format) !== null || specialCasedFormats.has(format);
1163
+ }
1164
+ /**
799
1165
  * Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
800
1166
  * Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
801
1167
  */
@@ -805,12 +1171,6 @@ function getPrimitiveType(type) {
805
1171
  return "string";
806
1172
  }
807
1173
  /**
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
1174
  * Returns all parameters for an operation, merging path-level and operation-level entries.
815
1175
  * Operation-level parameters override path-level ones with the same `in:name` key.
816
1176
  * `$ref` parameters resolve via `dereferenceWithRef` for backward compatibility.
@@ -842,7 +1202,7 @@ function getResponseBody(responseBody, contentType) {
842
1202
  }
843
1203
  let availableContentType;
844
1204
  const contentTypes = Object.keys(body.content);
845
- for (const mt of contentTypes) if (matchesMimeType.json(mt)) {
1205
+ for (const mt of contentTypes) if (isJsonMimeType(mt)) {
846
1206
  availableContentType = mt;
847
1207
  break;
848
1208
  }
@@ -873,7 +1233,11 @@ function getResponseSchema(document, operation, statusCode, options = {}) {
873
1233
  if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
874
1234
  }
875
1235
  }
876
- const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType);
1236
+ const responseBody = getResponseBody(getResponseByStatusCode({
1237
+ document,
1238
+ operation,
1239
+ statusCode
1240
+ }), options.contentType);
877
1241
  if (responseBody === false) return {};
878
1242
  const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
879
1243
  if (!schema) return {};
@@ -889,7 +1253,11 @@ function getResponseSchema(document, operation, statusCode, options = {}) {
889
1253
  */
890
1254
  function getRequestSchema(document, operation, options = {}) {
891
1255
  if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
892
- const requestBody = operation.getRequestBody(options.contentType);
1256
+ const requestBody = getRequestContent({
1257
+ document,
1258
+ operation,
1259
+ mediaType: options.contentType
1260
+ });
893
1261
  if (requestBody === false) return null;
894
1262
  const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
895
1263
  if (!schema) return null;
@@ -898,7 +1266,7 @@ function getRequestSchema(document, operation, options = {}) {
898
1266
  /**
899
1267
  * Flattens a keyword-only `allOf` into its parent schema.
900
1268
  *
901
- * Only flattens when every member is a plain fragment no `$ref` and no structural keywords
1269
+ * Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
902
1270
  * (see `structuralKeys`). Outer schema values take precedence over fragment values.
903
1271
  * Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
904
1272
  *
@@ -908,7 +1276,7 @@ function getRequestSchema(document, operation, options = {}) {
908
1276
  * // { type: 'object', properties: {}, description: 'A pet' }
909
1277
  *
910
1278
  * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
911
- * // returned unchanged contains a $ref
1279
+ * // returned unchanged, contains a $ref
912
1280
  * ```
913
1281
  */
914
1282
  /**
@@ -924,7 +1292,7 @@ function hasStructuralKeywords(fragment) {
924
1292
  function flattenSchema(schema) {
925
1293
  if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
926
1294
  const allOfFragments = schema.allOf;
927
- if (allOfFragments.some((item) => isRef(item))) return schema;
1295
+ if (allOfFragments.some((item) => isReference(item))) return schema;
928
1296
  if (allOfFragments.some(hasStructuralKeywords)) return schema;
929
1297
  const merged = { ...schema };
930
1298
  delete merged.allOf;
@@ -934,7 +1302,7 @@ function flattenSchema(schema) {
934
1302
  /**
935
1303
  * Extracts the inline schema from a media-type `content` map.
936
1304
  *
937
- * Prefers `preferredContentType` when given; otherwise uses the first key in the map.
1305
+ * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
938
1306
  * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
939
1307
  *
940
1308
  * @example
@@ -953,21 +1321,22 @@ function extractSchemaFromContent(content, preferredContentType) {
953
1321
  /**
954
1322
  * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
955
1323
  */
956
- function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
1324
+ function* collectRefs(schema) {
957
1325
  if (Array.isArray(schema)) {
958
- for (const item of schema) collectRefs(item, refs);
959
- return refs;
1326
+ for (const item of schema) yield* collectRefs(item);
1327
+ return;
960
1328
  }
961
1329
  if (schema && typeof schema === "object") for (const key in schema) {
962
1330
  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);
1331
+ if (!(key === "$ref" && typeof value === "string")) {
1332
+ yield* collectRefs(value);
1333
+ continue;
1334
+ }
1335
+ if (value.startsWith("#/components/schemas/")) {
1336
+ const name = value.slice(21);
1337
+ if (name) yield name;
1338
+ }
969
1339
  }
970
- return refs;
971
1340
  }
972
1341
  /**
973
1342
  * Returns a copy of `schemas` topologically sorted by `$ref` dependency.
@@ -983,7 +1352,7 @@ function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
983
1352
  */
984
1353
  function sortSchemas(schemas) {
985
1354
  const deps = /* @__PURE__ */ new Map();
986
- for (const [name, schema] of Object.entries(schemas)) deps.set(name, Array.from(collectRefs(schema)));
1355
+ for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
987
1356
  const sorted = [];
988
1357
  const visited = /* @__PURE__ */ new Set();
989
1358
  function visit(name, stack) {
@@ -1074,7 +1443,7 @@ function getSchemas(document, { contentType }) {
1074
1443
  }
1075
1444
  /**
1076
1445
  * Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
1077
- * Returns `null` when `dateType: false`, signalling the format should fall through to `string`.
1446
+ * Returns `null` when `dateType: false`, so the format falls through to `string`.
1078
1447
  */
1079
1448
  function getDateType(options, format) {
1080
1449
  if (!options.dateType) return null;
@@ -1118,15 +1487,16 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
1118
1487
  readOnly: schema.readOnly,
1119
1488
  writeOnly: schema.writeOnly,
1120
1489
  default: defaultValue,
1121
- example: schema.example
1490
+ example: schema.example,
1491
+ format: schema.format
1122
1492
  };
1123
1493
  }
1124
1494
  /**
1125
1495
  * Returns all request body content type keys for an operation.
1126
1496
  *
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.
1497
+ * The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
1498
+ * `getRequestSchema` already performs), so the returned list accurately reflects the
1499
+ * available content types even for referenced bodies.
1130
1500
  *
1131
1501
  * @example
1132
1502
  * ```ts
@@ -1140,15 +1510,44 @@ function getRequestBodyContentTypes(document, operation) {
1140
1510
  if (!body) return [];
1141
1511
  return body.content ? Object.keys(body.content) : [];
1142
1512
  }
1513
+ /**
1514
+ * Returns all response content type keys for an operation at a given status code.
1515
+ *
1516
+ * Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
1517
+ * so the returned list reflects the available content types even for referenced responses.
1518
+ *
1519
+ * @example
1520
+ * ```ts
1521
+ * getResponseBodyContentTypes(document, operation, 200)
1522
+ * // ['application/json', 'application/xml']
1523
+ * ```
1524
+ */
1525
+ function getResponseBodyContentTypes(document, operation, statusCode) {
1526
+ if (operation.schema.responses) {
1527
+ const responses = operation.schema.responses;
1528
+ for (const key in responses) {
1529
+ const schema = responses[key];
1530
+ if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
1531
+ }
1532
+ }
1533
+ const responseObj = getResponseByStatusCode({
1534
+ document,
1535
+ operation,
1536
+ statusCode
1537
+ });
1538
+ if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
1539
+ const body = responseObj;
1540
+ return body.content ? Object.keys(body.content) : [];
1541
+ }
1143
1542
  //#endregion
1144
1543
  //#region src/parser.ts
1145
1544
  /**
1146
1545
  * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
1147
1546
  *
1148
1547
  * This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
1149
- * from the array to its items sub-schema, making them valid for downstream processing.
1548
+ * from the array to its items sub-schema, so they are valid for downstream processing.
1150
1549
  *
1151
- * @note This is a defensive measure for robustness with non-compliant specs.
1550
+ * @note A defensive measure for non-compliant specs.
1152
1551
  */
1153
1552
  function normalizeArrayEnum(schema) {
1154
1553
  const normalizedItems = {
@@ -1162,15 +1561,32 @@ function normalizeArrayEnum(schema) {
1162
1561
  };
1163
1562
  }
1164
1563
  /**
1564
+ * Names the inline enums on a property's schema, and on each item when the property is a tuple, from
1565
+ * the parent and property name. Wraps `macroEnumName` at the property construction site.
1566
+ */
1567
+ function nameEnums(node, options) {
1568
+ const macro = macroEnumName(options);
1569
+ const named = ast.applyMacros(node, [macro], { depth: "shallow" });
1570
+ const tupleNode = ast.narrowSchema(named, "tuple");
1571
+ if (tupleNode?.items) {
1572
+ const namedItems = tupleNode.items.map((item) => ast.applyMacros(item, [macro], { depth: "shallow" }));
1573
+ if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
1574
+ ...tupleNode,
1575
+ items: namedItems
1576
+ };
1577
+ }
1578
+ return named;
1579
+ }
1580
+ /**
1165
1581
  * Factory function that creates schema and operation converters for a given OpenAPI context.
1166
1582
  *
1167
1583
  * Returns closures that share mutable state (`resolvingRefs` set for cycle detection).
1168
1584
  * Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
1169
- * made possible by hoisting of function declarations.
1585
+ * which works because function declarations hoist.
1170
1586
  *
1171
- * @note Not exported; called internally by `parseOas()` and `parseSchema()`.
1587
+ * @internal
1172
1588
  */
1173
- function createSchemaParser(ctx) {
1589
+ function createSchemaParser(ctx, dialect = oasDialect) {
1174
1590
  const document = ctx.document;
1175
1591
  /**
1176
1592
  * Tracks `$ref` paths that are currently being resolved to prevent infinite
@@ -1178,6 +1594,15 @@ function createSchemaParser(ctx) {
1178
1594
  */
1179
1595
  const resolvingRefs = /* @__PURE__ */ new Set();
1180
1596
  /**
1597
+ * Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
1598
+ *
1599
+ * Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
1600
+ * it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
1601
+ * since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
1602
+ * Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
1603
+ */
1604
+ const resolvedRefCache = /* @__PURE__ */ new Map();
1605
+ /**
1181
1606
  * Converts a `$ref` schema into a `RefSchemaNode`.
1182
1607
  *
1183
1608
  * The resolved schema is stored in `node.schema`. Usage-site sibling fields
@@ -1186,20 +1611,26 @@ function createSchemaParser(ctx) {
1186
1611
  * Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
1187
1612
  */
1188
1613
  function convertRef({ schema, name, nullable, defaultValue, rawOptions }) {
1189
- let resolvedSchema;
1614
+ let resolvedSchema = null;
1190
1615
  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);
1616
+ if (refPath && !resolvingRefs.has(refPath)) {
1617
+ if (!resolvedRefCache.has(refPath)) {
1618
+ try {
1619
+ const referenced = dialect.resolveRef(document, refPath);
1620
+ if (referenced) {
1621
+ resolvingRefs.add(refPath);
1622
+ resolvedSchema = parseSchema({ schema: referenced }, rawOptions);
1623
+ resolvingRefs.delete(refPath);
1624
+ }
1625
+ } catch {}
1626
+ resolvedRefCache.set(refPath, resolvedSchema);
1197
1627
  }
1198
- } catch {}
1199
- return ast.createSchema({
1628
+ resolvedSchema = resolvedRefCache.get(refPath) ?? null;
1629
+ }
1630
+ return ast.factory.createSchema({
1200
1631
  ...buildSchemaNode(schema, name, nullable, defaultValue),
1201
1632
  type: "ref",
1202
- name: ast.extractRefName(schema.$ref),
1633
+ name: extractRefName(schema.$ref),
1203
1634
  ref: schema.$ref,
1204
1635
  schema: resolvedSchema
1205
1636
  });
@@ -1212,12 +1643,12 @@ function createSchemaParser(ctx) {
1212
1643
  const [memberSchema] = schema.allOf;
1213
1644
  const memberNode = parseSchema({
1214
1645
  schema: memberSchema,
1215
- name: null
1646
+ name
1216
1647
  }, rawOptions);
1217
1648
  const { kind: _kind, ...memberNodeProps } = memberNode;
1218
1649
  const mergedNullable = nullable || memberNode.nullable || void 0;
1219
1650
  const mergedDefault = schema.default === null && mergedNullable ? void 0 : schema.default ?? memberNode.default;
1220
- return ast.createSchema({
1651
+ return ast.factory.createSchema({
1221
1652
  ...memberNodeProps,
1222
1653
  name,
1223
1654
  title: schema.title ?? memberNode.title,
@@ -1228,21 +1659,22 @@ function createSchemaParser(ctx) {
1228
1659
  writeOnly: schema.writeOnly ?? memberNode.writeOnly,
1229
1660
  default: mergedDefault,
1230
1661
  example: schema.example ?? memberNode.example,
1231
- pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0)
1662
+ pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0),
1663
+ format: schema.format ?? memberNode.format
1232
1664
  });
1233
1665
  }
1234
1666
  const filteredDiscriminantValues = [];
1235
1667
  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;
1668
+ if (!dialect.isReference(item) || !name) return true;
1669
+ const deref = dialect.resolveRef(document, item.$ref);
1670
+ if (!deref || !dialect.isDiscriminator(deref)) return true;
1239
1671
  const parentUnion = deref.oneOf ?? deref.anyOf;
1240
1672
  if (!parentUnion) return true;
1241
1673
  const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1242
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1674
+ const inOneOf = parentUnion.some((oneOfItem) => dialect.isReference(oneOfItem) && oneOfItem.$ref === childRef);
1243
1675
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1244
1676
  if (inOneOf || inMapping) {
1245
- const discriminatorValue = ast.findDiscriminator(deref.discriminator.mapping, childRef);
1677
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
1246
1678
  if (discriminatorValue) filteredDiscriminantValues.push({
1247
1679
  propertyName: deref.discriminator.propertyName,
1248
1680
  value: discriminatorValue
@@ -1250,22 +1682,28 @@ function createSchemaParser(ctx) {
1250
1682
  return false;
1251
1683
  }
1252
1684
  return true;
1253
- }).map((s) => parseSchema({ schema: s }, rawOptions));
1685
+ }).map((s) => parseSchema({
1686
+ schema: s,
1687
+ name
1688
+ }, rawOptions));
1254
1689
  const syntheticStart = allOfMembers.length;
1255
1690
  if (Array.isArray(schema.required) && schema.required.length) {
1256
1691
  const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
1257
1692
  const missingRequired = schema.required.filter((key) => !outerKeys.has(key));
1258
1693
  if (missingRequired.length) {
1259
1694
  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] : [];
1695
+ if (!dialect.isReference(item)) return [item];
1696
+ const deref = dialect.resolveRef(document, item.$ref);
1697
+ return deref && !dialect.isReference(deref) ? [deref] : [];
1263
1698
  });
1264
1699
  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));
1700
+ allOfMembers.push(parseSchema({
1701
+ schema: {
1702
+ properties: { [key]: resolved.properties[key] },
1703
+ required: [key]
1704
+ },
1705
+ name
1706
+ }, rawOptions));
1269
1707
  break;
1270
1708
  }
1271
1709
  }
@@ -1274,13 +1712,13 @@ function createSchemaParser(ctx) {
1274
1712
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
1275
1713
  allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions));
1276
1714
  }
1277
- for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(ast.createDiscriminantNode({
1715
+ for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(createDiscriminantNode({
1278
1716
  propertyName,
1279
1717
  value
1280
1718
  }));
1281
- return ast.createSchema({
1719
+ return ast.factory.createSchema({
1282
1720
  type: "intersection",
1283
- members: [...ast.mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
1721
+ members: [...mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
1284
1722
  ...buildSchemaNode(schema, name, nullable, defaultValue)
1285
1723
  });
1286
1724
  }
@@ -1291,7 +1729,7 @@ function createSchemaParser(ctx) {
1291
1729
  function pickDiscriminatorPropertyNode(node, propertyName) {
1292
1730
  const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1293
1731
  if (!discriminatorProperty) return null;
1294
- return ast.createSchema({
1732
+ return ast.factory.createSchema({
1295
1733
  type: "object",
1296
1734
  primitive: "object",
1297
1735
  properties: [discriminatorProperty]
@@ -1301,10 +1739,10 @@ function createSchemaParser(ctx) {
1301
1739
  const strategy = schema.oneOf ? "one" : "any";
1302
1740
  const unionBase = {
1303
1741
  ...buildSchemaNode(schema, name, nullable, defaultValue),
1304
- discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1742
+ discriminatorPropertyName: dialect.isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1305
1743
  strategy
1306
1744
  };
1307
- const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
1745
+ const discriminator = dialect.isDiscriminator(schema) ? schema.discriminator : void 0;
1308
1746
  const sharedPropertiesNode = schema.properties ? (() => {
1309
1747
  const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema;
1310
1748
  return parseSchema({
@@ -1314,56 +1752,63 @@ function createSchemaParser(ctx) {
1314
1752
  })() : void 0;
1315
1753
  if (sharedPropertiesNode || discriminator?.mapping) {
1316
1754
  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);
1755
+ const ref = dialect.isReference(s) ? s.$ref : void 0;
1756
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref);
1757
+ const memberNode = parseSchema({
1758
+ schema: s,
1759
+ name
1760
+ }, rawOptions);
1320
1761
  if (!discriminatorValue || !discriminator) return memberNode;
1321
- const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.setDiscriminatorEnum({
1322
- node: sharedPropertiesNode,
1762
+ const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.applyMacros(sharedPropertiesNode, [macroDiscriminatorEnum({
1323
1763
  propertyName: discriminator.propertyName,
1324
1764
  values: [discriminatorValue]
1325
- }), discriminator.propertyName) : void 0;
1326
- return ast.createSchema({
1765
+ })], { depth: "shallow" }), discriminator.propertyName) : void 0;
1766
+ return ast.factory.createSchema({
1327
1767
  type: "intersection",
1328
- members: [memberNode, narrowedDiscriminatorNode ?? ast.createDiscriminantNode({
1768
+ members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
1329
1769
  propertyName: discriminator.propertyName,
1330
1770
  value: discriminatorValue
1331
1771
  })]
1332
1772
  });
1333
1773
  });
1334
- const unionNode = ast.createSchema({
1774
+ const unionNode = ast.factory.createSchema({
1335
1775
  type: "union",
1336
1776
  ...unionBase,
1337
1777
  members
1338
1778
  });
1339
1779
  if (!sharedPropertiesNode) return unionNode;
1340
- return ast.createSchema({
1780
+ return ast.factory.createSchema({
1341
1781
  type: "intersection",
1342
1782
  ...buildSchemaNode(schema, name, nullable, defaultValue),
1343
1783
  members: [unionNode, sharedPropertiesNode]
1344
1784
  });
1345
1785
  }
1346
- return ast.createSchema({
1786
+ const unionNode = ast.factory.createSchema({
1347
1787
  type: "union",
1348
1788
  ...unionBase,
1349
- members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s }, rawOptions)))
1789
+ members: unionMembers.map((s) => parseSchema({
1790
+ schema: s,
1791
+ name
1792
+ }, rawOptions))
1350
1793
  });
1794
+ return ast.applyMacros(unionNode, [macroSimplifyUnion], { depth: "shallow" });
1351
1795
  }
1352
1796
  /**
1353
1797
  * Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
1354
1798
  */
1355
1799
  function convertConst({ schema, name, nullable, defaultValue }) {
1356
1800
  const constValue = schema.const;
1357
- if (constValue === null) return ast.createSchema({
1801
+ if (constValue === null) return ast.factory.createSchema({
1358
1802
  type: "null",
1359
1803
  primitive: "null",
1360
1804
  name,
1361
1805
  title: schema.title,
1362
1806
  description: schema.description,
1363
- deprecated: schema.deprecated
1807
+ deprecated: schema.deprecated,
1808
+ format: schema.format
1364
1809
  });
1365
1810
  const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
1366
- return ast.createSchema({
1811
+ return ast.factory.createSchema({
1367
1812
  type: "enum",
1368
1813
  primitive: constPrimitive,
1369
1814
  enumValues: [constValue],
@@ -1376,7 +1821,7 @@ function createSchemaParser(ctx) {
1376
1821
  */
1377
1822
  function convertFormat({ schema, name, nullable, defaultValue, options }) {
1378
1823
  const base = buildSchemaNode(schema, name, nullable, defaultValue);
1379
- if (schema.format === "int64") return ast.createSchema({
1824
+ if (schema.format === "int64") return ast.factory.createSchema({
1380
1825
  type: options.integerType === "bigint" ? "bigint" : "integer",
1381
1826
  primitive: "integer",
1382
1827
  ...base,
@@ -1388,14 +1833,14 @@ function createSchemaParser(ctx) {
1388
1833
  if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
1389
1834
  const dateType = getDateType(options, schema.format);
1390
1835
  if (!dateType) return null;
1391
- if (dateType.type === "datetime") return ast.createSchema({
1836
+ if (dateType.type === "datetime") return ast.factory.createSchema({
1392
1837
  ...base,
1393
1838
  primitive: "string",
1394
1839
  type: "datetime",
1395
1840
  offset: dateType.offset,
1396
1841
  local: dateType.local
1397
1842
  });
1398
- return ast.createSchema({
1843
+ return ast.factory.createSchema({
1399
1844
  ...base,
1400
1845
  primitive: "string",
1401
1846
  type: dateType.type,
@@ -1405,36 +1850,36 @@ function createSchemaParser(ctx) {
1405
1850
  const specialType = getSchemaType(schema.format);
1406
1851
  if (!specialType) return null;
1407
1852
  const specialPrimitive = specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string";
1408
- if (specialType === "number" || specialType === "integer" || specialType === "bigint") return ast.createSchema({
1853
+ if (specialType === "number" || specialType === "integer" || specialType === "bigint") return ast.factory.createSchema({
1409
1854
  ...base,
1410
1855
  primitive: specialPrimitive,
1411
1856
  type: specialType
1412
1857
  });
1413
- if (specialType === "url") return ast.createSchema({
1858
+ if (specialType === "url") return ast.factory.createSchema({
1414
1859
  ...base,
1415
1860
  primitive: "string",
1416
1861
  type: "url",
1417
1862
  min: schema.minLength,
1418
1863
  max: schema.maxLength
1419
1864
  });
1420
- if (specialType === "ipv4") return ast.createSchema({
1865
+ if (specialType === "ipv4") return ast.factory.createSchema({
1421
1866
  ...base,
1422
1867
  primitive: "string",
1423
1868
  type: "ipv4"
1424
1869
  });
1425
- if (specialType === "ipv6") return ast.createSchema({
1870
+ if (specialType === "ipv6") return ast.factory.createSchema({
1426
1871
  ...base,
1427
1872
  primitive: "string",
1428
1873
  type: "ipv6"
1429
1874
  });
1430
- if (specialType === "uuid" || specialType === "email") return ast.createSchema({
1875
+ if (specialType === "uuid" || specialType === "email") return ast.factory.createSchema({
1431
1876
  ...base,
1432
1877
  primitive: "string",
1433
1878
  type: specialType,
1434
1879
  min: schema.minLength,
1435
1880
  max: schema.maxLength
1436
1881
  });
1437
- return ast.createSchema({
1882
+ return ast.factory.createSchema({
1438
1883
  ...base,
1439
1884
  primitive: specialPrimitive,
1440
1885
  type: specialType
@@ -1450,6 +1895,15 @@ function createSchemaParser(ctx) {
1450
1895
  }, rawOptions);
1451
1896
  const nullInEnum = schema.enum.includes(null);
1452
1897
  const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
1898
+ if (nullInEnum && filteredValues.length === 0) return ast.factory.createSchema({
1899
+ type: "null",
1900
+ primitive: "null",
1901
+ name,
1902
+ title: schema.title,
1903
+ description: schema.description,
1904
+ deprecated: schema.deprecated,
1905
+ format: schema.format
1906
+ });
1453
1907
  const enumNullable = nullable || nullInEnum || void 0;
1454
1908
  const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1455
1909
  const enumPrimitive = getPrimitiveType(type);
@@ -1464,7 +1918,8 @@ function createSchemaParser(ctx) {
1464
1918
  readOnly: schema.readOnly,
1465
1919
  writeOnly: schema.writeOnly,
1466
1920
  default: enumDefault,
1467
- example: schema.example
1921
+ example: schema.example,
1922
+ format: schema.format
1468
1923
  };
1469
1924
  const extensionKey = enumExtensionKeys.find((key) => key in schema);
1470
1925
  if (extensionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
@@ -1472,7 +1927,7 @@ function createSchemaParser(ctx) {
1472
1927
  const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
1473
1928
  const uniqueValues = [...new Set(filteredValues)];
1474
1929
  const seenNames = /* @__PURE__ */ new Set();
1475
- return ast.createSchema({
1930
+ return ast.factory.createSchema({
1476
1931
  ...enumBase,
1477
1932
  primitive: enumPrimitiveType,
1478
1933
  namedEnumValues: uniqueValues.map((value, index) => ({
@@ -1486,7 +1941,7 @@ function createSchemaParser(ctx) {
1486
1941
  })
1487
1942
  });
1488
1943
  }
1489
- return ast.createSchema({
1944
+ return ast.factory.createSchema({
1490
1945
  ...enumBase,
1491
1946
  enumValues: [...new Set(filteredValues)]
1492
1947
  });
@@ -1498,21 +1953,16 @@ function createSchemaParser(ctx) {
1498
1953
  const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
1499
1954
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
1500
1955
  const resolvedPropSchema = propSchema;
1501
- const propNullable = isNullable(resolvedPropSchema);
1502
- const propNode = parseSchema({
1956
+ const propNullable = dialect.isNullable(resolvedPropSchema);
1957
+ const schemaNode = nameEnums(parseSchema({
1503
1958
  schema: resolvedPropSchema,
1504
- name: ast.childName(name, propName)
1505
- }, 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
- }
1515
- return ast.createProperty({
1959
+ name: childName(name, propName)
1960
+ }, rawOptions), {
1961
+ parentName: name,
1962
+ propName,
1963
+ enumSuffix: options.enumSuffix
1964
+ });
1965
+ return ast.factory.createProperty({
1516
1966
  name: propName,
1517
1967
  schema: {
1518
1968
  ...schemaNode,
@@ -1522,14 +1972,15 @@ function createSchemaParser(ctx) {
1522
1972
  });
1523
1973
  }) : [];
1524
1974
  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) });
1975
+ const additionalPropertiesNode = (() => {
1976
+ if (additionalProperties === true) return true;
1977
+ if (additionalProperties === false) return false;
1978
+ if (additionalProperties && Object.keys(additionalProperties).length > 0) return parseSchema({ schema: additionalProperties }, rawOptions);
1979
+ if (additionalProperties) return ast.factory.createSchema({ type: typeOptionMap.get(options.unknownType) });
1980
+ })();
1530
1981
  const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
1531
- 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
- const objectNode = ast.createSchema({
1982
+ 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: typeOptionMap.get(options.unknownType) }) : parseSchema({ schema: patternSchema }, rawOptions)])) : void 0;
1983
+ const objectNode = ast.factory.createSchema({
1533
1984
  type: "object",
1534
1985
  primitive: "object",
1535
1986
  properties,
@@ -1539,16 +1990,15 @@ function createSchemaParser(ctx) {
1539
1990
  maxProperties: schema.maxProperties,
1540
1991
  ...buildSchemaNode(schema, name, nullable, defaultValue)
1541
1992
  });
1542
- if (isDiscriminator(schema) && schema.discriminator.mapping) {
1993
+ if (dialect.isDiscriminator(schema) && schema.discriminator.mapping) {
1543
1994
  const discPropName = schema.discriminator.propertyName;
1544
1995
  const values = Object.keys(schema.discriminator.mapping);
1545
- const enumName = name ? ast.enumPropName(name, discPropName, options.enumSuffix) : void 0;
1546
- return ast.setDiscriminatorEnum({
1547
- node: objectNode,
1996
+ const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
1997
+ return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
1548
1998
  propertyName: discPropName,
1549
1999
  values,
1550
2000
  enumName
1551
- });
2001
+ })], { depth: "shallow" });
1552
2002
  }
1553
2003
  return objectNode;
1554
2004
  }
@@ -1557,8 +2007,8 @@ function createSchemaParser(ctx) {
1557
2007
  */
1558
2008
  function convertTuple({ schema, name, nullable, defaultValue, rawOptions }) {
1559
2009
  const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item }, rawOptions));
1560
- const rest = schema.items ? parseSchema({ schema: schema.items }, rawOptions) : ast.createSchema({ type: "any" });
1561
- return ast.createSchema({
2010
+ const rest = schema.items ? parseSchema({ schema: schema.items }, rawOptions) : ast.factory.createSchema({ type: "any" });
2011
+ return ast.factory.createSchema({
1562
2012
  type: "tuple",
1563
2013
  primitive: "array",
1564
2014
  items: tupleItems,
@@ -1573,12 +2023,12 @@ function createSchemaParser(ctx) {
1573
2023
  */
1574
2024
  function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }) {
1575
2025
  const rawItems = schema.items;
1576
- const itemName = rawItems?.enum?.length && name ? ast.enumPropName(void 0, name, options.enumSuffix) : void 0;
2026
+ const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name;
1577
2027
  const items = rawItems ? [parseSchema({
1578
2028
  schema: rawItems,
1579
2029
  name: itemName
1580
2030
  }, rawOptions)] : [];
1581
- return ast.createSchema({
2031
+ return ast.factory.createSchema({
1582
2032
  type: "array",
1583
2033
  primitive: "array",
1584
2034
  items,
@@ -1592,7 +2042,7 @@ function createSchemaParser(ctx) {
1592
2042
  * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1593
2043
  */
1594
2044
  function convertString({ schema, name, nullable, defaultValue }) {
1595
- return ast.createSchema({
2045
+ return ast.factory.createSchema({
1596
2046
  type: "string",
1597
2047
  primitive: "string",
1598
2048
  min: schema.minLength,
@@ -1605,7 +2055,7 @@ function createSchemaParser(ctx) {
1605
2055
  * Converts a `type: 'number'` or `type: 'integer'` schema.
1606
2056
  */
1607
2057
  function convertNumeric({ schema, name, nullable, defaultValue }, type) {
1608
- return ast.createSchema({
2058
+ return ast.factory.createSchema({
1609
2059
  type,
1610
2060
  primitive: type,
1611
2061
  min: schema.minimum,
@@ -1620,7 +2070,7 @@ function createSchemaParser(ctx) {
1620
2070
  * Converts a `type: 'boolean'` schema.
1621
2071
  */
1622
2072
  function convertBoolean({ schema, name, nullable, defaultValue }) {
1623
- return ast.createSchema({
2073
+ return ast.factory.createSchema({
1624
2074
  type: "boolean",
1625
2075
  primitive: "boolean",
1626
2076
  ...buildSchemaNode(schema, name, nullable, defaultValue)
@@ -1630,22 +2080,155 @@ function createSchemaParser(ctx) {
1630
2080
  * Converts an explicit `type: 'null'` schema.
1631
2081
  */
1632
2082
  function convertNull({ schema, name, nullable }) {
1633
- return ast.createSchema({
2083
+ return ast.factory.createSchema({
1634
2084
  type: "null",
1635
2085
  primitive: "null",
1636
2086
  name,
1637
2087
  title: schema.title,
1638
2088
  description: schema.description,
1639
2089
  deprecated: schema.deprecated,
1640
- nullable
2090
+ nullable,
2091
+ format: schema.format
2092
+ });
2093
+ }
2094
+ /**
2095
+ * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
2096
+ * into a `blob` node.
2097
+ */
2098
+ function convertBlob({ schema, name, nullable, defaultValue }) {
2099
+ return ast.factory.createSchema({
2100
+ type: "blob",
2101
+ primitive: "string",
2102
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
2103
+ });
2104
+ }
2105
+ /**
2106
+ * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
2107
+ *
2108
+ * Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parseSchema`
2109
+ * falls through and handles it as that single type with nullability already folded in.
2110
+ */
2111
+ function convertMultiType({ schema, name, nullable, defaultValue, rawOptions }) {
2112
+ const types = schema.type;
2113
+ const nonNullTypes = types.filter((t) => t !== "null");
2114
+ if (nonNullTypes.length <= 1) return null;
2115
+ const arrayNullable = types.includes("null") || nullable || void 0;
2116
+ return ast.factory.createSchema({
2117
+ type: "union",
2118
+ members: nonNullTypes.map((t) => parseSchema({
2119
+ schema: {
2120
+ ...schema,
2121
+ type: t
2122
+ },
2123
+ name
2124
+ }, rawOptions)),
2125
+ ...buildSchemaNode(schema, name, arrayNullable, defaultValue)
1641
2126
  });
1642
2127
  }
1643
2128
  /**
1644
- * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
2129
+ * Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
2130
+ * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
2131
+ * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
2132
+ * match/convert/fall-through contract.
2133
+ */
2134
+ const schemaRules = [
2135
+ {
2136
+ name: "ref",
2137
+ match: ({ schema }) => dialect.isReference(schema),
2138
+ convert: convertRef
2139
+ },
2140
+ {
2141
+ name: "allOf",
2142
+ match: ({ schema }) => !!schema.allOf?.length,
2143
+ convert: convertAllOf
2144
+ },
2145
+ {
2146
+ name: "union",
2147
+ match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length),
2148
+ convert: convertUnion
2149
+ },
2150
+ {
2151
+ name: "const",
2152
+ match: ({ schema }) => "const" in schema && schema.const !== void 0,
2153
+ convert: convertConst
2154
+ },
2155
+ {
2156
+ name: "format",
2157
+ match: ({ schema }) => !!schema.format,
2158
+ convert: convertFormat
2159
+ },
2160
+ {
2161
+ name: "blob",
2162
+ match: ({ schema }) => dialect.isBinary(schema),
2163
+ convert: convertBlob
2164
+ },
2165
+ {
2166
+ name: "multi-type",
2167
+ match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
2168
+ convert: convertMultiType
2169
+ },
2170
+ {
2171
+ name: "constrained-string",
2172
+ match: ({ schema, type }) => !type && (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0),
2173
+ convert: convertString
2174
+ },
2175
+ {
2176
+ name: "constrained-number",
2177
+ match: ({ schema, type }) => !type && (schema.minimum !== void 0 || schema.maximum !== void 0),
2178
+ convert: (ctx) => convertNumeric(ctx, "number")
2179
+ },
2180
+ {
2181
+ name: "enum",
2182
+ match: ({ schema }) => !!schema.enum?.length,
2183
+ convert: convertEnum
2184
+ },
2185
+ {
2186
+ name: "object",
2187
+ match: ({ schema, type }) => type === "object" || !!schema.properties || !!schema.additionalProperties || "patternProperties" in schema,
2188
+ convert: convertObject
2189
+ },
2190
+ {
2191
+ name: "tuple",
2192
+ match: ({ schema }) => "prefixItems" in schema,
2193
+ convert: convertTuple
2194
+ },
2195
+ {
2196
+ name: "array",
2197
+ match: ({ schema, type }) => type === "array" || "items" in schema,
2198
+ convert: convertArray
2199
+ },
2200
+ {
2201
+ name: "string",
2202
+ match: ({ type }) => type === "string",
2203
+ convert: convertString
2204
+ },
2205
+ {
2206
+ name: "number",
2207
+ match: ({ type }) => type === "number",
2208
+ convert: (ctx) => convertNumeric(ctx, "number")
2209
+ },
2210
+ {
2211
+ name: "integer",
2212
+ match: ({ type }) => type === "integer",
2213
+ convert: (ctx) => convertNumeric(ctx, "integer")
2214
+ },
2215
+ {
2216
+ name: "boolean",
2217
+ match: ({ type }) => type === "boolean",
2218
+ convert: convertBoolean
2219
+ },
2220
+ {
2221
+ name: "null",
2222
+ match: ({ type }) => type === "null",
2223
+ convert: convertNull
2224
+ }
2225
+ ];
2226
+ /**
2227
+ * Converts an OAS `SchemaObject` into a `SchemaNode`.
1645
2228
  *
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).
2229
+ * Builds the per-schema context, then walks the ordered {@link schemaRules} table and returns
2230
+ * the first converter that produces a node. When none match, falls back to the configured
2231
+ * `emptySchemaType`.
1649
2232
  */
1650
2233
  function parseSchema({ schema, name }, rawOptions) {
1651
2234
  const options = {
@@ -1657,75 +2240,43 @@ function createSchemaParser(ctx) {
1657
2240
  schema: flattenedSchema,
1658
2241
  name
1659
2242
  }, 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 = {
2243
+ const nullable = dialect.isNullable(schema) || void 0;
2244
+ const schemaCtx = {
1664
2245
  schema,
1665
2246
  name,
1666
2247
  nullable,
1667
- defaultValue,
1668
- type,
2248
+ defaultValue: schema.default === null && nullable ? void 0 : schema.default,
2249
+ type: Array.isArray(schema.type) ? schema.type[0] : schema.type,
1669
2250
  rawOptions,
1670
2251
  options
1671
2252
  };
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
- });
2253
+ for (const rule of schemaRules) {
2254
+ if (!rule.match(schemaCtx)) continue;
2255
+ const node = rule.convert(schemaCtx);
2256
+ if (node) return node;
1699
2257
  }
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);
1713
2258
  const emptyType = typeOptionMap.get(options.emptySchemaType);
1714
- return ast.createSchema({
2259
+ return ast.factory.createSchema({
1715
2260
  type: emptyType,
1716
2261
  name,
1717
2262
  title: schema.title,
1718
- description: schema.description
2263
+ description: schema.description,
2264
+ format: schema.format
1719
2265
  });
1720
2266
  }
1721
2267
  /**
1722
2268
  * Converts a dereferenced OAS parameter object into a `ParameterNode`.
1723
2269
  */
1724
- function parseParameter(options, param) {
2270
+ function parseParameter(options, param, parentName) {
1725
2271
  const required = param["required"] ?? false;
1726
- const schema = param["schema"] ? parseSchema({ schema: param["schema"] }, options) : ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1727
- return ast.createParameter({
1728
- name: param["name"],
2272
+ const paramName = param["name"];
2273
+ const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : void 0;
2274
+ const schema = param["schema"] ? parseSchema({
2275
+ schema: param["schema"],
2276
+ name: schemaName
2277
+ }, options) : ast.factory.createSchema({ type: typeOptionMap.get(options.unknownType) });
2278
+ return ast.factory.createParameter({
2279
+ name: paramName,
1729
2280
  in: param["in"],
1730
2281
  schema: {
1731
2282
  ...schema,
@@ -1762,27 +2313,33 @@ function createSchemaParser(ctx) {
1762
2313
  * `$ref` entries are skipped since their flags live on the dereferenced target.
1763
2314
  */
1764
2315
  function collectPropertyKeysByFlag(schema, flag) {
1765
- if (!schema?.properties) return void 0;
2316
+ if (!schema?.properties) return null;
1766
2317
  const keys = [];
1767
2318
  for (const key in schema.properties) {
1768
2319
  const prop = schema.properties[key];
1769
- if (prop && !isReference(prop) && prop[flag]) keys.push(key);
2320
+ if (prop && !dialect.isReference(prop) && prop[flag]) keys.push(key);
1770
2321
  }
1771
- return keys.length ? keys : void 0;
2322
+ return keys.length ? keys : null;
1772
2323
  }
1773
2324
  /**
1774
2325
  * Converts an OAS `Operation` into an `OperationNode`.
1775
2326
  */
1776
2327
  function parseOperation(options, operation) {
1777
- const parameters = getParameters(document, operation).map((param) => parseParameter(options, param));
2328
+ const operationId = getOperationId(operation);
2329
+ const operationName = operationId ? pascalCase(operationId) : void 0;
2330
+ const parameters = getParameters(document, operation).map((param) => parseParameter(options, param, operationName));
1778
2331
  const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
1779
2332
  const requestBodyMeta = getRequestBodyMeta(operation);
2333
+ const requestBodyName = operationName ? `${operationName}Request` : void 0;
1780
2334
  const content = allContentTypes.flatMap((ct) => {
1781
2335
  const schema = getRequestSchema(document, operation, { contentType: ct });
1782
2336
  if (!schema) return [];
1783
2337
  return [{
1784
2338
  contentType: ct,
1785
- schema: ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
2339
+ schema: ast.syncOptionality(parseSchema({
2340
+ schema,
2341
+ name: requestBodyName
2342
+ }, options), requestBodyMeta.required),
1786
2343
  keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
1787
2344
  }];
1788
2345
  });
@@ -1791,29 +2348,58 @@ function createSchemaParser(ctx) {
1791
2348
  required: requestBodyMeta.required || void 0,
1792
2349
  content: content.length > 0 ? content : void 0
1793
2350
  } : void 0;
1794
- const responses = operation.getResponseStatusCodes().map((statusCode) => {
1795
- 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 ?? "");
1800
- return ast.createResponse({
2351
+ const responses = getResponseStatusCodes(operation).map((statusCode) => {
2352
+ const responseObj = getResponseByStatusCode({
2353
+ document,
2354
+ operation,
2355
+ statusCode
2356
+ });
2357
+ const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
2358
+ const { description } = getResponseMeta(responseObj);
2359
+ const parseEntrySchema = (contentType) => {
2360
+ const raw = getResponseSchema(document, operation, statusCode, { contentType });
2361
+ return {
2362
+ schema: raw && Object.keys(raw).length > 0 ? parseSchema({
2363
+ schema: raw,
2364
+ name: responseName
2365
+ }, options) : ast.factory.createSchema({ type: typeOptionMap.get(options.emptySchemaType) }),
2366
+ keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
2367
+ };
2368
+ };
2369
+ const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)).map((contentType) => ({
2370
+ contentType,
2371
+ ...parseEntrySchema(contentType)
2372
+ }));
2373
+ if (content.length === 0) content.push({
2374
+ contentType: getRequestContentType({
2375
+ document,
2376
+ operation
2377
+ }) || "application/json",
2378
+ ...parseEntrySchema(ctx.contentType)
2379
+ });
2380
+ return ast.factory.createResponse({
1801
2381
  statusCode,
1802
2382
  description,
1803
- schema,
1804
- mediaType,
1805
- keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
2383
+ content
1806
2384
  });
1807
2385
  });
1808
- const urlPath = new URLPath(operation.path);
1809
- return ast.createOperation({
1810
- operationId: operation.getOperationId(),
2386
+ const pathItem = document.paths?.[operation.path];
2387
+ const pathItemDoc = pathItem && !dialect.isReference(pathItem) ? pathItem : void 0;
2388
+ const pickDoc = (key) => {
2389
+ const own = operation.schema[key];
2390
+ if (typeof own === "string") return own;
2391
+ const fallback = pathItemDoc?.[key];
2392
+ return typeof fallback === "string" ? fallback : void 0;
2393
+ };
2394
+ return ast.factory.createOperation({
2395
+ operationId,
2396
+ protocol: "http",
1811
2397
  method: operation.method.toUpperCase(),
1812
- path: urlPath.path,
1813
- tags: operation.getTags().map((tag) => tag.name),
1814
- summary: operation.getSummary() || void 0,
1815
- description: operation.getDescription() || void 0,
1816
- deprecated: operation.isDeprecated() || void 0,
2398
+ path: operation.path,
2399
+ tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
2400
+ summary: pickDoc("summary") || void 0,
2401
+ description: pickDoc("description") || void 0,
2402
+ deprecated: operation.schema.deprecated || void 0,
1817
2403
  parameters,
1818
2404
  requestBody,
1819
2405
  responses
@@ -1825,59 +2411,263 @@ function createSchemaParser(ctx) {
1825
2411
  parseParameter
1826
2412
  };
1827
2413
  }
2414
+ //#endregion
2415
+ //#region src/schemaDiagnostics.ts
2416
+ /**
2417
+ * Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
2418
+ * top-level schema. Walks the node the parser produced during `preScan`, threading the RFC 6901
2419
+ * pointer as it descends so a nested field reports against its full path
2420
+ * (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
2421
+ * resolved schema is reported under its own walk. Reports land in the active build run, are a
2422
+ * no-op outside one, and repeats are deduped by the build.
2423
+ */
2424
+ function reportSchemaDiagnostics({ node, name }) {
2425
+ visit(node, `#/components/schemas/${escapePointerToken(name)}`);
2426
+ }
2427
+ /**
2428
+ * Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
2429
+ * property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
2430
+ */
2431
+ function escapePointerToken(token) {
2432
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
2433
+ }
2434
+ function visit(node, pointer) {
2435
+ if (node.deprecated) Diagnostics.report({
2436
+ code: Diagnostics.code.deprecated,
2437
+ severity: "info",
2438
+ message: "This schema is marked as deprecated.",
2439
+ location: {
2440
+ kind: "schema",
2441
+ pointer
2442
+ }
2443
+ });
2444
+ if (typeof node.format === "string" && !isHandledFormat(node.format)) Diagnostics.report({
2445
+ code: Diagnostics.code.unsupportedFormat,
2446
+ severity: "warning",
2447
+ message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
2448
+ help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
2449
+ location: {
2450
+ kind: "schema",
2451
+ pointer
2452
+ }
2453
+ });
2454
+ if (node.type === "object") {
2455
+ for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
2456
+ if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
2457
+ return;
2458
+ }
2459
+ if (node.type === "array") {
2460
+ for (const item of node.items ?? []) visit(item, `${pointer}/items`);
2461
+ return;
2462
+ }
2463
+ if (node.type === "tuple") {
2464
+ for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
2465
+ return;
2466
+ }
2467
+ if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
2468
+ }
2469
+ //#endregion
2470
+ //#region src/stream.ts
2471
+ /**
2472
+ * Builds the deduplication plan from the already-parsed top-level schema nodes plus a
2473
+ * single extra parse pass over operations (so duplicates in request/response bodies are seen).
2474
+ *
2475
+ * Only enums and objects are candidates, and object shapes that reference a circular schema are
2476
+ * rejected to avoid hoisting recursive structures. Inline shapes reuse their context-derived
2477
+ * name (collision-resolved against existing component names). Shapes without a name stay inline.
2478
+ */
2479
+ function createDedupePlan({ schemaNodes, operationNodes, schemaNames, circularNames }) {
2480
+ const circularSchemas = new Set(circularNames);
2481
+ const usedNames = new Set(schemaNames);
2482
+ return ast.buildDedupePlan([...schemaNodes, ...operationNodes], {
2483
+ isCandidate: (node) => {
2484
+ if (node.type === "enum") return true;
2485
+ if (node.type !== "object") return false;
2486
+ if (node.name && circularSchemas.has(node.name)) return false;
2487
+ return !containsCircularRef(node, { circularSchemas });
2488
+ },
2489
+ nameFor: (node) => {
2490
+ const base = node.name;
2491
+ if (!base) return null;
2492
+ let name = base;
2493
+ let counter = 2;
2494
+ while (usedNames.has(name)) name = `${base}${counter++}`;
2495
+ usedNames.add(name);
2496
+ return name;
2497
+ },
2498
+ refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`
2499
+ });
2500
+ }
2501
+ /**
2502
+ * Reads the server URL from the document's `servers` array at `serverIndex`,
2503
+ * interpolating any `serverVariables` into the URL template.
2504
+ *
2505
+ * Returns `null` when `serverIndex` is omitted or out of range.
2506
+ *
2507
+ * @example Resolve the first server
2508
+ * `resolveBaseUrl({ document, serverIndex: 0 })`
2509
+ *
2510
+ * @example Override a path variable
2511
+ * `resolveBaseUrl({ document, serverIndex: 0, serverVariables: { version: 'v2' } })`
2512
+ */
2513
+ function resolveBaseUrl({ document, serverIndex, serverVariables }) {
2514
+ const server = serverIndex !== void 0 ? document.servers?.at(serverIndex) : void 0;
2515
+ return server?.url ? resolveServerUrl(server, serverVariables) : null;
2516
+ }
1828
2517
  /**
1829
- * Parses an OpenAPI specification into Kubb's universal `InputNode` AST.
2518
+ * Parses every schema once to build the lookup structures that streaming needs upfront.
2519
+ *
2520
+ * Three things happen in this single pass:
2521
+ * - `refAliasMap` records schemas that are pure `$ref` aliases so the streaming pass can inline them.
2522
+ * - `enumNames` collects the names of every enum schema so plugins skip re-scanning the stream.
2523
+ * - `circularNames` runs cycle detection, which requires all nodes in memory simultaneously.
2524
+ * The `allNodes` array is local and drops out of scope as soon as this function returns.
1830
2525
  *
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.
2526
+ * After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
2527
+ * Both are proportional to the number of aliases or discriminator parents, not total schema count.
1834
2528
  *
1835
- * Returns the AST root and a `nameMapping` for resolving schema references.
2529
+ * Each schema is parsed again during the streaming pass. This is intentional.
2530
+ * Holding the parsed nodes in memory here would defeat the streaming memory benefit.
1836
2531
  *
1837
2532
  * @example
1838
2533
  * ```ts
1839
- * import { parseOas } from '@kubb/adapter-oas'
2534
+ * const { refAliasMap, enumNames, circularNames } = preScan({
2535
+ * schemas,
2536
+ * parseSchema,
2537
+ * parserOptions,
2538
+ * discriminator: 'strict',
2539
+ * })
2540
+ * ```
2541
+ */
2542
+ function preScan({ schemas, parseSchema, parseOperation, document, parserOptions, discriminator, dedupe }) {
2543
+ const allNodes = [];
2544
+ const refAliasMap = /* @__PURE__ */ new Map();
2545
+ const enumNames = [];
2546
+ const discriminatorParentNodes = [];
2547
+ for (const [name, schema] of Object.entries(schemas)) {
2548
+ const node = parseSchema({
2549
+ schema,
2550
+ name
2551
+ }, parserOptions);
2552
+ allNodes.push(node);
2553
+ reportSchemaDiagnostics({
2554
+ node,
2555
+ name
2556
+ });
2557
+ if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2558
+ if (ast.narrowSchema(node, ast.schemaTypes.enum) && node.name) enumNames.push(node.name);
2559
+ if (discriminator === "inherit" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2560
+ }
2561
+ const circularNames = [...findCircularSchemas(allNodes)];
2562
+ const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
2563
+ let dedupePlan = null;
2564
+ if (dedupe) {
2565
+ const operationNodes = [];
2566
+ for (const operation of getOperations(document)) {
2567
+ const operationNode = parseOperation(parserOptions, operation);
2568
+ if (operationNode) operationNodes.push(operationNode);
2569
+ }
2570
+ dedupePlan = createDedupePlan({
2571
+ schemaNodes: allNodes,
2572
+ operationNodes,
2573
+ schemaNames: Object.keys(schemas),
2574
+ circularNames
2575
+ });
2576
+ for (const definition of dedupePlan.hoisted) if (definition.type === "enum" && definition.name) enumNames.push(definition.name);
2577
+ }
2578
+ const aliasNames = dedupePlan?.aliasNames;
2579
+ return {
2580
+ refAliasMap,
2581
+ enumNames: aliasNames && aliasNames.size > 0 ? enumNames.filter((name) => !aliasNames.has(name)) : enumNames,
2582
+ circularNames,
2583
+ discriminatorChildMap,
2584
+ dedupePlan
2585
+ };
2586
+ }
2587
+ /**
2588
+ * Creates a lazy `InputNode<true>` from already-resolved adapter state.
2589
+ *
2590
+ * The schema and operation iterables each start a fresh parse pass on every
2591
+ * `[Symbol.asyncIterator]()` call. This lets multiple plugins consume the same
2592
+ * stream object independently without sharing a cursor or holding all nodes in memory.
1840
2593
  *
1841
- * const document = await parseFromConfig(config)
1842
- * const { root, nameMapping } = parseOas(document, { dateType: 'date', contentType: 'application/json' })
2594
+ * Ref aliases in `refAliasMap` are inlined during iteration: an alias entry is replaced
2595
+ * with its target's parsed node (but keeps the alias name) so plugins never receive bare `ref` nodes.
2596
+ *
2597
+ * @example
2598
+ * ```ts
2599
+ * const streamNode = createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, meta })
2600
+ * for await (const schema of streamNode.schemas) {
2601
+ * // each call to for-await restarts from the first schema
2602
+ * }
1843
2603
  * ```
1844
2604
  */
1845
- function parseOas(document, options = {}) {
1846
- const { contentType, ...parserOptions } = options;
1847
- const mergedOptions = {
1848
- ...DEFAULT_PARSER_OPTIONS,
1849
- ...parserOptions
2605
+ function createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, dedupePlan, meta }) {
2606
+ const rewriteTopLevelSchema = (node) => {
2607
+ if (!dedupePlan) return node;
2608
+ const canonical = dedupePlan.canonicalBySignature.get(ast.signatureOf(node));
2609
+ if (canonical && canonical.name !== node.name) return ast.factory.createSchema({
2610
+ type: "ref",
2611
+ name: node.name ?? null,
2612
+ ref: canonical.ref,
2613
+ description: node.description,
2614
+ deprecated: node.deprecated
2615
+ });
2616
+ return ast.applyDedupe(node, dedupePlan, true);
1850
2617
  };
1851
- const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType });
1852
- const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({
1853
- document,
1854
- contentType
2618
+ const schemasIterable = { [Symbol.asyncIterator]() {
2619
+ return (async function* () {
2620
+ if (dedupePlan) for (const definition of dedupePlan.hoisted) yield definition;
2621
+ for (const [name, schema] of Object.entries(schemas)) {
2622
+ if (dedupePlan?.aliasNames.has(name)) continue;
2623
+ const alias = refAliasMap.get(name);
2624
+ if (alias?.name && schemas[alias.name]) {
2625
+ yield rewriteTopLevelSchema({
2626
+ ...parseSchema({
2627
+ schema: schemas[alias.name],
2628
+ name: alias.name
2629
+ }, parserOptions),
2630
+ name
2631
+ });
2632
+ continue;
2633
+ }
2634
+ const parsed = parseSchema({
2635
+ schema,
2636
+ name
2637
+ }, parserOptions);
2638
+ yield rewriteTopLevelSchema(discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)) : parsed);
2639
+ }
2640
+ })();
2641
+ } };
2642
+ const operationsIterable = { [Symbol.asyncIterator]() {
2643
+ return (async function* () {
2644
+ for (const operation of getOperations(document)) {
2645
+ const node = parseOperation(parserOptions, operation);
2646
+ if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan) : node;
2647
+ }
2648
+ })();
2649
+ } };
2650
+ return ast.factory.createInput({
2651
+ stream: true,
2652
+ schemas: schemasIterable,
2653
+ operations: operationsIterable,
2654
+ meta
1855
2655
  });
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));
1862
- return {
1863
- root: ast.createInput({
1864
- schemas,
1865
- operations
1866
- }),
1867
- nameMapping
1868
- };
1869
2656
  }
1870
2657
  //#endregion
1871
2658
  //#region src/adapter.ts
1872
2659
  /**
1873
- * Stable string identifier for the OAS adapter used in Kubb's adapter registry.
2660
+ * Canonical adapter name for `@kubb/adapter-oas`. Used for driver lookups.
1874
2661
  */
1875
2662
  const adapterOasName = "oas";
1876
2663
  /**
1877
- * Creates the default OpenAPI / Swagger adapter for Kubb.
2664
+ * Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.1 specifications. Reads the
2665
+ * file at `input.path`, validates it, resolves the base URL, and converts every
2666
+ * schema and operation into the universal AST that every downstream plugin
2667
+ * consumes.
1878
2668
  *
1879
- * Parses the spec, optionally validates it, resolves the base URL, and converts
1880
- * everything into an `InputNode` that downstream plugins consume.
2669
+ * Configure once on `defineConfig`. The adapter's choices (date representation,
2670
+ * integer width, server URL) apply to every plugin in the build.
1881
2671
  *
1882
2672
  * @example
1883
2673
  * ```ts
@@ -1886,17 +2676,109 @@ const adapterOasName = "oas";
1886
2676
  * import { pluginTs } from '@kubb/plugin-ts'
1887
2677
  *
1888
2678
  * export default defineConfig({
1889
- * adapter: adapterOas({ dateType: 'date', serverIndex: 0 }),
1890
- * input: { path: './openapi.yaml' },
2679
+ * input: { path: './petStore.yaml' },
2680
+ * output: { path: './src/gen' },
2681
+ * adapter: adapterOas({
2682
+ * serverIndex: 0,
2683
+ * discriminator: 'inherit',
2684
+ * dateType: 'date',
2685
+ * }),
1891
2686
  * plugins: [pluginTs()],
1892
2687
  * })
1893
2688
  * ```
1894
2689
  */
1895
2690
  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;
2691
+ 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;
2692
+ const parserOptions = {
2693
+ ...DEFAULT_PARSER_OPTIONS,
2694
+ dateType,
2695
+ integerType,
2696
+ unknownType,
2697
+ emptySchemaType,
2698
+ enumSuffix
2699
+ };
1897
2700
  let nameMapping = /* @__PURE__ */ new Map();
1898
- let parsedDocument;
1899
- let inputNode;
2701
+ let parsedDocument = null;
2702
+ const documentCache = /* @__PURE__ */ new WeakMap();
2703
+ const schemasCache = /* @__PURE__ */ new WeakMap();
2704
+ const schemaParserCache = /* @__PURE__ */ new WeakMap();
2705
+ const preScanCache = /* @__PURE__ */ new WeakMap();
2706
+ function ensureDocument(source) {
2707
+ const cached = documentCache.get(source);
2708
+ if (cached) return cached;
2709
+ const promise = (async () => {
2710
+ const fresh = await parseFromConfig(source);
2711
+ if (validate) await validateDocument(fresh);
2712
+ parsedDocument = fresh;
2713
+ return fresh;
2714
+ })();
2715
+ documentCache.set(source, promise);
2716
+ return promise;
2717
+ }
2718
+ function ensureSchemas(document) {
2719
+ const cached = schemasCache.get(document);
2720
+ if (cached) return cached;
2721
+ const promise = Promise.resolve().then(() => {
2722
+ const result = getSchemas(document, { contentType });
2723
+ nameMapping = result.nameMapping;
2724
+ return result.schemas;
2725
+ });
2726
+ schemasCache.set(document, promise);
2727
+ return promise;
2728
+ }
2729
+ function ensureSchemaParser(document) {
2730
+ const cached = schemaParserCache.get(document);
2731
+ if (cached) return cached;
2732
+ const parser = createSchemaParser({
2733
+ document,
2734
+ contentType
2735
+ });
2736
+ schemaParserCache.set(document, parser);
2737
+ return parser;
2738
+ }
2739
+ function ensurePreScan(document, schemas, parseSchema, parseOperation) {
2740
+ const cached = preScanCache.get(document);
2741
+ if (cached) return cached;
2742
+ const result = preScan({
2743
+ schemas,
2744
+ parseSchema,
2745
+ parseOperation,
2746
+ document,
2747
+ parserOptions,
2748
+ discriminator,
2749
+ dedupe
2750
+ });
2751
+ preScanCache.set(document, result);
2752
+ return result;
2753
+ }
2754
+ async function createStream(source) {
2755
+ const document = await ensureDocument(source);
2756
+ const schemas = await ensureSchemas(document);
2757
+ const { parseSchema, parseOperation } = ensureSchemaParser(document);
2758
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation);
2759
+ return createInputStream({
2760
+ schemas,
2761
+ parseSchema,
2762
+ parseOperation,
2763
+ document,
2764
+ parserOptions,
2765
+ refAliasMap,
2766
+ discriminatorChildMap,
2767
+ dedupePlan,
2768
+ meta: {
2769
+ title: document.info?.title,
2770
+ description: document.info?.description,
2771
+ version: document.info?.version,
2772
+ baseURL: resolveBaseUrl({
2773
+ document,
2774
+ serverIndex,
2775
+ serverVariables
2776
+ }),
2777
+ circularNames,
2778
+ enumNames
2779
+ }
2780
+ });
2781
+ }
1900
2782
  return {
1901
2783
  name: "oas",
1902
2784
  get options() {
@@ -1906,6 +2788,7 @@ const adapterOas = createAdapter((options) => {
1906
2788
  serverIndex,
1907
2789
  serverVariables,
1908
2790
  discriminator,
2791
+ dedupe,
1909
2792
  dateType,
1910
2793
  integerType,
1911
2794
  unknownType,
@@ -1917,68 +2800,36 @@ const adapterOas = createAdapter((options) => {
1917
2800
  get document() {
1918
2801
  return parsedDocument;
1919
2802
  },
1920
- get inputNode() {
1921
- return inputNode;
1922
- },
1923
2803
  async validate(input, options) {
2804
+ await assertInputExists(input);
1924
2805
  await validateDocument(await parseDocument(input), options);
1925
2806
  },
1926
2807
  getImports(node, resolve) {
1927
- return ast.collectImports({
1928
- node,
1929
- nameMapping,
1930
- resolve: (schemaName) => {
1931
- const result = resolve(schemaName);
1932
- if (!result) return;
1933
- return ast.createImport({
1934
- name: [result.name],
1935
- path: result.path
1936
- });
1937
- }
1938
- });
2808
+ return collect(node, { schema(schemaNode) {
2809
+ const schemaRef = narrowSchema(schemaNode, "ref");
2810
+ if (!schemaRef?.ref) return null;
2811
+ const rawName = extractRefName(schemaRef.ref);
2812
+ const result = resolve(nameMapping.get(rawName) ?? rawName);
2813
+ if (!result) return null;
2814
+ return ast.factory.createImport({
2815
+ name: [result.name],
2816
+ path: result.path
2817
+ });
2818
+ } });
1939
2819
  },
1940
2820
  async parse(source) {
1941
- const document = await parseFromConfig(source);
1942
- if (validate) await validateDocument(document);
1943
- const server = serverIndex !== void 0 ? document.servers?.at(serverIndex) : void 0;
1944
- const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : void 0;
1945
- const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
1946
- contentType,
1947
- dateType,
1948
- integerType,
1949
- unknownType,
1950
- emptySchemaType,
1951
- enumSuffix
1952
- });
1953
- const node = discriminator === "inherit" ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot;
1954
- nameMapping = parsedNameMapping;
1955
- parsedDocument = document;
1956
- inputNode = ast.createInput({
1957
- ...node,
1958
- meta: {
1959
- title: document.info?.title,
1960
- description: document.info?.description,
1961
- version: document.info?.version,
1962
- baseURL
1963
- }
2821
+ const streamNode = await createStream(source);
2822
+ const [schemas, operations] = await Promise.all([Array.fromAsync(streamNode.schemas), Array.fromAsync(streamNode.operations)]);
2823
+ return ast.factory.createInput({
2824
+ schemas,
2825
+ operations,
2826
+ meta: streamNode.meta
1964
2827
  });
1965
- return inputNode;
1966
- }
2828
+ },
2829
+ stream: createStream
1967
2830
  };
1968
2831
  });
1969
2832
  //#endregion
1970
- //#region src/types.ts
1971
- /**
1972
- * Maps uppercase HTTP method names to lowercase for backwards compatibility.
1973
- *
1974
- * @example
1975
- * ```ts
1976
- * HttpMethods['GET'] // 'get'
1977
- * HttpMethods['POST'] // 'post'
1978
- * ```
1979
- */
1980
- const HttpMethods = Object.fromEntries(Object.entries(ast.httpMethods).map(([lower, upper]) => [upper, lower]));
1981
- //#endregion
1982
- export { HttpMethods, adapterOas, adapterOasName, mergeDocuments };
2833
+ export { adapterOas, adapterOasName };
1983
2834
 
1984
2835
  //# sourceMappingURL=index.js.map