@kubb/adapter-oas 5.0.0-beta.8 → 5.0.0-beta.81

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,22 +1,21 @@
1
- import "./chunk--u3MIqq1.js";
2
- import { ast, createAdapter } from "@kubb/core";
1
+ import "./rolldown-runtime-C0LytTxp.js";
2
+ import { ast, childName, collect, enumPropName, extractRefName, findCircularSchemas, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy, narrowSchema } from "@kubb/ast";
3
+ import { Diagnostics, createAdapter } from "@kubb/core";
3
4
  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";
5
+ import { access, readFile } from "node:fs/promises";
6
+ import { compileErrors, validate } from "@readme/openapi-parser";
7
+ import { upgrade } from "@scalar/openapi-upgrader";
8
+ import { parse } from "yaml";
9
+ import { bundle } from "api-ref-bundler";
10
10
  //#region src/constants.ts
11
11
  /**
12
12
  * Default parser options applied when no explicit options are provided.
13
13
  *
14
14
  * @example
15
15
  * ```ts
16
- * import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
16
+ * import { DEFAULT_PARSER_OPTIONS, parseOas } from '@kubb/adapter-oas'
17
17
  *
18
- * const parser = createOasParser(oas)
19
- * const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
18
+ * const { root } = parseOas(document, { ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
20
19
  * ```
21
20
  */
