@kubb/adapter-oas 5.0.0-beta.10 → 5.0.0-beta.100

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