22
21
  const DEFAULT_PARSER_OPTIONS = {
@@ -38,17 +37,19 @@ const DEFAULT_PARSER_OPTIONS = {
38
37
  */
39
38
  const SCHEMA_REF_PREFIX = "#/components/schemas/";
40
39
  /**
41
- * OpenAPI version string written into the stub document created during multi-spec merges.
40
+ * HTTP methods that count as operations on an OpenAPI path item. Other keys
41
+ * (`parameters`, `summary`, `$ref`, vendor extensions) are skipped when iterating operations.
42
42
  */
43
- const MERGE_OPENAPI_VERSION = "3.0.0";
44
- /**
45
- * Fallback `info.title` placed in the stub document when merging multiple API files.
46
- */
47
- const MERGE_DEFAULT_TITLE = "Merged API";
48
- /**
49
- * Fallback `info.version` placed in the stub document when merging multiple API files.
50
- */
51
- const MERGE_DEFAULT_VERSION = "1.0.0";
43
+ const SUPPORTED_METHODS = /* @__PURE__ */ new Set([
44
+ "get",
45
+ "put",
46
+ "post",
47
+ "delete",
48
+ "options",
49
+ "head",
50
+ "patch",
51
+ "trace"
52
+ ]);
52
53
  /**
53
54
  * Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
54
55
  *
@@ -63,7 +64,7 @@ const MERGE_DEFAULT_VERSION = "1.0.0";
63
64
  * // true when fragment has e.g. 'properties' or 'oneOf'
64
65
  * ```
65
66
  */
66
- const structuralKeys = new Set([
67
+ const structuralKeys = /* @__PURE__ */ new Set([
67
68
  "properties",
68
69
  "items",
69
70
  "additionalProperties",
@@ -73,12 +74,24 @@ const structuralKeys = new Set([
73
74
  "not"
74
75
  ]);
75
76
  /**
77
+ * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
78
+ * `int64` and the date/time family. Keep this in sync with the `convertFormat`
79
+ * special-cases in `parser.ts`. `isHandledFormat` reads it so the
80
+ * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
81
+ */
82
+ const specialCasedFormats = /* @__PURE__ */ new Set([
83
+ "int64",
84
+ "date-time",
85
+ "date",
86
+ "time"
87
+ ]);
88
+ /**
76
89
  * Static map from OAS `format` strings to Kubb `SchemaType` values.
77
90
  *
78
91
  * Only formats whose AST type differs from the OAS `type` field appear here.
79
- * 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
81
- * `idn-hostname` map to `'url'` as the closest generic string-format type.
92
+ * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled
93
+ * separately in the parser. `ipv4` and `ipv6` map to their own dedicated schema types. `hostname`
94
+ * and `idn-hostname` map to `'url'` as the closest generic string-format type.
82
95
  *
83
96
  * @example
84
97
  * ```ts
@@ -118,95 +131,16 @@ const formatMap = {
118
131
  */
119
132
  const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
120
133
  /**
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()`.
123
- */
124
- const typeOptionMap = new Map([
125
- ["any", ast.schemaTypes.any],
126
- ["unknown", ast.schemaTypes.unknown],
127
- ["void", ast.schemaTypes.void]
128
- ]);
129
- //#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.
134
+ * Vendor extension keys that attach human-readable descriptions to enum values, checked in priority order.
139
135
  *
140
136
  * @example
141
137
  * ```ts
142
- * const { root } = parseOas(document, options)
143
- * const next = applyDiscriminatorInheritance(root)
138
+ * import { enumDescriptionKeys } from '@kubb/adapter-oas'
139
+ *
140
+ * const key = enumDescriptionKeys.find((k) => k in schema) // 'x-enumDescriptions' | 'x-enum-descriptions' | undefined
144
141
  * ```
145
142
  */
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
- }
143
+ const enumDescriptionKeys = ["x-enumDescriptions", "x-enum-descriptions"];
210
144
  //#endregion
211
145
  //#region ../../internals/utils/src/casing.ts
212
146
  /**
@@ -219,357 +153,238 @@ function applyDiscriminatorInheritance(root) {
219
153
  function toCamelOrPascal(text, pascal) {
220
154
  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
155
  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);
156
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
224
157
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
225
158
  }
226
159
  /**
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.
230
- *
231
- * Only splits on dots followed by a letter so that version numbers
232
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
233
- *
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.
239
- */
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("/");
243
- }
244
- /**
245
- * Converts `text` to camelCase.
246
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
247
- *
248
- * @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);
258
- }
259
- /**
260
160
  * Converts `text` to PascalCase.
261
- * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
262
161
  *
263
- * @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);
273
- }
274
- //#endregion
275
- //#region ../../internals/utils/src/object.ts
276
- /**
277
- * Returns `true` when `value` is a plain (non-null, non-array) object.
278
- *
279
- * @example
280
- * ```ts
281
- * isPlainObject({}) // true
282
- * isPlainObject([]) // false
283
- * isPlainObject(null) // false
284
- * ```
285
- */
286
- function isPlainObject(value) {
287
- return typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
288
- }
289
- /**
290
- * Recursively merges `source` into `target`, combining nested plain objects.
291
- * Arrays and non-object values from `source` override the corresponding values in `target`.
292
- *
293
- * @example
294
- * ```ts
295
- * mergeDeep({ a: { x: 1 } }, { a: { y: 2 } })
296
- * // { a: { x: 1, y: 2 } }
297
- * ```
298
- */
299
- function mergeDeep(target, source) {
300
- const result = { ...target };
301
- for (const key of Object.keys(source)) {
302
- const sv = source[key];
303
- const tv = result[key];
304
- result[key] = sv !== null && typeof sv === "object" && !Array.isArray(sv) && tv !== null && typeof tv === "object" && !Array.isArray(tv) ? mergeDeep(tv, sv) : sv;
305
- }
306
- return result;
307
- }
308
- //#endregion
309
- //#region ../../internals/utils/src/reserved.ts
310
- /**
311
- * JavaScript and Java reserved words.
312
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
313
- */
314
- const reservedWords = new Set([
315
- "abstract",
316
- "arguments",
317
- "boolean",
318
- "break",
319
- "byte",
320
- "case",
321
- "catch",
322
- "char",
323
- "class",
324
- "const",
325
- "continue",
326
- "debugger",
327
- "default",
328
- "delete",
329
- "do",
330
- "double",
331
- "else",
332
- "enum",
333
- "eval",
334
- "export",
335
- "extends",
336
- "false",
337
- "final",
338
- "finally",
339
- "float",
340
- "for",
341
- "function",
342
- "goto",
343
- "if",
344
- "implements",
345
- "import",
346
- "in",
347
- "instanceof",
348
- "int",
349
- "interface",
350
- "let",
351
- "long",
352
- "native",
353
- "new",
354
- "null",
355
- "package",
356
- "private",
357
- "protected",
358
- "public",
359
- "return",
360
- "short",
361
- "static",
362
- "super",
363
- "switch",
364
- "synchronized",
365
- "this",
366
- "throw",
367
- "throws",
368
- "transient",
369
- "true",
370
- "try",
371
- "typeof",
372
- "var",
373
- "void",
374
- "volatile",
375
- "while",
376
- "with",
377
- "yield",
378
- "Array",
379
- "Date",
380
- "hasOwnProperty",
381
- "Infinity",
382
- "isFinite",
383
- "isNaN",
384
- "isPrototypeOf",
385
- "length",
386
- "Math",
387
- "name",
388
- "NaN",
389
- "Number",
390
- "Object",
391
- "prototype",
392
- "String",
393
- "toString",
394
- "undefined",
395
- "valueOf"
396
- ]);
397
- /**
398
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
162
+ * @example Word boundaries
163
+ * `pascalCase('hello-world') // 'HelloWorld'`
399
164
  *
400
- * @example
401
- * ```ts
402
- * isValidVarName('status') // true
403
- * isValidVarName('class') // false (reserved word)
404
- * isValidVarName('42foo') // false (starts with digit)
405
- * ```
165
+ * @example With a suffix
166
+ * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
406
167
  */
407
- function isValidVarName(name) {
408
- if (!name || reservedWords.has(name)) return false;
409
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
168
+ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
169
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
410
170
  }
411
171
  //#endregion
412
- //#region ../../internals/utils/src/urlPath.ts
172
+ //#region ../../internals/utils/src/runtime.ts
413
173
  /**
414
- * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
174
+ * Detects the JavaScript runtime executing the current process and exposes its name and version.
415
175
  *
416
- * @example
417
- * const p = new URLPath('/pet/{petId}')
418
- * p.URL // '/pet/:petId'
419
- * p.template // '`/pet/${petId}`'
176
+ * Prefer the shared {@link runtime} instance over constructing your own.
420
177
  */
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;
430
- }
431
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
432
- *
433
- * @example
434
- * ```ts
435
- * new URLPath('/pet/{petId}').URL // '/pet/:petId'
436
- * ```
437
- */
438
- get URL() {
439
- return this.toURLPath();
440
- }
441
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
442
- *
443
- * @example
444
- * ```ts
445
- * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
446
- * new URLPath('/pet/{petId}').isURL // false
447
- * ```
448
- */
449
- get isURL() {
450
- try {
451
- return !!new URL(this.path).href;
452
- } catch {
453
- return false;
454
- }
455
- }
178
+ var Runtime = class {
456
179
  /**
457
- * Converts the OpenAPI path to a TypeScript template literal string.
180
+ * `true` when the current process is running under Bun.
458
181
  *
459
- * @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.
182
+ * Detection keys off the global `Bun` object rather than `process.versions`,
183
+ * because Bun polyfills `process.versions.node` for Node compatibility and would
184
+ * otherwise look like Node.
467
185
  *
468
186
  * @example
469
187
  * ```ts
470
- * new URLPath('/pet/{petId}').object
471
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
188
+ * if (runtime.isBun) {
189
+ * await Bun.write(path, data)
190
+ * }
472
191
  * ```
473
192
  */
474
- get object() {
475
- return this.toObject();
476
- }
477
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
478
- *
479
- * @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;
193
+ get isBun() {
194
+ return typeof Bun !== "undefined";
491
195
  }
492
196
  /**
493
- * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
197
+ * `true` when the current process is running under Deno.
494
198
  */
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 } = {}) {
502
- const object = {
503
- url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
504
- params: this.getParams()
505
- };
506
- if (stringify) {
507
- if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
508
- if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
509
- return `{ url: '${object.url}' }`;
510
- }
511
- return object;
199
+ get isDeno() {
200
+ return typeof globalThis.Deno !== "undefined";
512
201
  }
513
202
  /**
514
- * Converts the OpenAPI path to a TypeScript template literal string.
515
- * An optional `replacer` can transform each extracted parameter name before interpolation.
203
+ * `true` when the current process is running under Node.
516
204
  *
517
- * @example
518
- * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
205
+ * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
519
206
  */
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("")}\``;
207
+ get isNode() {
208
+ return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
526
209
  }
527
210
  /**
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.
211
+ * Name of the runtime executing the current process.
531
212
  *
532
213
  * @example
533
214
  * ```ts
534
- * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
535
- * // { petId: 'petId', tagId: 'tagId' }
215
+ * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
536
216
  * ```
537
217
  */
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;
218
+ get name() {
219
+ if (this.isBun) return "bun";
220
+ if (this.isDeno) return "deno";
221
+ return "node";
545
222
  }
546
- /** Converts the OpenAPI path to Express-style colon syntax.
223
+ /**
224
+ * Version of the active runtime, or an empty string when it cannot be read.
547
225
  *
548
226
  * @example
549
227
  * ```ts
550
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
228
+ * runtime.version // '1.3.11' under Bun, '22.22.2' under Node
551
229
  * ```
552
230
  */
553
- toURLPath() {
554
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
231
+ get version() {
232
+ if (this.isBun) return process.versions.bun ?? "";
233
+ if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
234
+ return process.versions?.node ?? "";
555
235
  }
556
236
  };
237
+ /**
238
+ * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
239
+ */
240
+ const runtime = new Runtime();
557
241
  //#endregion
558
- //#region src/guards.ts
242
+ //#region ../../internals/utils/src/fs.ts
559
243
  /**
560
- * Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
244
+ * Resolves to `true` when the file or directory at `path` exists.
245
+ * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
561
246
  *
562
247
  * @example
563
248
  * ```ts
564
- * if (isOpenApiV2Document(doc)) {
565
- * // doc is OpenAPIV2.Document
249
+ * if (await exists('./kubb.config.ts')) {
250
+ * const content = await read('./kubb.config.ts')
566
251
  * }
567
252
  * ```
568
253
  */
569
- function isOpenApiV2Document(doc) {
570
- return !!doc && isPlainObject(doc) && !("openapi" in doc);
254
+ async function exists(path) {
255
+ if (runtime.isBun) return Bun.file(path).exists();
256
+ return access(path).then(() => true, () => false);
257
+ }
258
+ /**
259
+ * Reads the file at `path` as a UTF-8 string.
260
+ * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * const source = await read('./src/Pet.ts')
265
+ * ```
266
+ */
267
+ async function read(path) {
268
+ if (runtime.isBun) return Bun.file(path).text();
269
+ return readFile(path, { encoding: "utf8" });
270
+ }
271
+ //#endregion
272
+ //#region src/bundler.ts
273
+ const urlRegExp = /^https?:\/+/i;
274
+ async function readSource(sourcePath) {
275
+ if (urlRegExp.test(sourcePath)) {
276
+ const url = new URL(sourcePath);
277
+ const response = await fetch(url);
278
+ if (!response.ok) throw new Error(`Cannot fetch the OAS document at ${url.href} (HTTP ${response.status})`);
279
+ return response.text();
280
+ }
281
+ return read(sourcePath);
282
+ }
283
+ async function resolveSource(sourcePath) {
284
+ const data = await readSource(sourcePath);
285
+ if (sourcePath.toLowerCase().endsWith(".md")) return data;
286
+ return parse(data);
287
+ }
288
+ /**
289
+ * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
290
+ *
291
+ * External file schemas are hoisted into named `components.schemas` entries, so a property
292
+ * pointing at `./schemas/User.yaml` ends up referencing `#/components/schemas/User`. Generators
293
+ * can then emit a named type with an import instead of inlining the shape. Sources are read with
294
+ * the Bun-aware `read` util for local YAML and JSON files, and with `fetch` for HTTP(S) URLs.
295
+ *
296
+ * @example Local file
297
+ * `const document = await bundleDocument('./openapi.yaml')`
298
+ *
299
+ * @example Remote URL
300
+ * `const document = await bundleDocument('https://example.com/openapi.yaml')`
301
+ */
302
+ async function bundleDocument(pathOrUrl) {
303
+ const cache = /* @__PURE__ */ new Map();
304
+ const resolver = (sourcePath) => {
305
+ const key = urlRegExp.test(sourcePath) ? new URL(sourcePath).href : sourcePath;
306
+ const cached = cache.get(key);
307
+ if (cached) return cached;
308
+ const result = resolveSource(sourcePath);
309
+ cache.set(key, result);
310
+ return result;
311
+ };
312
+ await resolver(pathOrUrl);
313
+ return await bundle(pathOrUrl, resolver);
314
+ }
315
+ //#endregion
316
+ //#region src/factory.ts
317
+ /**
318
+ * Loads and bundles an OpenAPI document, returning the raw `Document`.
319
+ *
320
+ * A string is a file path or URL: it is bundled via `api-ref-bundler`, hoisting external file
321
+ * schemas into named `components.schemas` entries so generators can emit named types and imports.
322
+ * An object is treated as an already-parsed document. Swagger 2.0 and OpenAPI 3.0 documents are
323
+ * up-converted to OpenAPI 3.1 via `@scalar/openapi-upgrader`.
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * const document = await parseDocument('./openapi.yaml')
328
+ * const document = await parseDocument(rawDocumentObject)
329
+ * ```
330
+ */
331
+ async function parseDocument(pathOrApi) {
332
+ if (typeof pathOrApi === "string") return parseDocument(await bundleDocument(pathOrApi));
333
+ return upgrade(pathOrApi, "3.1");
571
334
  }
572
335
  /**
336
+ * Creates a `Document` from an `AdapterSource`.
337
+ *
338
+ * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
339
+ * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
340
+ *
341
+ * @example
342
+ * ```ts
343
+ * const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
344
+ * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
345
+ * ```
346
+ */
347
+ async function parseFromConfig(source) {
348
+ if (source.type === "data") return parseDocument(typeof source.data === "string" ? parse(source.data) : structuredClone(source.data));
349
+ if (URL.canParse(source.path)) return parseDocument(source.path);
350
+ const resolved = path.resolve(path.dirname(source.path), source.path);
351
+ await assertInputExists(resolved);
352
+ return parseDocument(resolved);
353
+ }
354
+ /**
355
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
356
+ * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
357
+ * its parse error instead.
358
+ */
359
+ async function assertInputExists(input) {
360
+ if (URL.canParse(input)) return;
361
+ if (!await exists(input)) throw new Diagnostics.Error({
362
+ code: Diagnostics.code.inputNotFound,
363
+ severity: "error",
364
+ message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
365
+ help: "Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.",
366
+ location: { kind: "config" }
367
+ });
368
+ }
369
+ /**
370
+ * Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
371
+ *
372
+ * @example
373
+ * ```ts
374
+ * await validateDocument(document)
375
+ * ```
376
+ */
377
+ async function validateDocument(document, { throwOnError = false } = {}) {
378
+ try {
379
+ const result = await validate(structuredClone(document), { validate: { errors: { colorize: true } } });
380
+ if (!result.valid) throw new Error(compileErrors(result));
381
+ } catch (error) {
382
+ if (throwOnError) throw error;
383
+ }
384
+ }
385
+ //#endregion
386
+ //#region src/guards.ts
387
+ /**
573
388
  * Returns `true` when a schema should be treated as nullable.
574
389
  *
575
390
  * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
@@ -615,158 +430,369 @@ function isDiscriminator(obj) {
615
430
  return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
616
431
  }
617
432
  //#endregion
618
- //#region src/factory.ts
433
+ //#region src/refs.ts
434
+ const _refCache = /* @__PURE__ */ new WeakMap();
619
435
  /**
620
- * Loads and dereferences an OpenAPI document, returning the raw `Document`.
436
+ * Resolves a local JSON pointer reference from a document.
621
437
  *
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`.
438
+ * Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
439
+ * resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
440
+ * build there is no sink to collect it, so it throws instead.
625
441
  *
626
442
  * @example
627
443
  * ```ts
628
- * const document = await parseDocument('./openapi.yaml')
629
- * const document = await parse(rawDocumentObject, { canBundle: false })
444
+ * resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
630
445
  * ```
631
446
  */
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();
645
- if (isOpenApiV2Document(document)) {
646
- const { openapi } = await swagger2openapi.convertObj(document, { anchors: true });
647
- return openapi;
447
+ function resolveRef(document, $ref) {
448
+ const origRef = $ref;
449
+ $ref = $ref.trim();
450
+ if ($ref === "") return null;
451
+ if (!$ref.startsWith("#")) return null;
452
+ $ref = globalThis.decodeURIComponent($ref.substring(1));
453
+ let docCache = _refCache.get(document);
454
+ if (!docCache) {
455
+ docCache = /* @__PURE__ */ new Map();
456
+ _refCache.set(document, docCache);
457
+ }
458
+ if (docCache.has($ref)) return docCache.get($ref);
459
+ const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
460
+ if (!current) {
461
+ const diagnostic = {
462
+ code: Diagnostics.code.refNotFound,
463
+ severity: "error",
464
+ message: `Could not find a definition for ${origRef}.`,
465
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
466
+ location: {
467
+ kind: "schema",
468
+ pointer: origRef,
469
+ ref: origRef
470
+ }
471
+ };
472
+ if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
473
+ return null;
648
474
  }
649
- return document;
475
+ docCache.set($ref, current);
476
+ return current;
650
477
  }
651
478
  /**
652
- * Deep-merges multiple OpenAPI documents into a single `Document`.
479
+ * Resolves a `$ref` object while preserving the original `$ref` field on the result.
653
480
  *
654
- * Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
655
- * Throws when the input array is empty.
481
+ * Useful for parser flows that need both dereferenced fields and pointer
482
+ * identity (for naming/import purposes). Non-reference values are returned as-is.
656
483
  *
657
484
  * @example
658
485
  * ```ts
659
- * const document = await mergeDocuments(['./pets.yaml', './orders.yaml'])
486
+ * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
487
+ * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
660
488
  * ```
661
489
  */
662
- async function mergeDocuments(pathOrApi) {
663
- const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, {
664
- enablePaths: false,
665
- canBundle: false
666
- })));
667
- if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
668
- const seed = {
669
- openapi: MERGE_OPENAPI_VERSION,
670
- info: {
671
- title: MERGE_DEFAULT_TITLE,
672
- version: MERGE_DEFAULT_VERSION
673
- },
674
- paths: {},
675
- components: { schemas: {} }
490
+ function dereferenceWithRef(document, schema) {
491
+ if (isReference(schema)) return {
492
+ ...schema,
493
+ ...resolveRef(document, schema.$ref),
494
+ $ref: schema.$ref
676
495
  };
677
- return parseDocument(documents.reduce((acc, current) => mergeDeep(acc, current), seed));
496
+ return schema;
678
497
  }
498
+ //#endregion
499
+ //#region src/dialect.ts
679
500
  /**
680
- * Creates a `Document` from an `AdapterSource`.
501
+ * The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
502
+ *
503
+ * Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
504
+ * decisions that differ between specs (nullability, `$ref`, discriminator, binary,
505
+ * ref resolution) so the converter pipeline and dispatch rules stay shared. A
506
+ * future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
507
+ * nullability, no discriminator object, binary via `contentEncoding` and reuses
508
+ * the rest unchanged.
681
509
  *
682
- * Handles all three source types:
683
- * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
684
- * - `{ type: 'paths' }` — merges multiple file paths into a single document.
685
- * - `{ type: 'data' }` — parses an inline string (YAML/JSON) or raw object.
510
+ * Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
511
+ * JSON Schema vocabulary, so the converters keep that common case.
686
512
  *
687
513
  * @example
688
514
  * ```ts
689
- * const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
690
- * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
515
+ * const parser = createSchemaParser(context) // uses oasDialect
516
+ * const parser = createSchemaParser(context, oasDialect) // explicit
691
517
  * ```
692
518
  */
693
- function parseFromConfig(source) {
694
- if (source.type === "data") {
695
- if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
696
- return parseDocument(source.data, { canBundle: false });
519
+ const oasDialect = ast.defineDialect({
520
+ name: "oas",
521
+ schema: {
522
+ isNullable,
523
+ isReference,
524
+ isDiscriminator,
525
+ isBinary: (schema) => schema.type === "string" && schema.contentMediaType === "application/octet-stream",
526
+ resolveRef
697
527
  }
698
- if (source.type === "paths") return mergeDocuments(source.paths);
699
- if (new URLPath(source.path).isURL) return parseDocument(source.path);
700
- return parseDocument(path.resolve(path.dirname(source.path), source.path));
528
+ });
529
+ //#endregion
530
+ //#region src/discriminator.ts
531
+ /**
532
+ * Maps each child schema name to its discriminator patch data by scanning the given
533
+ * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
534
+ *
535
+ * The streaming path calls this on a small pre-parsed subset of schemas (only the
536
+ * discriminator parents) rather than on all schemas at once.
537
+ */
538
+ function buildDiscriminatorChildMap(schemas) {
539
+ const childMap = /* @__PURE__ */ new Map();
540
+ for (const schema of schemas) {
541
+ let unionNode = ast.narrowSchema(schema, "union");
542
+ if (!unionNode) {
543
+ const intersectionMembers = ast.narrowSchema(schema, "intersection")?.members;
544
+ if (intersectionMembers) for (const m of intersectionMembers) {
545
+ const u = ast.narrowSchema(m, "union");
546
+ if (u) {
547
+ unionNode = u;
548
+ break;
549
+ }
550
+ }
551
+ }
552
+ if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue;
553
+ const { discriminatorPropertyName, members } = unionNode;
554
+ for (const member of members) {
555
+ const intersectionNode = ast.narrowSchema(member, "intersection");
556
+ if (!intersectionNode?.members) continue;
557
+ let refNode = null;
558
+ let objNode = null;
559
+ for (const m of intersectionNode.members) {
560
+ refNode ??= ast.narrowSchema(m, "ref");
561
+ objNode ??= ast.narrowSchema(m, "object");
562
+ }
563
+ if (!refNode?.name || !objNode) continue;
564
+ const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName);
565
+ const enumNode = prop ? ast.narrowSchema(prop.schema, "enum") : null;
566
+ if (!enumNode?.enumValues?.length) continue;
567
+ const enumValues = enumNode.enumValues.filter((v) => v !== null);
568
+ if (!enumValues.length) continue;
569
+ const existing = childMap.get(refNode.name);
570
+ if (!existing) {
571
+ childMap.set(refNode.name, {
572
+ propertyName: discriminatorPropertyName,
573
+ enumValues: [...enumValues]
574
+ });
575
+ continue;
576
+ }
577
+ existing.enumValues.push(...enumValues);
578
+ }
579
+ }
580
+ return childMap;
701
581
  }
702
582
  /**
703
- * Validates an OpenAPI document using `oas-normalize` with colorized error output.
583
+ * Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
584
+ * the discriminant property). Used by the streaming path to apply patches inline per yield
585
+ * without buffering all schemas.
586
+ */
587
+ function patchDiscriminatorNode(node, entry) {
588
+ const objectNode = ast.narrowSchema(node, "object");
589
+ if (!objectNode) return node;
590
+ const { propertyName, enumValues } = entry;
591
+ const enumSchema = ast.factory.createSchema({
592
+ type: "enum",
593
+ enumValues
594
+ });
595
+ const newProp = ast.factory.createProperty({
596
+ name: propertyName,
597
+ required: true,
598
+ schema: enumSchema
599
+ });
600
+ const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
601
+ const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
602
+ return {
603
+ ...objectNode,
604
+ properties: newProperties
605
+ };
606
+ }
607
+ /**
608
+ * Creates a single-property object schema used as a discriminator literal.
704
609
  *
705
610
  * @example
706
611
  * ```ts
707
- * await validateDocument(document)
612
+ * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
613
+ * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
708
614
  * ```
709
615
  */
710
- async function validateDocument(document, { throwOnError = false } = {}) {
711
- try {
712
- await new OASNormalize(document, {
713
- enablePaths: true,
714
- colorizeErrors: true
715
- }).validate({ parser: { validate: { errors: { colorize: true } } } });
716
- } catch (error) {
717
- if (throwOnError) throw error;
718
- }
616
+ function createDiscriminantNode({ propertyName, value }) {
617
+ return ast.factory.createSchema({
618
+ type: "object",
619
+ primitive: "object",
620
+ properties: [ast.factory.createProperty({
621
+ name: propertyName,
622
+ schema: ast.factory.createSchema({
623
+ type: "enum",
624
+ primitive: "string",
625
+ enumValues: [value]
626
+ }),
627
+ required: true
628
+ })]
629
+ });
630
+ }
631
+ /**
632
+ * Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
633
+ *
634
+ * @example
635
+ * ```ts
636
+ * findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
637
+ * ```
638
+ */
639
+ function findDiscriminator(mapping, ref) {
640
+ if (!mapping || !ref) return null;
641
+ return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
719
642
  }
720
643
  //#endregion
721
- //#region src/refs.ts
722
- const _refCache = /* @__PURE__ */ new WeakMap();
644
+ //#region src/mime.ts
723
645
  /**
724
- * Resolves a local JSON pointer reference from a document.
646
+ * MIME type fragments that mark a media type as JSON-like.
725
647
  *
726
- * Accepts `#/...` refs. Returns `null` for empty or non-local refs.
727
- * Throws when the pointer cannot be resolved.
648
+ * A content type is JSON when it contains any of these substrings. The `+json` entry catches
649
+ * structured-syntax suffixes such as `application/vnd.api+json`.
650
+ */
651
+ const jsonMimeFragments = [
652
+ "application/json",
653
+ "application/x-json",
654
+ "text/json",
655
+ "text/x-json",
656
+ "+json"
657
+ ];
658
+ /**
659
+ * Returns `true` when a media type string is JSON-like.
728
660
  *
729
661
  * @example
730
662
  * ```ts
731
- * resolveRef<SchemaObject>(document, '#/components/schemas/Pet') // SchemaObject | null
663
+ * isJsonMimeType('application/json') // true
664
+ * isJsonMimeType('application/vnd.api+json') // true
665
+ * isJsonMimeType('multipart/form-data') // false
732
666
  * ```
733
667
  */
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
- let docCache = _refCache.get(document);
741
- if (!docCache) {
742
- docCache = /* @__PURE__ */ new Map();
743
- _refCache.set(document, docCache);
668
+ function isJsonMimeType(mimeType) {
669
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
670
+ }
671
+ //#endregion
672
+ //#region src/operation.ts
673
+ /**
674
+ * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
675
+ * with no leading or trailing dash.
676
+ */
677
+ function slugify(value) {
678
+ return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
679
+ }
680
+ /**
681
+ * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
682
+ */
683
+ function getOperationId({ path, method, schema }) {
684
+ const { operationId } = schema;
685
+ if (typeof operationId === "string" && operationId.length > 0) return operationId;
686
+ return `${method}_${slugify(path).toLowerCase()}`;
687
+ }
688
+ /**
689
+ * Returns the declared response status codes, skipping `x-` extensions and non-object entries.
690
+ */
691
+ function getResponseStatusCodes({ schema }) {
692
+ const responses = schema.responses;
693
+ if (!responses || isReference(responses)) return [];
694
+ return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
695
+ }
696
+ /**
697
+ * Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
698
+ */
699
+ function getResponseByStatusCode({ document, operation, statusCode }) {
700
+ const responses = operation.schema.responses;
701
+ if (!responses || isReference(responses)) return false;
702
+ const response = responses[statusCode];
703
+ if (!response) return false;
704
+ if (isReference(response)) {
705
+ const resolved = resolveRef(document, response.$ref);
706
+ responses[statusCode] = resolved;
707
+ if (!resolved || isReference(resolved)) return false;
708
+ return resolved;
744
709
  }
745
- if (docCache.has($ref)) return docCache.get($ref);
746
- const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
747
- if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
748
- docCache.set($ref, current);
749
- return current;
710
+ return response;
750
711
  }
751
712
  /**
752
- * Resolves a `$ref` object while preserving the original `$ref` field on the result.
753
- *
754
- * Useful for parser flows that need both dereferenced fields and pointer
755
- * identity (for naming/import purposes). Non-reference values are returned as-is.
713
+ * Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
714
+ * `undefined` when the operation has no request body.
715
+ */
716
+ function getRequestBodyContent({ document, operation }) {
717
+ const { schema } = operation;
718
+ let requestBody = schema.requestBody;
719
+ if (!requestBody) return;
720
+ if (isReference(requestBody)) {
721
+ const resolved = resolveRef(document, requestBody.$ref);
722
+ schema.requestBody = resolved;
723
+ if (!resolved || isReference(resolved)) return;
724
+ requestBody = resolved;
725
+ }
726
+ return requestBody.content;
727
+ }
728
+ /**
729
+ * Returns the request body media type. With `mediaType` set, returns that entry or `false`.
730
+ * Otherwise picks the first JSON-like media type, then the first declared one, as a
731
+ * `[mediaType, object]` tuple.
732
+ */
733
+ function getRequestContent({ document, operation, mediaType }) {
734
+ const content = getRequestBodyContent({
735
+ document,
736
+ operation
737
+ });
738
+ if (!content) return false;
739
+ if (mediaType) return mediaType in content ? content[mediaType] : false;
740
+ const mediaTypes = Object.keys(content);
741
+ const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
742
+ return available ? [available, content[available]] : false;
743
+ }
744
+ /**
745
+ * Returns the primary request content type. Prefers a JSON-like media type (the last one wins
746
+ * when several are declared), then the first declared one, defaulting to `'application/json'`.
747
+ */
748
+ function getRequestContentType({ document, operation }) {
749
+ const content = getRequestBodyContent({
750
+ document,
751
+ operation
752
+ });
753
+ const mediaTypes = content ? Object.keys(content) : [];
754
+ let result = mediaTypes[0] ?? "application/json";
755
+ for (const mt of mediaTypes) if (isJsonMimeType(mt)) result = mt;
756
+ return result;
757
+ }
758
+ /**
759
+ * Builds an `Operation` for every supported HTTP method on every path, in document order.
760
+ * `x-` path keys and unresolvable path-item `$ref`s are skipped.
756
761
  *
757
762
  * @example
758
763
  * ```ts
759
- * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
760
- * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
764
+ * for (const operation of getOperations(document)) {
765
+ * parseOperation(options, operation)
766
+ * }
761
767
  * ```
762
768
  */
763
- function dereferenceWithRef(document, schema) {
764
- if (isReference(schema)) return {
765
- ...schema,
766
- ...resolveRef(document, schema.$ref),
767
- $ref: schema.$ref
768
- };
769
- return schema;
769
+ function getOperations(document) {
770
+ const operations = [];
771
+ const paths = document.paths;
772
+ if (!paths) return operations;
773
+ for (const path of Object.keys(paths)) {
774
+ if (path.startsWith("x-")) continue;
775
+ let pathItem = paths[path];
776
+ if (!pathItem) continue;
777
+ if (isReference(pathItem)) {
778
+ const resolved = resolveRef(document, pathItem.$ref);
779
+ paths[path] = resolved;
780
+ if (!resolved || isReference(resolved)) continue;
781
+ pathItem = resolved;
782
+ }
783
+ const item = pathItem;
784
+ for (const method of Object.keys(item)) {
785
+ if (!SUPPORTED_METHODS.has(method)) continue;
786
+ const schema = item[method];
787
+ if (!schema || typeof schema !== "object") continue;
788
+ operations.push({
789
+ path,
790
+ method,
791
+ schema
792
+ });
793
+ }
794
+ }
795
+ return operations;
770
796
  }
771
797
  //#endregion
772
798
  //#region src/resolvers.ts
@@ -790,7 +816,16 @@ function resolveServerUrl(server, overrides) {
790
816
  for (const [key, variable] of Object.entries(server.variables)) {
791
817
  const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
792
818
  if (value === void 0) continue;
793
- 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(", ")}.`);
819
+ if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Diagnostics.Error({
820
+ code: Diagnostics.code.invalidServerVariable,
821
+ severity: "error",
822
+ message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
823
+ help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
824
+ location: {
825
+ kind: "document",
826
+ pointer: "#/servers"
827
+ }
828
+ });
794
829
  url = url.replaceAll(`{${key}}`, value);
795
830
  }
796
831
  return url;
@@ -803,6 +838,15 @@ function getSchemaType(format) {
803
838
  return formatMap[format] ?? null;
804
839
  }
805
840
  /**
841
+ * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
842
+ * `specialCasedFormats` that `convertFormat` handles directly. False means the format falls back to
843
+ * the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
844
+ * diagnostic in step with the parser as `formatMap` grows.
845
+ */
846
+ function isHandledFormat(format) {
847
+ return getSchemaType(format) !== null || specialCasedFormats.has(format);
848
+ }
849
+ /**
806
850
  * Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
807
851
  * Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
808
852
  */
@@ -812,15 +856,9 @@ function getPrimitiveType(type) {
812
856
  return "string";
813
857
  }
814
858
  /**
815
- * Narrows a content-type string to the `MediaType` union Kubb recognizes, or returns `null`.
816
- */
817
- function getMediaType(contentType) {
818
- return Object.values(ast.mediaTypes).includes(contentType) ? contentType : null;
819
- }
820
- /**
821
859
  * Returns all parameters for an operation, merging path-level and operation-level entries.
822
860
  * Operation-level parameters override path-level ones with the same `in:name` key.
823
- * `$ref` parameters resolve via `dereferenceWithRef` for backward compatibility.
861
+ * Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
824
862
  *
825
863
  * @example
826
864
  * ```ts
@@ -849,7 +887,7 @@ function getResponseBody(responseBody, contentType) {
849
887
  }
850
888
  let availableContentType;
851
889
  const contentTypes = Object.keys(body.content);
852
- for (const mt of contentTypes) if (matchesMimeType.json(mt)) {
890
+ for (const mt of contentTypes) if (isJsonMimeType(mt)) {
853
891
  availableContentType = mt;
854
892
  break;
855
893
  }
@@ -872,15 +910,21 @@ function getResponseBody(responseBody, contentType) {
872
910
  * getResponseSchema(document, operation, '4XX') // {}
873
911
  * ```
874
912
  */
875
- function getResponseSchema(document, operation, statusCode, options = {}) {
876
- if (operation.schema.responses) {
877
- const responses = operation.schema.responses;
878
- for (const key in responses) {
879
- const schema = responses[key];
880
- if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
881
- }
913
+ function resolveResponseRefs(document, operation) {
914
+ const responses = operation.schema.responses;
915
+ if (!responses) return;
916
+ for (const key in responses) {
917
+ const schema = responses[key];
918
+ if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
882
919
  }
883
- const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType);
920
+ }
921
+ function getResponseSchema(document, operation, statusCode, options = {}) {
922
+ resolveResponseRefs(document, operation);
923
+ const responseBody = getResponseBody(getResponseByStatusCode({
924
+ document,
925
+ operation,
926
+ statusCode
927
+ }), options.contentType);
884
928
  if (responseBody === false) return {};
885
929
  const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
886
930
  if (!schema) return {};
@@ -896,16 +940,25 @@ function getResponseSchema(document, operation, statusCode, options = {}) {
896
940
  */
897
941
  function getRequestSchema(document, operation, options = {}) {
898
942
  if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
899
- const requestBody = operation.getRequestBody(options.contentType);
943
+ const requestBody = getRequestContent({
944
+ document,
945
+ operation,
946
+ mediaType: options.contentType
947
+ });
900
948
  if (requestBody === false) return null;
949
+ const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
901
950
  const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
951
+ if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
952
+ type: "string",
953
+ contentMediaType: "application/octet-stream"
954
+ };
902
955
  if (!schema) return null;
903
956
  return dereferenceWithRef(document, schema);
904
957
  }
905
958
  /**
906
959
  * Flattens a keyword-only `allOf` into its parent schema.
907
960
  *
908
- * Only flattens when every member is a plain fragment no `$ref` and no structural keywords
961
+ * Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
909
962
  * (see `structuralKeys`). Outer schema values take precedence over fragment values.
910
963
  * Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
911
964
  *
@@ -913,9 +966,12 @@ function getRequestSchema(document, operation, options = {}) {
913
966
  * ```ts
914
967
  * flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
915
968
  * // { type: 'object', properties: {}, description: 'A pet' }
969
+ * ```
916
970
  *
971
+ * @example
972
+ * ```ts
917
973
  * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
918
- * // returned unchanged contains a $ref
974
+ * // returned unchanged, contains a $ref
919
975
  * ```
920
976
  */
921
977
  /**
@@ -931,17 +987,17 @@ function hasStructuralKeywords(fragment) {
931
987
  function flattenSchema(schema) {
932
988
  if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
933
989
  const allOfFragments = schema.allOf;
934
- if (allOfFragments.some((item) => isRef(item))) return schema;
990
+ if (allOfFragments.some((item) => isReference(item))) return schema;
935
991
  if (allOfFragments.some(hasStructuralKeywords)) return schema;
936
- const merged = { ...schema };
937
- delete merged.allOf;
992
+ const { allOf: _allOf, ...rest } = schema;
993
+ const merged = rest;
938
994
  for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) if (merged[key] === void 0) merged[key] = value;
939
995
  return merged;
940
996
  }
941
997
  /**
942
998
  * Extracts the inline schema from a media-type `content` map.
943
999
  *
944
- * Prefers `preferredContentType` when given; otherwise uses the first key in the map.
1000
+ * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
945
1001
  * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
946
1002
  *
947
1003
  * @example
@@ -960,21 +1016,22 @@ function extractSchemaFromContent(content, preferredContentType) {
960
1016
  /**
961
1017
  * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
962
1018
  */
963
- function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
1019
+ function* collectRefs(schema) {
964
1020
  if (Array.isArray(schema)) {
965
- for (const item of schema) collectRefs(item, refs);
966
- return refs;
1021
+ for (const item of schema) yield* collectRefs(item);
1022
+ return;
967
1023
  }
968
1024
  if (schema && typeof schema === "object") for (const key in schema) {
969
1025
  const value = schema[key];
970
- if (key === "$ref" && typeof value === "string") {
971
- if (value.startsWith("#/components/schemas/")) {
972
- const name = value.slice(21);
973
- if (name) refs.add(name);
974
- }
975
- } else collectRefs(value, refs);
1026
+ if (!(key === "$ref" && typeof value === "string")) {
1027
+ yield* collectRefs(value);
1028
+ continue;
1029
+ }
1030
+ if (value.startsWith("#/components/schemas/")) {
1031
+ const name = value.slice(21);
1032
+ if (name) yield name;
1033
+ }
976
1034
  }
977
- return refs;
978
1035
  }
979
1036
  /**
980
1037
  * Returns a copy of `schemas` topologically sorted by `$ref` dependency.
@@ -990,7 +1047,7 @@ function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
990
1047
  */
991
1048
  function sortSchemas(schemas) {
992
1049
  const deps = /* @__PURE__ */ new Map();
993
- for (const [name, schema] of Object.entries(schemas)) deps.set(name, Array.from(collectRefs(schema)));
1050
+ for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
994
1051
  const sorted = [];
995
1052
  const visited = /* @__PURE__ */ new Set();
996
1053
  function visit(name, stack) {
@@ -1011,9 +1068,6 @@ const semanticSuffixes = {
1011
1068
  responses: "Response",
1012
1069
  requestBodies: "Request"
1013
1070
  };
1014
- function getSemanticSuffix(source) {
1015
- return semanticSuffixes[source];
1016
- }
1017
1071
  function resolveSchemaRef(document, schema) {
1018
1072
  if (!isReference(schema)) return schema;
1019
1073
  const resolved = resolveRef(document, schema.$ref);
@@ -1062,13 +1116,13 @@ function getSchemas(document, { contentType }) {
1062
1116
  let hasMultipleSources = false;
1063
1117
  if (!isSingle) {
1064
1118
  const firstSource = items[0].source;
1065
- for (let i = 1; i < items.length; i++) if (items[i].source !== firstSource) {
1119
+ for (const item of items) if (item.source !== firstSource) {
1066
1120
  hasMultipleSources = true;
1067
1121
  break;
1068
1122
  }
1069
1123
  }
1070
1124
  items.forEach((item, index) => {
1071
- const suffix = isSingle ? "" : hasMultipleSources ? getSemanticSuffix(item.source) : index === 0 ? "" : String(index + 1);
1125
+ const suffix = isSingle ? "" : hasMultipleSources ? semanticSuffixes[item.source] : index === 0 ? "" : String(index + 1);
1072
1126
  const uniqueName = item.originalName + suffix;
1073
1127
  schemas[uniqueName] = item.schema;
1074
1128
  nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
@@ -1081,7 +1135,7 @@ function getSchemas(document, { contentType }) {
1081
1135
  }
1082
1136
  /**
1083
1137
  * Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
1084
- * Returns `null` when `dateType: false`, signalling the format should fall through to `string`.
1138
+ * Returns `null` when `dateType: false`, so the format falls through to `string`.
1085
1139
  */
1086
1140
  function getDateType(options, format) {
1087
1141
  if (!options.dateType) return null;
@@ -1115,6 +1169,15 @@ function getDateType(options, format) {
1115
1169
  /**
1116
1170
  * Collects the shared metadata fields passed to every `createSchema` call.
1117
1171
  */
1172
+ /**
1173
+ * Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
1174
+ * labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
1175
+ * 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
1176
+ */
1177
+ function extractExamples(schema) {
1178
+ if (Array.isArray(schema.examples)) return schema.examples;
1179
+ return schema.example !== void 0 ? [schema.example] : void 0;
1180
+ }
1118
1181
  function buildSchemaNode(schema, name, nullable, defaultValue) {
1119
1182
  return {
1120
1183
  name,
@@ -1125,15 +1188,16 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
1125
1188
  readOnly: schema.readOnly,
1126
1189
  writeOnly: schema.writeOnly,
1127
1190
  default: defaultValue,
1128
- example: schema.example
1191
+ examples: extractExamples(schema),
1192
+ format: schema.format
1129
1193
  };
1130
1194
  }
1131
1195
  /**
1132
1196
  * Returns all request body content type keys for an operation.
1133
1197
  *
1134
- * The requestBody is dereferenced **in-place** when it is a `$ref` the same mutation
1135
- * that `getRequestSchema` already performs so that the returned list accurately reflects
1136
- * the available content types even for referenced bodies.
1198
+ * The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
1199
+ * `getRequestSchema` already performs), so the returned list accurately reflects the
1200
+ * available content types even for referenced bodies.
1137
1201
  *
1138
1202
  * @example
1139
1203
  * ```ts
@@ -1147,15 +1211,38 @@ function getRequestBodyContentTypes(document, operation) {
1147
1211
  if (!body) return [];
1148
1212
  return body.content ? Object.keys(body.content) : [];
1149
1213
  }
1214
+ /**
1215
+ * Returns all response content type keys for an operation at a given status code.
1216
+ *
1217
+ * Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
1218
+ * so the returned list reflects the available content types even for referenced responses.
1219
+ *
1220
+ * @example
1221
+ * ```ts
1222
+ * getResponseBodyContentTypes(document, operation, 200)
1223
+ * // ['application/json', 'application/xml']
1224
+ * ```
1225
+ */
1226
+ function getResponseBodyContentTypes(document, operation, statusCode) {
1227
+ resolveResponseRefs(document, operation);
1228
+ const responseObj = getResponseByStatusCode({
1229
+ document,
1230
+ operation,
1231
+ statusCode
1232
+ });
1233
+ if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
1234
+ const body = responseObj;
1235
+ return body.content ? Object.keys(body.content) : [];
1236
+ }
1150
1237
  //#endregion
1151
1238
  //#region src/parser.ts
1152
1239
  /**
1153
1240
  * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
1154
1241
  *
1155
1242
  * This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
1156
- * from the array to its items sub-schema, making them valid for downstream processing.
1243
+ * from the array to its items sub-schema, so they are valid for downstream processing.
1157
1244
  *
1158
- * @note This is a defensive measure for robustness with non-compliant specs.
1245
+ * @note A defensive measure for non-compliant specs.
1159
1246
  */
1160
1247
  function normalizeArrayEnum(schema) {
1161
1248
  const normalizedItems = {
@@ -1169,15 +1256,48 @@ function normalizeArrayEnum(schema) {
1169
1256
  };
1170
1257
  }
1171
1258
  /**
1259
+ * Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
1260
+ * and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
1261
+ */
1262
+ function createNullSchema(schema, name, nullable) {
1263
+ return ast.factory.createSchema({
1264
+ type: "null",
1265
+ primitive: "null",
1266
+ name,
1267
+ title: schema.title,
1268
+ description: schema.description,
1269
+ deprecated: schema.deprecated,
1270
+ nullable,
1271
+ format: schema.format
1272
+ });
1273
+ }
1274
+ /**
1275
+ * Names the inline enums on a property's schema, and on each item when the property is a tuple, from
1276
+ * the parent and property name. Wraps `macroEnumName` at the property construction site.
1277
+ */
1278
+ function nameEnums(node, options) {
1279
+ const macro = macroEnumName(options);
1280
+ const named = ast.applyMacros(node, [macro], { depth: "shallow" });
1281
+ const tupleNode = ast.narrowSchema(named, "tuple");
1282
+ if (tupleNode?.items) {
1283
+ const namedItems = tupleNode.items.map((item) => ast.applyMacros(item, [macro], { depth: "shallow" }));
1284
+ if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
1285
+ ...tupleNode,
1286
+ items: namedItems
1287
+ };
1288
+ }
1289
+ return named;
1290
+ }
1291
+ /**
1172
1292
  * Factory function that creates schema and operation converters for a given OpenAPI context.
1173
1293
  *
1174
1294
  * Returns closures that share mutable state (`resolvingRefs` set for cycle detection).
1175
1295
  * Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
1176
- * made possible by hoisting of function declarations.
1296
+ * which works because function declarations hoist.
1177
1297
  *
1178
- * @note Not exported; called internally by `parseOas()` and `parseSchema()`.
1298
+ * @internal
1179
1299
  */
1180
- function createSchemaParser(ctx) {
1300
+ function createSchemaParser(ctx, dialect = oasDialect) {
1181
1301
  const document = ctx.document;
1182
1302
  /**
1183
1303
  * Tracks `$ref` paths that are currently being resolved to prevent infinite
@@ -1185,6 +1305,33 @@ function createSchemaParser(ctx) {
1185
1305
  */
1186
1306
  const resolvingRefs = /* @__PURE__ */ new Set();
1187
1307
  /**
1308
+ * Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
1309
+ *
1310
+ * Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
1311
+ * it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
1312
+ * since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
1313
+ * Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
1314
+ */
1315
+ const resolvedRefCache = /* @__PURE__ */ new Map();
1316
+ /**
1317
+ * Memoized record of whether a `$ref` path resolves to a node the document actually defines.
1318
+ * A circular ref still resolves to an existing target, so this stays `true` for cycles and only
1319
+ * goes `false` for a `$ref` that points at a component the spec never declares.
1320
+ */
1321
+ const refExistence = /* @__PURE__ */ new Map();
1322
+ function refExists(refPath) {
1323
+ if (!refExistence.has(refPath)) {
1324
+ let exists = false;
1325
+ try {
1326
+ exists = !!dialect.schema.resolveRef(document, refPath);
1327
+ } catch {
1328
+ exists = false;
1329
+ }
1330
+ refExistence.set(refPath, exists);
1331
+ }
1332
+ return refExistence.get(refPath) ?? false;
1333
+ }
1334
+ /**
1188
1335
  * Converts a `$ref` schema into a `RefSchemaNode`.
1189
1336
  *
1190
1337
  * The resolved schema is stored in `node.schema`. Usage-site sibling fields
@@ -1193,20 +1340,30 @@ function createSchemaParser(ctx) {
1193
1340
  * Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
1194
1341
  */
1195
1342
  function convertRef({ schema, name, nullable, defaultValue, rawOptions }) {
1196
- let resolvedSchema;
1343
+ let resolvedSchema = null;
1197
1344
  const refPath = schema.$ref;
1198
- if (refPath && !resolvingRefs.has(refPath)) try {
1199
- const referenced = resolveRef(document, refPath);
1200
- if (referenced) {
1201
- resolvingRefs.add(refPath);
1202
- resolvedSchema = parseSchema({ schema: referenced }, rawOptions);
1203
- resolvingRefs.delete(refPath);
1345
+ if (refPath && !resolvingRefs.has(refPath)) {
1346
+ if (!resolvedRefCache.has(refPath)) {
1347
+ try {
1348
+ const referenced = dialect.schema.resolveRef(document, refPath);
1349
+ if (referenced) {
1350
+ resolvingRefs.add(refPath);
1351
+ resolvedSchema = parseSchema({ schema: referenced }, rawOptions);
1352
+ resolvingRefs.delete(refPath);
1353
+ }
1354
+ } catch {}
1355
+ resolvedRefCache.set(refPath, resolvedSchema);
1204
1356
  }
1205
- } catch {}
1206
- return ast.createSchema({
1357
+ resolvedSchema = resolvedRefCache.get(refPath) ?? null;
1358
+ }
1359
+ if (refPath && document.components && !refExists(refPath)) return ast.factory.createSchema({
1360
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
1361
+ type: "unknown"
1362
+ });
1363
+ return ast.factory.createSchema({
1207
1364
  ...buildSchemaNode(schema, name, nullable, defaultValue),
1208
1365
  type: "ref",
1209
- name: ast.extractRefName(schema.$ref),
1366
+ name: extractRefName(schema.$ref),
1210
1367
  ref: schema.$ref,
1211
1368
  schema: resolvedSchema
1212
1369
  });
@@ -1219,12 +1376,12 @@ function createSchemaParser(ctx) {
1219
1376
  const [memberSchema] = schema.allOf;
1220
1377
  const memberNode = parseSchema({
1221
1378
  schema: memberSchema,
1222
- name: null
1379
+ name
1223
1380
  }, rawOptions);
1224
1381
  const { kind: _kind, ...memberNodeProps } = memberNode;
1225
1382
  const mergedNullable = nullable || memberNode.nullable || void 0;
1226
1383
  const mergedDefault = schema.default === null && mergedNullable ? void 0 : schema.default ?? memberNode.default;
1227
- return ast.createSchema({
1384
+ return ast.factory.createSchema({
1228
1385
  ...memberNodeProps,
1229
1386
  name,
1230
1387
  title: schema.title ?? memberNode.title,
@@ -1234,22 +1391,23 @@ function createSchemaParser(ctx) {
1234
1391
  readOnly: schema.readOnly ?? memberNode.readOnly,
1235
1392
  writeOnly: schema.writeOnly ?? memberNode.writeOnly,
1236
1393
  default: mergedDefault,
1237
- example: schema.example ?? memberNode.example,
1238
- pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0)
1394
+ examples: extractExamples(schema) ?? memberNode.examples,
1395
+ pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0),
1396
+ format: schema.format ?? memberNode.format
1239
1397
  });
1240
1398
  }
1241
1399
  const filteredDiscriminantValues = [];
1242
1400
  const allOfMembers = schema.allOf.filter((item) => {
1243
- if (!isReference(item) || !name) return true;
1244
- const deref = resolveRef(document, item.$ref);
1245
- if (!deref || !isDiscriminator(deref)) return true;
1401
+ if (!dialect.schema.isReference(item) || !name) return true;
1402
+ const deref = dialect.schema.resolveRef(document, item.$ref);
1403
+ if (!deref || !dialect.schema.isDiscriminator(deref)) return true;
1246
1404
  const parentUnion = deref.oneOf ?? deref.anyOf;
1247
1405
  if (!parentUnion) return true;
1248
1406
  const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1249
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1407
+ const inOneOf = parentUnion.some((oneOfItem) => dialect.schema.isReference(oneOfItem) && oneOfItem.$ref === childRef);
1250
1408
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1251
1409
  if (inOneOf || inMapping) {
1252
- const discriminatorValue = ast.findDiscriminator(deref.discriminator.mapping, childRef);
1410
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
1253
1411
  if (discriminatorValue) filteredDiscriminantValues.push({
1254
1412
  propertyName: deref.discriminator.propertyName,
1255
1413
  value: discriminatorValue
@@ -1257,22 +1415,28 @@ function createSchemaParser(ctx) {
1257
1415
  return false;
1258
1416
  }
1259
1417
  return true;
1260
- }).map((s) => parseSchema({ schema: s }, rawOptions));
1418
+ }).map((s) => parseSchema({
1419
+ schema: s,
1420
+ name
1421
+ }, rawOptions));
1261
1422
  const syntheticStart = allOfMembers.length;
1262
1423
  if (Array.isArray(schema.required) && schema.required.length) {
1263
1424
  const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
1264
1425
  const missingRequired = schema.required.filter((key) => !outerKeys.has(key));
1265
1426
  if (missingRequired.length) {
1266
1427
  const resolvedMembers = schema.allOf.flatMap((item) => {
1267
- if (!isReference(item)) return [item];
1268
- const deref = resolveRef(document, item.$ref);
1269
- return deref && !isReference(deref) ? [deref] : [];
1428
+ if (!dialect.schema.isReference(item)) return [item];
1429
+ const deref = dialect.schema.resolveRef(document, item.$ref);
1430
+ return deref && !dialect.schema.isReference(deref) ? [deref] : [];
1270
1431
  });
1271
1432
  for (const key of missingRequired) for (const resolved of resolvedMembers) if (resolved.properties?.[key]) {
1272
- allOfMembers.push(parseSchema({ schema: {
1273
- properties: { [key]: resolved.properties[key] },
1274
- required: [key]
1275
- } }, rawOptions));
1433
+ allOfMembers.push(parseSchema({
1434
+ schema: {
1435
+ properties: { [key]: resolved.properties[key] },
1436
+ required: [key]
1437
+ },
1438
+ name
1439
+ }, rawOptions));
1276
1440
  break;
1277
1441
  }
1278
1442
  }
@@ -1281,13 +1445,13 @@ function createSchemaParser(ctx) {
1281
1445
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
1282
1446
  allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions));
1283
1447
  }
1284
- for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(ast.createDiscriminantNode({
1448
+ for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(createDiscriminantNode({
1285
1449
  propertyName,
1286
1450
  value
1287
1451
  }));
1288
- return ast.createSchema({
1452
+ return ast.factory.createSchema({
1289
1453
  type: "intersection",
1290
- members: [...ast.mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
1454
+ members: [...mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
1291
1455
  ...buildSchemaNode(schema, name, nullable, defaultValue)
1292
1456
  });
1293
1457
  }
@@ -1298,79 +1462,104 @@ function createSchemaParser(ctx) {
1298
1462
  function pickDiscriminatorPropertyNode(node, propertyName) {
1299
1463
  const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1300
1464
  if (!discriminatorProperty) return null;
1301
- return ast.createSchema({
1465
+ return ast.factory.createSchema({
1302
1466
  type: "object",
1303
1467
  primitive: "object",
1304
1468
  properties: [discriminatorProperty]
1305
1469
  });
1306
1470
  }
1471
+ function resolveRefSilent($ref) {
1472
+ if (!$ref.startsWith("#")) return null;
1473
+ return decodeURIComponent($ref.substring(1)).split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document) ?? null;
1474
+ }
1475
+ function implicitDiscriminantValue(member) {
1476
+ if (!discriminator || discriminator.mapping || !dialect.schema.isReference(member)) return null;
1477
+ const value = extractRefName(member.$ref);
1478
+ if (!value) return null;
1479
+ const variant = resolveRefSilent(member.$ref);
1480
+ if (!variant) return null;
1481
+ const propertyName = discriminator.propertyName;
1482
+ const seen = /* @__PURE__ */ new Set([member.$ref]);
1483
+ function constrains(v) {
1484
+ const prop = v.properties?.[propertyName];
1485
+ const resolved = prop && dialect.schema.isReference(prop) ? resolveRefSilent(prop.$ref) : prop;
1486
+ if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
1487
+ const composition = v.allOf ?? v.oneOf ?? v.anyOf;
1488
+ if (!composition) return false;
1489
+ return composition.some((m) => {
1490
+ if (!dialect.schema.isReference(m)) return constrains(m);
1491
+ if (seen.has(m.$ref)) return false;
1492
+ seen.add(m.$ref);
1493
+ const r = resolveRefSilent(m.$ref);
1494
+ return r ? constrains(r) : false;
1495
+ });
1496
+ }
1497
+ return constrains(variant) ? null : value;
1498
+ }
1307
1499
  const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
1308
1500
  const strategy = schema.oneOf ? "one" : "any";
1309
1501
  const unionBase = {
1310
1502
  ...buildSchemaNode(schema, name, nullable, defaultValue),
1311
- discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1503
+ discriminatorPropertyName: dialect.schema.isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1312
1504
  strategy
1313
1505
  };
1314
- const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
1315
- const sharedPropertiesNode = schema.properties ? (() => {
1316
- const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema;
1317
- return parseSchema({
1318
- schema: discriminator ? Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== "discriminator")) : schemaWithoutUnion,
1319
- name
1320
- }, rawOptions);
1321
- })() : void 0;
1322
- if (sharedPropertiesNode || discriminator?.mapping) {
1506
+ const discriminator = dialect.schema.isDiscriminator(schema) ? schema.discriminator : void 0;
1507
+ const { oneOf: _o, anyOf: _a, discriminator: _d, ...memberBaseSchema } = schema;
1508
+ const sharedPropertiesNode = schema.properties ? parseSchema({
1509
+ schema: memberBaseSchema,
1510
+ name
1511
+ }, rawOptions) : void 0;
1512
+ if (sharedPropertiesNode || discriminator) {
1323
1513
  const members = unionMembers.map((s) => {
1324
- const ref = isReference(s) ? s.$ref : void 0;
1325
- const discriminatorValue = ast.findDiscriminator(discriminator?.mapping, ref);
1326
- const memberNode = parseSchema({ schema: s }, rawOptions);
1514
+ const ref = dialect.schema.isReference(s) ? s.$ref : void 0;
1515
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
1516
+ const memberNode = parseSchema({
1517
+ schema: s,
1518
+ name
1519
+ }, rawOptions);
1327
1520
  if (!discriminatorValue || !discriminator) return memberNode;
1328
- const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.setDiscriminatorEnum({
1329
- node: sharedPropertiesNode,
1521
+ const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.applyMacros(sharedPropertiesNode, [macroDiscriminatorEnum({
1330
1522
  propertyName: discriminator.propertyName,
1331
1523
  values: [discriminatorValue]
1332
- }), discriminator.propertyName) : void 0;
1333
- return ast.createSchema({
1524
+ })], { depth: "shallow" }), discriminator.propertyName) : void 0;
1525
+ return ast.factory.createSchema({
1334
1526
  type: "intersection",
1335
- members: [memberNode, narrowedDiscriminatorNode ?? ast.createDiscriminantNode({
1527
+ members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
1336
1528
  propertyName: discriminator.propertyName,
1337
1529
  value: discriminatorValue
1338
1530
  })]
1339
1531
  });
1340
1532
  });
1341
- const unionNode = ast.createSchema({
1533
+ const unionNode = ast.factory.createSchema({
1342
1534
  type: "union",
1343
1535
  ...unionBase,
1344
1536
  members
1345
1537
  });
1346
1538
  if (!sharedPropertiesNode) return unionNode;
1347
- return ast.createSchema({
1539
+ return ast.factory.createSchema({
1348
1540
  type: "intersection",
1349
1541
  ...buildSchemaNode(schema, name, nullable, defaultValue),
1350
1542
  members: [unionNode, sharedPropertiesNode]
1351
1543
  });
1352
1544
  }
1353
- return ast.createSchema({
1545
+ const unionNode = ast.factory.createSchema({
1354
1546
  type: "union",
1355
1547
  ...unionBase,
1356
- members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s }, rawOptions)))
1548
+ members: unionMembers.map((s) => parseSchema({
1549
+ schema: s,
1550
+ name
1551
+ }, rawOptions))
1357
1552
  });
1553
+ return ast.applyMacros(unionNode, [macroSimplifyUnion], { depth: "shallow" });
1358
1554
  }
1359
1555
  /**
1360
1556
  * Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
1361
1557
  */
1362
1558
  function convertConst({ schema, name, nullable, defaultValue }) {
1363
1559
  const constValue = schema.const;
1364
- if (constValue === null) return ast.createSchema({
1365
- type: "null",
1366
- primitive: "null",
1367
- name,
1368
- title: schema.title,
1369
- description: schema.description,
1370
- deprecated: schema.deprecated
1371
- });
1560
+ if (constValue === null) return createNullSchema(schema, name);
1372
1561
  const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
1373
- return ast.createSchema({
1562
+ return ast.factory.createSchema({
1374
1563
  type: "enum",
1375
1564
  primitive: constPrimitive,
1376
1565
  enumValues: [constValue],
@@ -1383,7 +1572,7 @@ function createSchemaParser(ctx) {
1383
1572
  */
1384
1573
  function convertFormat({ schema, name, nullable, defaultValue, options }) {
1385
1574
  const base = buildSchemaNode(schema, name, nullable, defaultValue);
1386
- if (schema.format === "int64") return ast.createSchema({
1575
+ if (schema.format === "int64") return ast.factory.createSchema({
1387
1576
  type: options.integerType === "bigint" ? "bigint" : "integer",
1388
1577
  primitive: "integer",
1389
1578
  ...base,
@@ -1395,14 +1584,14 @@ function createSchemaParser(ctx) {
1395
1584
  if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
1396
1585
  const dateType = getDateType(options, schema.format);
1397
1586
  if (!dateType) return null;
1398
- if (dateType.type === "datetime") return ast.createSchema({
1587
+ if (dateType.type === "datetime") return ast.factory.createSchema({
1399
1588
  ...base,
1400
1589
  primitive: "string",
1401
1590
  type: "datetime",
1402
1591
  offset: dateType.offset,
1403
1592
  local: dateType.local
1404
1593
  });
1405
- return ast.createSchema({
1594
+ return ast.factory.createSchema({
1406
1595
  ...base,
1407
1596
  primitive: "string",
1408
1597
  type: dateType.type,
@@ -1412,36 +1601,36 @@ function createSchemaParser(ctx) {
1412
1601
  const specialType = getSchemaType(schema.format);
1413
1602
  if (!specialType) return null;
1414
1603
  const specialPrimitive = specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string";
1415
- if (specialType === "number" || specialType === "integer" || specialType === "bigint") return ast.createSchema({
1604
+ if (specialType === "number" || specialType === "integer" || specialType === "bigint") return ast.factory.createSchema({
1416
1605
  ...base,
1417
1606
  primitive: specialPrimitive,
1418
1607
  type: specialType
1419
1608
  });
1420
- if (specialType === "url") return ast.createSchema({
1609
+ if (specialType === "url") return ast.factory.createSchema({
1421
1610
  ...base,
1422
1611
  primitive: "string",
1423
1612
  type: "url",
1424
1613
  min: schema.minLength,
1425
1614
  max: schema.maxLength
1426
1615
  });
1427
- if (specialType === "ipv4") return ast.createSchema({
1616
+ if (specialType === "ipv4") return ast.factory.createSchema({
1428
1617
  ...base,
1429
1618
  primitive: "string",
1430
1619
  type: "ipv4"
1431
1620
  });
1432
- if (specialType === "ipv6") return ast.createSchema({
1621
+ if (specialType === "ipv6") return ast.factory.createSchema({
1433
1622
  ...base,
1434
1623
  primitive: "string",
1435
1624
  type: "ipv6"
1436
1625
  });
1437
- if (specialType === "uuid" || specialType === "email") return ast.createSchema({
1626
+ if (specialType === "uuid" || specialType === "email") return ast.factory.createSchema({
1438
1627
  ...base,
1439
1628
  primitive: "string",
1440
1629
  type: specialType,
1441
1630
  min: schema.minLength,
1442
1631
  max: schema.maxLength
1443
1632
  });
1444
- return ast.createSchema({
1633
+ return ast.factory.createSchema({
1445
1634
  ...base,
1446
1635
  primitive: specialPrimitive,
1447
1636
  type: specialType
@@ -1457,6 +1646,7 @@ function createSchemaParser(ctx) {
1457
1646
  }, rawOptions);
1458
1647
  const nullInEnum = schema.enum.includes(null);
1459
1648
  const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
1649
+ if (nullInEnum && filteredValues.length === 0) return createNullSchema(schema, name);
1460
1650
  const enumNullable = nullable || nullInEnum || void 0;
1461
1651
  const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1462
1652
  const enumPrimitive = getPrimitiveType(type);
@@ -1471,21 +1661,25 @@ function createSchemaParser(ctx) {
1471
1661
  readOnly: schema.readOnly,
1472
1662
  writeOnly: schema.writeOnly,
1473
1663
  default: enumDefault,
1474
- example: schema.example
1664
+ examples: extractExamples(schema),
1665
+ format: schema.format
1475
1666
  };
1476
1667
  const extensionKey = enumExtensionKeys.find((key) => key in schema);
1477
- if (extensionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
1668
+ const descriptionKey = enumDescriptionKeys.find((key) => key in schema);
1669
+ if (extensionKey || descriptionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
1478
1670
  const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
1479
1671
  const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
1672
+ const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
1480
1673
  const uniqueValues = [...new Set(filteredValues)];
1481
1674
  const seenNames = /* @__PURE__ */ new Set();
1482
- return ast.createSchema({
1675
+ return ast.factory.createSchema({
1483
1676
  ...enumBase,
1484
1677
  primitive: enumPrimitiveType,
1485
1678
  namedEnumValues: uniqueValues.map((value, index) => ({
1486
1679
  name: String(rawEnumNames?.[index] ?? value),
1487
1680
  value,
1488
- primitive: enumPrimitiveType
1681
+ primitive: enumPrimitiveType,
1682
+ description: rawEnumDescriptions?.[index]
1489
1683
  })).filter((entry) => {
1490
1684
  if (seenNames.has(entry.name)) return false;
1491
1685
  seenNames.add(entry.name);
@@ -1493,7 +1687,7 @@ function createSchemaParser(ctx) {
1493
1687
  })
1494
1688
  });
1495
1689
  }
1496
- return ast.createSchema({
1690
+ return ast.factory.createSchema({
1497
1691
  ...enumBase,
1498
1692
  enumValues: [...new Set(filteredValues)]
1499
1693
  });
@@ -1505,21 +1699,16 @@ function createSchemaParser(ctx) {
1505
1699
  const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
1506
1700
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
1507
1701
  const resolvedPropSchema = propSchema;
1508
- const propNullable = isNullable(resolvedPropSchema);
1509
- const propNode = parseSchema({
1702
+ const propNullable = dialect.schema.isNullable(resolvedPropSchema);
1703
+ const schemaNode = nameEnums(parseSchema({
1510
1704
  schema: resolvedPropSchema,
1511
- name: ast.childName(name, propName)
1512
- }, rawOptions);
1513
- let schemaNode = ast.setEnumName(propNode, name, propName, options.enumSuffix);
1514
- const tupleNode = ast.narrowSchema(schemaNode, "tuple");
1515
- if (tupleNode?.items) {
1516
- const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix));
1517
- if (namedItems.some((item, i) => item !== tupleNode.items[i])) schemaNode = {
1518
- ...tupleNode,
1519
- items: namedItems
1520
- };
1521
- }
1522
- return ast.createProperty({
1705
+ name: childName(name, propName)
1706
+ }, rawOptions), {
1707
+ parentName: name,
1708
+ propName,
1709
+ enumSuffix: options.enumSuffix
1710
+ });
1711
+ return ast.factory.createProperty({
1523
1712
  name: propName,
1524
1713
  schema: {
1525
1714
  ...schemaNode,
@@ -1529,14 +1718,15 @@ function createSchemaParser(ctx) {
1529
1718
  });
1530
1719
  }) : [];
1531
1720
  const additionalProperties = schema.additionalProperties;
1532
- let additionalPropertiesNode;
1533
- if (additionalProperties === true) additionalPropertiesNode = true;
1534
- else if (additionalProperties && Object.keys(additionalProperties).length > 0) additionalPropertiesNode = parseSchema({ schema: additionalProperties }, rawOptions);
1535
- else if (additionalProperties === false) additionalPropertiesNode = false;
1536
- else if (additionalProperties) additionalPropertiesNode = ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1721
+ const additionalPropertiesNode = (() => {
1722
+ if (additionalProperties === true) return true;
1723
+ if (additionalProperties === false) return false;
1724
+ if (additionalProperties && Object.keys(additionalProperties).length > 0) return parseSchema({ schema: additionalProperties }, rawOptions);
1725
+ if (additionalProperties) return ast.factory.createSchema({ type: options.unknownType });
1726
+ })();
1537
1727
  const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
1538
- 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;
1539
- const objectNode = ast.createSchema({
1728
+ const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? ast.factory.createSchema({ type: options.unknownType }) : parseSchema({ schema: patternSchema }, rawOptions)])) : void 0;
1729
+ const objectNode = ast.factory.createSchema({
1540
1730
  type: "object",
1541
1731
  primitive: "object",
1542
1732
  properties,
@@ -1546,16 +1736,15 @@ function createSchemaParser(ctx) {
1546
1736
  maxProperties: schema.maxProperties,
1547
1737
  ...buildSchemaNode(schema, name, nullable, defaultValue)
1548
1738
  });
1549
- if (isDiscriminator(schema) && schema.discriminator.mapping) {
1739
+ if (dialect.schema.isDiscriminator(schema) && schema.discriminator.mapping) {
1550
1740
  const discPropName = schema.discriminator.propertyName;
1551
1741
  const values = Object.keys(schema.discriminator.mapping);
1552
- const enumName = name ? ast.enumPropName(name, discPropName, options.enumSuffix) : void 0;
1553
- return ast.setDiscriminatorEnum({
1554
- node: objectNode,
1742
+ const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
1743
+ return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
1555
1744
  propertyName: discPropName,
1556
1745
  values,
1557
1746
  enumName
1558
- });
1747
+ })], { depth: "shallow" });
1559
1748
  }
1560
1749
  return objectNode;
1561
1750
  }
@@ -1564,8 +1753,8 @@ function createSchemaParser(ctx) {
1564
1753
  */
1565
1754
  function convertTuple({ schema, name, nullable, defaultValue, rawOptions }) {
1566
1755
  const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item }, rawOptions));
1567
- const rest = schema.items ? parseSchema({ schema: schema.items }, rawOptions) : ast.createSchema({ type: "any" });
1568
- return ast.createSchema({
1756
+ const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? ast.factory.createSchema({ type: "any" }) : parseSchema({ schema: schema.items }, rawOptions);
1757
+ return ast.factory.createSchema({
1569
1758
  type: "tuple",
1570
1759
  primitive: "array",
1571
1760
  items: tupleItems,
@@ -1580,12 +1769,12 @@ function createSchemaParser(ctx) {
1580
1769
  */
1581
1770
  function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }) {
1582
1771
  const rawItems = schema.items;
1583
- const itemName = rawItems?.enum?.length && name ? ast.enumPropName(void 0, name, options.enumSuffix) : void 0;
1772
+ const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name;
1584
1773
  const items = rawItems ? [parseSchema({
1585
1774
  schema: rawItems,
1586
1775
  name: itemName
1587
1776
  }, rawOptions)] : [];
1588
- return ast.createSchema({
1777
+ return ast.factory.createSchema({
1589
1778
  type: "array",
1590
1779
  primitive: "array",
1591
1780
  items,
@@ -1599,7 +1788,7 @@ function createSchemaParser(ctx) {
1599
1788
  * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1600
1789
  */
1601
1790
  function convertString({ schema, name, nullable, defaultValue }) {
1602
- return ast.createSchema({
1791
+ return ast.factory.createSchema({
1603
1792
  type: "string",
1604
1793
  primitive: "string",
1605
1794
  min: schema.minLength,
@@ -1612,7 +1801,7 @@ function createSchemaParser(ctx) {
1612
1801
  * Converts a `type: 'number'` or `type: 'integer'` schema.
1613
1802
  */
1614
1803
  function convertNumeric({ schema, name, nullable, defaultValue }, type) {
1615
- return ast.createSchema({
1804
+ return ast.factory.createSchema({
1616
1805
  type,
1617
1806
  primitive: type,
1618
1807
  min: schema.minimum,
@@ -1627,32 +1816,132 @@ function createSchemaParser(ctx) {
1627
1816
  * Converts a `type: 'boolean'` schema.
1628
1817
  */
1629
1818
  function convertBoolean({ schema, name, nullable, defaultValue }) {
1630
- return ast.createSchema({
1819
+ return ast.factory.createSchema({
1631
1820
  type: "boolean",
1632
1821
  primitive: "boolean",
1633
1822
  ...buildSchemaNode(schema, name, nullable, defaultValue)
1634
1823
  });
1635
1824
  }
1636
1825
  /**
1637
- * Converts an explicit `type: 'null'` schema.
1826
+ * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
1827
+ * into a `blob` node.
1638
1828
  */
1639
- function convertNull({ schema, name, nullable }) {
1640
- return ast.createSchema({
1641
- type: "null",
1642
- primitive: "null",
1643
- name,
1644
- title: schema.title,
1645
- description: schema.description,
1646
- deprecated: schema.deprecated,
1647
- nullable
1829
+ function convertBlob({ schema, name, nullable, defaultValue }) {
1830
+ return ast.factory.createSchema({
1831
+ type: "blob",
1832
+ primitive: "string",
1833
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1834
+ });
1835
+ }
1836
+ /**
1837
+ * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
1838
+ *
1839
+ * Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parseSchema`
1840
+ * falls through and handles it as that single type with nullability already folded in.
1841
+ */
1842
+ function convertMultiType({ schema, name, nullable, defaultValue, rawOptions }) {
1843
+ const types = schema.type;
1844
+ const nonNullTypes = types.filter((t) => t !== "null");
1845
+ if (nonNullTypes.length <= 1) return null;
1846
+ const arrayNullable = types.includes("null") || nullable || void 0;
1847
+ return ast.factory.createSchema({
1848
+ type: "union",
1849
+ members: nonNullTypes.map((t) => parseSchema({
1850
+ schema: {
1851
+ ...schema,
1852
+ type: t
1853
+ },
1854
+ name
1855
+ }, rawOptions)),
1856
+ ...buildSchemaNode(schema, name, arrayNullable, defaultValue)
1648
1857
  });
1649
1858
  }
1650
1859
  /**
1651
- * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
1860
+ * Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
1861
+ * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
1862
+ * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
1863
+ * match/convert/fall-through contract.
1864
+ */
1865
+ const schemaRules = [
1866
+ {
1867
+ match: ({ schema }) => dialect.schema.isReference(schema),
1868
+ convert: convertRef
1869
+ },
1870
+ {
1871
+ match: ({ schema }) => !!schema.allOf?.length,
1872
+ convert: convertAllOf
1873
+ },
1874
+ {
1875
+ match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length),
1876
+ convert: convertUnion
1877
+ },
1878
+ {
1879
+ match: ({ schema }) => "const" in schema && schema.const !== void 0,
1880
+ convert: convertConst
1881
+ },
1882
+ {
1883
+ match: ({ schema }) => !!schema.format,
1884
+ convert: convertFormat
1885
+ },
1886
+ {
1887
+ match: ({ schema }) => dialect.schema.isBinary(schema),
1888
+ convert: convertBlob
1889
+ },
1890
+ {
1891
+ match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
1892
+ convert: convertMultiType
1893
+ },
1894
+ {
1895
+ match: ({ schema, type }) => !type && (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0),
1896
+ convert: convertString
1897
+ },
1898
+ {
1899
+ match: ({ schema, type }) => !type && (schema.minimum !== void 0 || schema.maximum !== void 0),
1900
+ convert: (ctx) => convertNumeric(ctx, "number")
1901
+ },
1902
+ {
1903
+ match: ({ schema }) => !!schema.enum?.length,
1904
+ convert: convertEnum
1905
+ },
1906
+ {
1907
+ match: ({ schema, type }) => type === "object" || !!schema.properties || !!schema.additionalProperties || "patternProperties" in schema,
1908
+ convert: convertObject
1909
+ },
1910
+ {
1911
+ match: ({ schema }) => "prefixItems" in schema,
1912
+ convert: convertTuple
1913
+ },
1914
+ {
1915
+ match: ({ schema, type }) => type === "array" || "items" in schema,
1916
+ convert: convertArray
1917
+ },
1918
+ {
1919
+ match: ({ type }) => type === "string",
1920
+ convert: convertString
1921
+ },
1922
+ {
1923
+ match: ({ type }) => type === "number",
1924
+ convert: (ctx) => convertNumeric(ctx, "number")
1925
+ },
1926
+ {
1927
+ match: ({ type }) => type === "integer",
1928
+ convert: (ctx) => convertNumeric(ctx, "integer")
1929
+ },
1930
+ {
1931
+ match: ({ type }) => type === "boolean",
1932
+ convert: convertBoolean
1933
+ },
1934
+ {
1935
+ match: ({ type }) => type === "null",
1936
+ convert: ({ schema, name, nullable }) => createNullSchema(schema, name, nullable)
1937
+ }
1938
+ ];
1939
+ /**
1940
+ * Converts an OAS `SchemaObject` into a `SchemaNode`.
1652
1941
  *
1653
- * Dispatch order (first match wins): `$ref` `allOf` `oneOf`/`anyOf` `const` → `format`
1654
- * octet-stream blob multi-type array constraint-inferred type `enum` object/array/tuple/scalar
1655
- * → empty-schema fallback (`emptySchemaType` option).
1942
+ * Builds the per-schema context, then walks the ordered {@link schemaRules} table and returns
1943
+ * the first converter that produces a node. When none match, falls back to the configured
1944
+ * `emptySchemaType`.
1656
1945
  */
1657
1946
  function parseSchema({ schema, name }, rawOptions) {
1658
1947
  const options = {
@@ -1664,81 +1953,53 @@ function createSchemaParser(ctx) {
1664
1953
  schema: flattenedSchema,
1665
1954
  name
1666
1955
  }, rawOptions);
1667
- const nullable = isNullable(schema) || void 0;
1668
- const defaultValue = schema.default === null && nullable ? void 0 : schema.default;
1669
- const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
1670
- const ctx = {
1956
+ const nullable = dialect.schema.isNullable(schema) || void 0;
1957
+ const schemaCtx = {
1671
1958
  schema,
1672
1959
  name,
1673
1960
  nullable,
1674
- defaultValue,
1675
- type,
1961
+ defaultValue: schema.default === null && nullable ? void 0 : schema.default,
1962
+ type: Array.isArray(schema.type) ? schema.type[0] : schema.type,
1676
1963
  rawOptions,
1677
1964
  options
1678
1965
  };
1679
- if (isReference(schema)) return convertRef(ctx);
1680
- if (schema.allOf?.length) return convertAllOf(ctx);
1681
- if ([...schema.oneOf ?? [], ...schema.anyOf ?? []].length) return convertUnion(ctx);
1682
- if ("const" in schema && schema.const !== void 0) return convertConst(ctx);
1683
- if (schema.format) {
1684
- const formatResult = convertFormat(ctx);
1685
- if (formatResult) return formatResult;
1686
- }
1687
- if (schema.type === "string" && schema.contentMediaType === "application/octet-stream") return ast.createSchema({
1688
- type: "blob",
1689
- primitive: "string",
1690
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1691
- });
1692
- if (Array.isArray(schema.type) && schema.type.length > 1) {
1693
- const nonNullTypes = schema.type.filter((t) => t !== "null");
1694
- const arrayNullable = schema.type.includes("null") || nullable || void 0;
1695
- if (nonNullTypes.length > 1) return ast.createSchema({
1696
- type: "union",
1697
- members: nonNullTypes.map((t) => parseSchema({
1698
- schema: {
1699
- ...schema,
1700
- type: t
1701
- },
1702
- name
1703
- }, rawOptions)),
1704
- ...buildSchemaNode(schema, name, arrayNullable, defaultValue)
1705
- });
1966
+ for (const rule of schemaRules) {
1967
+ if (!rule.match(schemaCtx)) continue;
1968
+ const node = rule.convert(schemaCtx);
1969
+ if (node) return node;
1706
1970
  }
1707
- if (!type) {
1708
- if (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0) return convertString(ctx);
1709
- if (schema.minimum !== void 0 || schema.maximum !== void 0) return convertNumeric(ctx, "number");
1710
- }
1711
- if (schema.enum?.length) return convertEnum(ctx);
1712
- if (type === "object" || schema.properties || schema.additionalProperties || "patternProperties" in schema) return convertObject(ctx);
1713
- if ("prefixItems" in schema) return convertTuple(ctx);
1714
- if (type === "array" || "items" in schema) return convertArray(ctx);
1715
- if (type === "string") return convertString(ctx);
1716
- if (type === "number") return convertNumeric(ctx, "number");
1717
- if (type === "integer") return convertNumeric(ctx, "integer");
1718
- if (type === "boolean") return convertBoolean(ctx);
1719
- if (type === "null") return convertNull(ctx);
1720
- const emptyType = typeOptionMap.get(options.emptySchemaType);
1721
- return ast.createSchema({
1971
+ const emptyType = options.emptySchemaType;
1972
+ return ast.factory.createSchema({
1722
1973
  type: emptyType,
1723
1974
  name,
1724
1975
  title: schema.title,
1725
- description: schema.description
1976
+ description: schema.description,
1977
+ format: schema.format
1726
1978
  });
1727
1979
  }
1728
1980
  /**
1729
1981
  * Converts a dereferenced OAS parameter object into a `ParameterNode`.
1730
1982
  */
1731
- function parseParameter(options, param) {
1983
+ function parseParameter(options, param, parentName) {
1732
1984
  const required = param["required"] ?? false;
1733
- const schema = param["schema"] ? parseSchema({ schema: param["schema"] }, options) : ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1734
- return ast.createParameter({
1735
- name: param["name"],
1985
+ const paramName = param["name"];
1986
+ const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : void 0;
1987
+ const schema = param["schema"] ? parseSchema({
1988
+ schema: param["schema"],
1989
+ name: schemaName
1990
+ }, options) : ast.factory.createSchema({ type: options.unknownType });
1991
+ const style = param["style"];
1992
+ const explode = param["explode"];
1993
+ return ast.factory.createParameter({
1994
+ name: paramName,
1736
1995
  in: param["in"],
1737
1996
  schema: {
1738
1997
  ...schema,
1739
1998
  description: param["description"] ?? schema.description
1740
1999
  },
1741
- required
2000
+ required,
2001
+ ...style !== void 0 ? { style } : {},
2002
+ ...explode !== void 0 ? { explode } : {}
1742
2003
  });
1743
2004
  }
1744
2005
  /**
@@ -1754,73 +2015,97 @@ function createSchemaParser(ctx) {
1754
2015
  };
1755
2016
  }
1756
2017
  /**
1757
- * Reads the inline response object (not a `$ref`) and returns its description plus its `content` map.
1758
- */
1759
- function getResponseMeta(responseObj) {
1760
- if (typeof responseObj !== "object" || responseObj === null || Array.isArray(responseObj)) return {};
1761
- const inline = responseObj;
1762
- return {
1763
- description: inline.description,
1764
- content: inline.content
1765
- };
1766
- }
1767
- /**
1768
2018
  * Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
1769
2019
  * `$ref` entries are skipped since their flags live on the dereferenced target.
1770
2020
  */
1771
2021
  function collectPropertyKeysByFlag(schema, flag) {
1772
- if (!schema?.properties) return void 0;
2022
+ if (!schema?.properties) return null;
1773
2023
  const keys = [];
1774
2024
  for (const key in schema.properties) {
1775
2025
  const prop = schema.properties[key];
1776
- if (prop && !isReference(prop) && prop[flag]) keys.push(key);
2026
+ if (prop && !dialect.schema.isReference(prop) && prop[flag]) keys.push(key);
1777
2027
  }
1778
- return keys.length ? keys : void 0;
2028
+ return keys.length ? keys : null;
1779
2029
  }
1780
2030
  /**
1781
2031
  * Converts an OAS `Operation` into an `OperationNode`.
1782
2032
  */
1783
2033
  function parseOperation(options, operation) {
1784
- const parameters = getParameters(document, operation).map((param) => parseParameter(options, param));
2034
+ const operationId = getOperationId(operation);
2035
+ const operationName = operationId ? pascalCase(operationId) : void 0;
2036
+ const parameters = getParameters(document, operation).map((param) => parseParameter(options, param, operationName));
1785
2037
  const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
1786
2038
  const requestBodyMeta = getRequestBodyMeta(operation);
2039
+ const requestBodyName = operationName ? `${operationName}Request` : void 0;
1787
2040
  const content = allContentTypes.flatMap((ct) => {
1788
2041
  const schema = getRequestSchema(document, operation, { contentType: ct });
1789
2042
  if (!schema) return [];
1790
- return [{
2043
+ return [ast.factory.createContent({
1791
2044
  contentType: ct,
1792
- schema: ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
2045
+ schema: ast.optionality(parseSchema({
2046
+ schema,
2047
+ name: requestBodyName
2048
+ }, options), requestBodyMeta.required),
1793
2049
  keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
1794
- }];
2050
+ })];
1795
2051
  });
1796
2052
  const requestBody = content.length > 0 || requestBodyMeta.description ? {
1797
2053
  description: requestBodyMeta.description,
1798
2054
  required: requestBodyMeta.required || void 0,
1799
2055
  content: content.length > 0 ? content : void 0
1800
2056
  } : void 0;
1801
- const responses = operation.getResponseStatusCodes().map((statusCode) => {
1802
- const responseObj = operation.getResponseByStatusCode(statusCode);
1803
- const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType });
1804
- const schema = responseSchema && Object.keys(responseSchema).length > 0 ? parseSchema({ schema: responseSchema }, options) : ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType) });
1805
- const { description, content } = getResponseMeta(responseObj);
1806
- const mediaType = content ? getMediaType(Object.keys(content)[0] ?? "") : getMediaType(operation.contentType ?? "");
1807
- return ast.createResponse({
2057
+ const responses = getResponseStatusCodes(operation).map((statusCode) => {
2058
+ const responseObj = getResponseByStatusCode({
2059
+ document,
2060
+ operation,
2061
+ statusCode
2062
+ });
2063
+ const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
2064
+ const description = typeof responseObj === "object" && responseObj !== null ? responseObj.description : void 0;
2065
+ const parseEntrySchema = (contentType) => {
2066
+ const raw = getResponseSchema(document, operation, statusCode, { contentType });
2067
+ return {
2068
+ schema: raw && Object.keys(raw).length > 0 ? parseSchema({
2069
+ schema: raw,
2070
+ name: responseName
2071
+ }, options) : ast.factory.createSchema({ type: options.emptySchemaType }),
2072
+ keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
2073
+ };
2074
+ };
2075
+ const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)).map((contentType) => ast.factory.createContent({
2076
+ contentType,
2077
+ ...parseEntrySchema(contentType)
2078
+ }));
2079
+ if (content.length === 0) content.push(ast.factory.createContent({
2080
+ contentType: getRequestContentType({
2081
+ document,
2082
+ operation
2083
+ }) || "application/json",
2084
+ ...parseEntrySchema(ctx.contentType)
2085
+ }));
2086
+ return ast.factory.createResponse({
1808
2087
  statusCode,
1809
2088
  description,
1810
- schema,
1811
- mediaType,
1812
- keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
2089
+ content
1813
2090
  });
1814
2091
  });
1815
- const urlPath = new URLPath(operation.path);
1816
- return ast.createOperation({
1817
- operationId: operation.getOperationId(),
2092
+ const pathItem = document.paths?.[operation.path];
2093
+ const pathItemDoc = pathItem && !dialect.schema.isReference(pathItem) ? pathItem : void 0;
2094
+ const pickDoc = (key) => {
2095
+ const own = operation.schema[key];
2096
+ if (typeof own === "string") return own;
2097
+ const fallback = pathItemDoc?.[key];
2098
+ return typeof fallback === "string" ? fallback : void 0;
2099
+ };
2100
+ return ast.factory.createOperation({
2101
+ operationId,
2102
+ protocol: "http",
1818
2103
  method: operation.method.toUpperCase(),
1819
- path: urlPath.path,
1820
- tags: operation.getTags().map((tag) => tag.name),
1821
- summary: operation.getSummary() || void 0,
1822
- description: operation.getDescription() || void 0,
1823
- deprecated: operation.isDeprecated() || void 0,
2104
+ path: operation.path,
2105
+ tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
2106
+ summary: pickDoc("summary") || void 0,
2107
+ description: pickDoc("description") || void 0,
2108
+ deprecated: operation.schema.deprecated || void 0,
1824
2109
  parameters,
1825
2110
  requestBody,
1826
2111
  responses
@@ -1832,59 +2117,264 @@ function createSchemaParser(ctx) {
1832
2117
  parseParameter
1833
2118
  };
1834
2119
  }
2120
+ //#endregion
2121
+ //#region src/promoteEnums.ts
2122
+ /**
2123
+ * Collects inline enums to lift to the top level, keyed by the name the parser derived for them
2124
+ * (e.g. `PetStatusEnum`). An enum already defined as a top-level component is left as-is, and a
2125
+ * name that recurs maps to the first definition so each name yields one shared type.
2126
+ */
2127
+ function collectInlineEnums(roots, topLevelNames) {
2128
+ const promoted = /* @__PURE__ */ new Map();
2129
+ for (const root of roots) {
2130
+ const isSchemaRoot = root.kind === "Schema";
2131
+ for (const node of ast.collect(root, { schema: (schemaNode) => schemaNode })) {
2132
+ if (node.type !== "enum" || !node.name) continue;
2133
+ if (isSchemaRoot && node === root) continue;
2134
+ if (topLevelNames.has(node.name)) continue;
2135
+ if (!promoted.has(node.name)) promoted.set(node.name, {
2136
+ ...node,
2137
+ optional: void 0,
2138
+ nullish: void 0
2139
+ });
2140
+ }
2141
+ }
2142
+ return promoted;
2143
+ }
2144
+ /**
2145
+ * Replaces every promoted inline enum in `node` with a `ref` to its lifted definition, keeping the
2146
+ * occurrence's usage-slot and documentation fields.
2147
+ */
2148
+ function refPromotedEnums(node, promoted) {
2149
+ if (promoted.size === 0) return node;
2150
+ return ast.transform(node, { schema(schemaNode) {
2151
+ if (schemaNode.type !== "enum" || !schemaNode.name || !promoted.has(schemaNode.name)) return void 0;
2152
+ return ast.factory.createSchema({
2153
+ type: "ref",
2154
+ name: schemaNode.name,
2155
+ ref: `${SCHEMA_REF_PREFIX}${schemaNode.name}`,
2156
+ optional: schemaNode.optional,
2157
+ nullish: schemaNode.nullish,
2158
+ readOnly: schemaNode.readOnly,
2159
+ writeOnly: schemaNode.writeOnly,
2160
+ deprecated: schemaNode.deprecated,
2161
+ description: schemaNode.description,
2162
+ default: schemaNode.default,
2163
+ examples: schemaNode.examples
2164
+ });
2165
+ } });
2166
+ }
2167
+ //#endregion
2168
+ //#region src/schemaDiagnostics.ts
2169
+ /**
2170
+ * Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
2171
+ * top-level schema. Walks the node the parser produced during `preScan`, threading the RFC 6901
2172
+ * pointer as it descends so a nested field reports against its full path
2173
+ * (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
2174
+ * resolved schema is reported under its own walk. Reports land in the active build run, are a
2175
+ * no-op outside one, and repeats are deduped by the build.
2176
+ */
2177
+ function reportSchemaDiagnostics({ node, name }) {
2178
+ visit(node, `#/components/schemas/${escapePointerToken(name)}`);
2179
+ }
2180
+ /**
2181
+ * Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
2182
+ * property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
2183
+ */
2184
+ function escapePointerToken(token) {
2185
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
2186
+ }
2187
+ function visit(node, pointer) {
2188
+ if (node.deprecated) Diagnostics.report({
2189
+ code: Diagnostics.code.deprecated,
2190
+ severity: "info",
2191
+ message: "This schema is marked as deprecated.",
2192
+ location: {
2193
+ kind: "schema",
2194
+ pointer
2195
+ }
2196
+ });
2197
+ if (typeof node.format === "string" && !isHandledFormat(node.format)) Diagnostics.report({
2198
+ code: Diagnostics.code.unsupportedFormat,
2199
+ severity: "warning",
2200
+ message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
2201
+ help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
2202
+ location: {
2203
+ kind: "schema",
2204
+ pointer
2205
+ }
2206
+ });
2207
+ if (node.type === "object") {
2208
+ for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
2209
+ if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
2210
+ return;
2211
+ }
2212
+ if (node.type === "array") {
2213
+ for (const item of node.items ?? []) visit(item, `${pointer}/items`);
2214
+ return;
2215
+ }
2216
+ if (node.type === "tuple") {
2217
+ for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
2218
+ return;
2219
+ }
2220
+ if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
2221
+ }
2222
+ //#endregion
2223
+ //#region src/stream.ts
2224
+ /**
2225
+ * Reads the server URL from the document's `servers` array at `server.index`,
2226
+ * interpolating any `server.variables` into the URL template.
2227
+ *
2228
+ * Returns `null` when `server.index` is omitted or out of range.
2229
+ *
2230
+ * @example Resolve the first server
2231
+ * `resolveBaseUrl({ document, server: { index: 0 } })`
2232
+ *
2233
+ * @example Override a path variable
2234
+ * `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
2235
+ */
2236
+ function resolveBaseUrl({ document, server }) {
2237
+ const index = server?.index;
2238
+ const entry = index !== void 0 ? document.servers?.at(index) : void 0;
2239
+ return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
2240
+ }
1835
2241
  /**
1836
- * Parses an OpenAPI specification into Kubb's universal `InputNode` AST.
2242
+ * Parses every schema once to build the lookup structures that streaming needs upfront.
1837
2243
  *
1838
- * This is the main entry point for `@kubb/adapter-oas`. It converts OpenAPI/Swagger specs into a spec-agnostic tree
1839
- * that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here
1840
- * the tree is a pure data structure representing all schemas and operations.
2244
+ * Three things happen in this single pass:
2245
+ * - `refAliasMap` records schemas that are pure `$ref` aliases so the streaming pass can inline them.
2246
+ * - `enumNames` collects the names of every enum schema so plugins skip re-scanning the stream.
2247
+ * - `circularNames` runs cycle detection, which requires all nodes in memory simultaneously.
2248
+ * The `allNodes` array is local and drops out of scope as soon as this function returns.
1841
2249
  *
1842
- * Returns the AST root and a `nameMapping` for resolving schema references.
2250
+ * After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
2251
+ * Both are proportional to the number of aliases or discriminator parents, not total schema count.
2252
+ *
2253
+ * Each schema is parsed again during the streaming pass. This is intentional.
2254
+ * Holding the parsed nodes in memory here would defeat the streaming memory benefit.
1843
2255
  *
1844
2256
  * @example
1845
2257
  * ```ts
1846
- * import { parseOas } from '@kubb/adapter-oas'
1847
- *
1848
- * const document = await parseFromConfig(config)
1849
- * const { root, nameMapping } = parseOas(document, { dateType: 'date', contentType: 'application/json' })
2258
+ * const { refAliasMap, enumNames, circularNames } = preScan({
2259
+ * schemas,
2260
+ * parseSchema,
2261
+ * parserOptions,
2262
+ * discriminator: 'preserve',
2263
+ * })
1850
2264
  * ```
1851
2265
  */
1852
- function parseOas(document, options = {}) {
1853
- const { contentType, ...parserOptions } = options;
1854
- const mergedOptions = {
1855
- ...DEFAULT_PARSER_OPTIONS,
1856
- ...parserOptions
1857
- };
1858
- const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType });
1859
- const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({
1860
- document,
1861
- contentType
1862
- });
1863
- const schemas = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({
1864
- schema,
1865
- name
1866
- }, mergedOptions));
1867
- const paths = new BaseOas(document).getPaths();
1868
- const operations = Object.entries(paths).flatMap(([_path, methods]) => Object.entries(methods).map(([, operation]) => operation ? _parseOperation(mergedOptions, operation) : null).filter((op) => op !== null));
2266
+ function preScan({ schemas, parseSchema, parseOperation, document, parserOptions, discriminator, enums = "inline" }) {
2267
+ const allNodes = [];
2268
+ const refAliasMap = /* @__PURE__ */ new Map();
2269
+ const enumNames = [];
2270
+ const discriminatorParentNodes = [];
2271
+ for (const [name, schema] of Object.entries(schemas)) {
2272
+ const node = parseSchema({
2273
+ schema,
2274
+ name
2275
+ }, parserOptions);
2276
+ allNodes.push(node);
2277
+ reportSchemaDiagnostics({
2278
+ node,
2279
+ name
2280
+ });
2281
+ if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2282
+ if (ast.narrowSchema(node, ast.schemaTypes.enum) && node.name) enumNames.push(node.name);
2283
+ if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2284
+ }
2285
+ const circularNames = [...findCircularSchemas(allNodes)];
2286
+ const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
2287
+ let promotedEnums = null;
2288
+ if (enums === "root" && document && parseOperation) {
2289
+ const operationNodes = [];
2290
+ for (const operation of getOperations(document)) {
2291
+ const operationNode = parseOperation(parserOptions, operation);
2292
+ if (operationNode) operationNodes.push(operationNode);
2293
+ }
2294
+ promotedEnums = collectInlineEnums([...allNodes, ...operationNodes], new Set(Object.keys(schemas)));
2295
+ for (const name of promotedEnums.keys()) enumNames.push(name);
2296
+ }
1869
2297
  return {
1870
- root: ast.createInput({
1871
- schemas,
1872
- operations
1873
- }),
1874
- nameMapping
2298
+ refAliasMap,
2299
+ enumNames,
2300
+ circularNames,
2301
+ discriminatorChildMap,
2302
+ promotedEnums
1875
2303
  };
1876
2304
  }
2305
+ /**
2306
+ * Creates a lazy `InputNode<true>` from already-resolved adapter state.
2307
+ *
2308
+ * The schema and operation iterables each start a fresh parse pass on every
2309
+ * `[Symbol.asyncIterator]()` call. This lets multiple plugins consume the same
2310
+ * stream object independently without sharing a cursor or holding all nodes in memory.
2311
+ *
2312
+ * Ref aliases in `refAliasMap` are inlined during iteration: an alias entry is replaced
2313
+ * with its target's parsed node (but keeps the alias name) so plugins never receive bare `ref` nodes.
2314
+ *
2315
+ * @example
2316
+ * ```ts
2317
+ * const streamNode = createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, meta })
2318
+ * for await (const schema of streamNode.schemas) {
2319
+ * // each call to for-await restarts from the first schema
2320
+ * }
2321
+ * ```
2322
+ */
2323
+ function createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, promotedEnums, meta }) {
2324
+ const schemasIterable = { [Symbol.asyncIterator]() {
2325
+ return (async function* () {
2326
+ if (promotedEnums) for (const definition of promotedEnums.values()) yield definition;
2327
+ for (const [name, schema] of Object.entries(schemas)) {
2328
+ const alias = refAliasMap.get(name);
2329
+ if (alias?.name && schemas[alias.name]) {
2330
+ const aliasNode = {
2331
+ ...parseSchema({
2332
+ schema: schemas[alias.name],
2333
+ name: alias.name
2334
+ }, parserOptions),
2335
+ name
2336
+ };
2337
+ yield promotedEnums ? refPromotedEnums(aliasNode, promotedEnums) : aliasNode;
2338
+ continue;
2339
+ }
2340
+ const parsed = parseSchema({
2341
+ schema,
2342
+ name
2343
+ }, parserOptions);
2344
+ const node = discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)) : parsed;
2345
+ yield promotedEnums ? refPromotedEnums(node, promotedEnums) : node;
2346
+ }
2347
+ })();
2348
+ } };
2349
+ const operationsIterable = { [Symbol.asyncIterator]() {
2350
+ return (async function* () {
2351
+ for (const operation of getOperations(document)) {
2352
+ const node = parseOperation(parserOptions, operation);
2353
+ if (node) yield promotedEnums ? refPromotedEnums(node, promotedEnums) : node;
2354
+ }
2355
+ })();
2356
+ } };
2357
+ return ast.factory.createInput({
2358
+ stream: true,
2359
+ schemas: schemasIterable,
2360
+ operations: operationsIterable,
2361
+ meta
2362
+ });
2363
+ }
1877
2364
  //#endregion
1878
2365
  //#region src/adapter.ts
1879
2366
  /**
1880
- * Stable string identifier for the OAS adapter used in Kubb's adapter registry.
2367
+ * The `name` of `@kubb/adapter-oas`, used to identify this adapter in a Kubb config.
1881
2368
  */
1882
2369
  const adapterOasName = "oas";
1883
2370
  /**
1884
- * Creates the default OpenAPI / Swagger adapter for Kubb.
2371
+ * Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.1 specifications. Reads the
2372
+ * file at `input.path`, validates it, resolves the base URL, and converts every
2373
+ * schema and operation into the universal AST that every downstream plugin
2374
+ * consumes.
1885
2375
  *
1886
- * Parses the spec, optionally validates it, resolves the base URL, and converts
1887
- * everything into an `InputNode` that downstream plugins consume.
2376
+ * Configure once on `defineConfig`. The adapter's choices (date representation,
2377
+ * integer width, server URL) apply to every plugin in the build.
1888
2378
  *
1889
2379
  * @example
1890
2380
  * ```ts
@@ -1893,26 +2383,117 @@ const adapterOasName = "oas";
1893
2383
  * import { pluginTs } from '@kubb/plugin-ts'
1894
2384
  *
1895
2385
  * export default defineConfig({
1896
- * adapter: adapterOas({ dateType: 'date', serverIndex: 0 }),
1897
- * input: { path: './openapi.yaml' },
2386
+ * input: { path: './petStore.yaml' },
2387
+ * output: { path: './src/gen' },
2388
+ * adapter: adapterOas({
2389
+ * server: { index: 0 },
2390
+ * discriminator: 'propagate',
2391
+ * dateType: 'date',
2392
+ * }),
1898
2393
  * plugins: [pluginTs()],
1899
2394
  * })
1900
2395
  * ```
1901
2396
  */
1902
2397
  const adapterOas = createAdapter((options) => {
1903
- 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;
2398
+ const { validate = true, contentType, server, discriminator = "preserve", enums = "inline", 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;
2399
+ const parserOptions = {
2400
+ ...DEFAULT_PARSER_OPTIONS,
2401
+ dateType,
2402
+ integerType,
2403
+ unknownType,
2404
+ emptySchemaType,
2405
+ enumSuffix
2406
+ };
1904
2407
  let nameMapping = /* @__PURE__ */ new Map();
1905
- let parsedDocument;
1906
- let inputNode;
2408
+ let parsedDocument = null;
2409
+ const documentCache = /* @__PURE__ */ new WeakMap();
2410
+ const schemasCache = /* @__PURE__ */ new WeakMap();
2411
+ const schemaParserCache = /* @__PURE__ */ new WeakMap();
2412
+ const preScanCache = /* @__PURE__ */ new WeakMap();
2413
+ function ensureDocument(source) {
2414
+ const cached = documentCache.get(source);
2415
+ if (cached) return cached;
2416
+ const promise = (async () => {
2417
+ const fresh = await parseFromConfig(source);
2418
+ if (validate) await validateDocument(fresh);
2419
+ parsedDocument = fresh;
2420
+ return fresh;
2421
+ })();
2422
+ documentCache.set(source, promise);
2423
+ return promise;
2424
+ }
2425
+ function ensureSchemas(document) {
2426
+ const cached = schemasCache.get(document);
2427
+ if (cached) return cached;
2428
+ const promise = Promise.resolve().then(() => {
2429
+ const result = getSchemas(document, { contentType });
2430
+ nameMapping = result.nameMapping;
2431
+ return result.schemas;
2432
+ });
2433
+ schemasCache.set(document, promise);
2434
+ return promise;
2435
+ }
2436
+ function ensureSchemaParser(document) {
2437
+ const cached = schemaParserCache.get(document);
2438
+ if (cached) return cached;
2439
+ const parser = createSchemaParser({
2440
+ document,
2441
+ contentType
2442
+ });
2443
+ schemaParserCache.set(document, parser);
2444
+ return parser;
2445
+ }
2446
+ function ensurePreScan(document, schemas, parseSchema, parseOperation) {
2447
+ const cached = preScanCache.get(document);
2448
+ if (cached) return cached;
2449
+ const result = preScan({
2450
+ schemas,
2451
+ parseSchema,
2452
+ parseOperation,
2453
+ document,
2454
+ parserOptions,
2455
+ discriminator,
2456
+ enums
2457
+ });
2458
+ preScanCache.set(document, result);
2459
+ return result;
2460
+ }
2461
+ async function createStream(source) {
2462
+ const document = await ensureDocument(source);
2463
+ const schemas = await ensureSchemas(document);
2464
+ const { parseSchema, parseOperation } = ensureSchemaParser(document);
2465
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap, promotedEnums } = ensurePreScan(document, schemas, parseSchema, parseOperation);
2466
+ return createInputStream({
2467
+ schemas,
2468
+ parseSchema,
2469
+ parseOperation,
2470
+ document,
2471
+ parserOptions,
2472
+ refAliasMap,
2473
+ discriminatorChildMap,
2474
+ promotedEnums,
2475
+ meta: {
2476
+ title: document.info?.title,
2477
+ description: document.info?.description,
2478
+ version: document.info?.version,
2479
+ baseURL: resolveBaseUrl({
2480
+ document,
2481
+ server
2482
+ }),
2483
+ circularNames,
2484
+ enumNames
2485
+ }
2486
+ });
2487
+ }
1907
2488
  return {
1908
2489
  name: "oas",
1909
2490
  get options() {
1910
2491
  return {
1911
2492
  validate,
1912
2493
  contentType,
1913
- serverIndex,
1914
- serverVariables,
2494
+ server,
1915
2495
  discriminator,
2496
+ enums,
1916
2497
  dateType,
1917
2498
  integerType,
1918
2499
  unknownType,
@@ -1924,68 +2505,35 @@ const adapterOas = createAdapter((options) => {
1924
2505
  get document() {
1925
2506
  return parsedDocument;
1926
2507
  },
1927
- get inputNode() {
1928
- return inputNode;
1929
- },
1930
2508
  async validate(input, options) {
2509
+ await assertInputExists(input);
1931
2510
  await validateDocument(await parseDocument(input), options);
1932
2511
  },
1933
2512
  getImports(node, resolve) {
1934
- return ast.collectImports({
1935
- node,
1936
- nameMapping,
1937
- resolve: (schemaName) => {
1938
- const result = resolve(schemaName);
1939
- if (!result) return;
1940
- return ast.createImport({
1941
- name: [result.name],
1942
- path: result.path
1943
- });
1944
- }
1945
- });
2513
+ return collect(node, { schema(schemaNode) {
2514
+ const schemaRef = narrowSchema(schemaNode, "ref");
2515
+ if (!schemaRef?.ref) return null;
2516
+ const result = resolve(nameMapping.get(schemaRef.ref) ?? extractRefName(schemaRef.ref));
2517
+ if (!result) return null;
2518
+ return ast.factory.createImport({
2519
+ name: [result.name],
2520
+ path: result.path
2521
+ });
2522
+ } });
1946
2523
  },
1947
2524
  async parse(source) {
1948
- const document = await parseFromConfig(source);
1949
- if (validate) await validateDocument(document);
1950
- const server = serverIndex !== void 0 ? document.servers?.at(serverIndex) : void 0;
1951
- const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : void 0;
1952
- const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
1953
- contentType,
1954
- dateType,
1955
- integerType,
1956
- unknownType,
1957
- emptySchemaType,
1958
- enumSuffix
2525
+ const streamNode = await createStream(source);
2526
+ const [schemas, operations] = await Promise.all([Array.fromAsync(streamNode.schemas), Array.fromAsync(streamNode.operations)]);
2527
+ return ast.factory.createInput({
2528
+ schemas,
2529
+ operations,
2530
+ meta: streamNode.meta
1959
2531
  });
1960
- const node = discriminator === "inherit" ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot;
1961
- nameMapping = parsedNameMapping;
1962
- parsedDocument = document;
1963
- inputNode = ast.createInput({
1964
- ...node,
1965
- meta: {
1966
- title: document.info?.title,
1967
- description: document.info?.description,
1968
- version: document.info?.version,
1969
- baseURL
1970
- }
1971
- });
1972
- return inputNode;
1973
- }
2532
+ },
2533
+ stream: createStream
1974
2534
  };
1975
2535
  });
1976
2536
  //#endregion
1977
- //#region src/types.ts
1978
- /**
1979
- * Maps uppercase HTTP method names to lowercase for backwards compatibility.
1980
- *
1981
- * @example
1982
- * ```ts
1983
- * HttpMethods['GET'] // 'get'
1984
- * HttpMethods['POST'] // 'post'
1985
- * ```
1986
- */
1987
- const HttpMethods = Object.fromEntries(Object.entries(ast.httpMethods).map(([lower, upper]) => [upper, lower]));
1988
- //#endregion
1989
- export { HttpMethods, adapterOas, adapterOasName, mergeDocuments };
2537
+ export { adapterOas, adapterOasName };
1990
2538
 
1991
2539
  //# sourceMappingURL=index.js.map