@kubb/adapter-oas 5.0.0-beta.11 → 5.0.0-beta.110

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
@@ -16,40 +16,29 @@ var __copyProps = (to, from, except, desc) => {
16
16
  }
17
17
  return to;
18
18
  };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
20
  value: mod,
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");
26
+ let node_fs_promises = require("node:fs/promises");
25
27
  let node_path = require("node:path");
26
28
  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");
29
+ let yaml = require("yaml");
30
+ let _scalar_openapi_upgrader = require("@scalar/openapi-upgrader");
31
+ let api_ref_bundler = require("api-ref-bundler");
32
+ let _kubb_kit = require("@kubb/kit");
36
33
  //#region src/constants.ts
37
34
  /**
38
35
  * 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
36
  */
48
37
  const DEFAULT_PARSER_OPTIONS = {
49
38
  dateType: "string",
50
39
  integerType: "bigint",
51
- unknownType: "any",
52
- emptySchemaType: "any",
40
+ unknownType: "unknown",
41
+ emptySchemaType: "unknown",
53
42
  enumSuffix: "enum"
54
43
  };
55
44
  /**
@@ -64,32 +53,26 @@ const DEFAULT_PARSER_OPTIONS = {
64
53
  */
65
54
  const SCHEMA_REF_PREFIX = "#/components/schemas/";
66
55
  /**
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.
56
+ * HTTP methods that count as operations on an OpenAPI path item. Other keys
57
+ * (`parameters`, `summary`, `$ref`, vendor extensions) are skipped when iterating operations.
76
58
  */
77
- const MERGE_DEFAULT_VERSION = "1.0.0";
59
+ const SUPPORTED_METHODS = /* @__PURE__ */ new Set([
60
+ "get",
61
+ "put",
62
+ "post",
63
+ "delete",
64
+ "options",
65
+ "head",
66
+ "patch",
67
+ "trace"
68
+ ]);
78
69
  /**
79
70
  * Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
80
71
  *
81
72
  * A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
82
73
  * 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
74
  */
92
- const structuralKeys = new Set([
75
+ const structuralKeys = /* @__PURE__ */ new Set([
93
76
  "properties",
94
77
  "items",
95
78
  "additionalProperties",
@@ -99,21 +82,39 @@ const structuralKeys = new Set([
99
82
  "not"
100
83
  ]);
101
84
  /**
85
+ * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
86
+ * `int64`, `uint64` and the date/time family. Keep this in sync with the `convertFormat`
87
+ * special-cases in `parser.ts`. `isHandledFormat` reads it so the
88
+ * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
89
+ */
90
+ const specialCasedFormats = /* @__PURE__ */ new Set([
91
+ "int64",
92
+ "uint64",
93
+ "date-time",
94
+ "date",
95
+ "time"
96
+ ]);
97
+ /**
98
+ * Formats that describe a number, whether they resolve through `formatMap` or through the
99
+ * `convertFormat` special cases. On a `type: 'string'` schema these do not make the value a
100
+ * number: gRPC-gateway and other ProtoJSON producers send 64-bit integers as JSON strings.
101
+ *
102
+ * @see https://protobuf.dev/programming-guides/json/#int64-strings
103
+ */
104
+ const numericFormats = /* @__PURE__ */ new Set([
105
+ "int32",
106
+ "int64",
107
+ "uint64",
108
+ "float",
109
+ "double"
110
+ ]);
111
+ /**
102
112
  * Static map from OAS `format` strings to Kubb `SchemaType` values.
103
113
  *
104
114
  * 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
- * ```
115
+ * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled
116
+ * separately in the parser. `ipv4` and `ipv6` map to their own dedicated schema types. `hostname`
117
+ * and `idn-hostname` map to `'url'` as the closest generic string-format type.
117
118
  */
118
119
  const formatMap = {
119
120
  uuid: "uuid",
@@ -134,49 +135,29 @@ const formatMap = {
134
135
  };
135
136
  /**
136
137
  * 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
138
  */
145
139
  const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
146
140
  /**
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()`.
141
+ * Vendor extension keys that attach human-readable descriptions to enum values, checked in priority order.
149
142
  */
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
- ]);
143
+ const enumDescriptionKeys = ["x-enumDescriptions", "x-enum-descriptions"];
155
144
  //#endregion
156
- //#region src/discriminator.ts
145
+ //#region src/emit/discriminator/propagate.ts
157
146
  /**
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.
147
+ * Maps each child schema name to its discriminator patch data by scanning the given
148
+ * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
163
149
  *
164
- * Returns a new `InputNode` — the original is never mutated.
165
- *
166
- * @example
167
- * ```ts
168
- * const { root } = parseOas(document, options)
169
- * const next = applyDiscriminatorInheritance(root)
170
- * ```
150
+ * Called on a small pre-parsed subset of schemas (only the discriminator parents)
151
+ * rather than on all schemas at once.
171
152
  */
172
- function applyDiscriminatorInheritance(root) {
153
+ function buildDiscriminatorChildMap(schemas) {
173
154
  const childMap = /* @__PURE__ */ new Map();
174
- for (const schema of root.schemas) {
175
- let unionNode = _kubb_core.ast.narrowSchema(schema, "union");
155
+ for (const schema of schemas) {
156
+ let unionNode = _kubb_ast.ast.narrowSchema(schema, "union");
176
157
  if (!unionNode) {
177
- const intersectionMembers = _kubb_core.ast.narrowSchema(schema, "intersection")?.members;
158
+ const intersectionMembers = _kubb_ast.ast.narrowSchema(schema, "intersection")?.members;
178
159
  if (intersectionMembers) for (const m of intersectionMembers) {
179
- const u = _kubb_core.ast.narrowSchema(m, "union");
160
+ const u = _kubb_ast.ast.narrowSchema(m, "union");
180
161
  if (u) {
181
162
  unionNode = u;
182
163
  break;
@@ -186,52 +167,56 @@ function applyDiscriminatorInheritance(root) {
186
167
  if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue;
187
168
  const { discriminatorPropertyName, members } = unionNode;
188
169
  for (const member of members) {
189
- const intersectionNode = _kubb_core.ast.narrowSchema(member, "intersection");
170
+ const intersectionNode = _kubb_ast.ast.narrowSchema(member, "intersection");
190
171
  if (!intersectionNode?.members) continue;
191
- let refNode;
192
- let objNode;
172
+ let refNode = null;
173
+ let objNode = null;
193
174
  for (const m of intersectionNode.members) {
194
- refNode ??= _kubb_core.ast.narrowSchema(m, "ref");
195
- objNode ??= _kubb_core.ast.narrowSchema(m, "object");
175
+ refNode ??= _kubb_ast.ast.narrowSchema(m, "ref");
176
+ objNode ??= _kubb_ast.ast.narrowSchema(m, "object");
196
177
  }
197
178
  if (!refNode?.name || !objNode) continue;
198
179
  const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName);
199
- const enumNode = prop ? _kubb_core.ast.narrowSchema(prop.schema, "enum") : void 0;
180
+ const enumNode = prop ? _kubb_ast.ast.narrowSchema(prop.schema, "enum") : null;
200
181
  if (!enumNode?.enumValues?.length) continue;
201
182
  const enumValues = enumNode.enumValues.filter((v) => v !== null);
202
183
  if (!enumValues.length) continue;
203
184
  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
- });
185
+ if (!existing) {
186
+ childMap.set(refNode.name, {
187
+ propertyName: discriminatorPropertyName,
188
+ enumValues: [...enumValues]
189
+ });
190
+ continue;
191
+ }
192
+ existing.enumValues.push(...enumValues);
209
193
  }
210
194
  }
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
- } });
195
+ return childMap;
196
+ }
197
+ /**
198
+ * Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
199
+ * the discriminant property).
200
+ */
201
+ function patchDiscriminatorNode(node, entry) {
202
+ const objectNode = _kubb_ast.ast.narrowSchema(node, "object");
203
+ if (!objectNode) return node;
204
+ const { propertyName, enumValues } = entry;
205
+ const enumSchema = _kubb_ast.ast.factory.createSchema({
206
+ type: "enum",
207
+ enumValues
208
+ });
209
+ const newProp = _kubb_ast.ast.factory.createProperty({
210
+ name: propertyName,
211
+ required: true,
212
+ schema: enumSchema
213
+ });
214
+ const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
215
+ const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
216
+ return {
217
+ ...objectNode,
218
+ properties: newProperties
219
+ };
235
220
  }
236
221
  //#endregion
237
222
  //#region ../../internals/utils/src/casing.ts
@@ -245,470 +230,291 @@ function applyDiscriminatorInheritance(root) {
245
230
  function toCamelOrPascal(text, pascal) {
246
231
  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
232
  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);
233
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
250
234
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
251
235
  }
252
236
  /**
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.
265
- */
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("/");
269
- }
270
- /**
271
- * Converts `text` to camelCase.
272
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
273
- *
274
- * @example
275
- * camelCase('hello-world') // 'helloWorld'
276
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
277
- */
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
237
  * 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);
299
- }
300
- //#endregion
301
- //#region ../../internals/utils/src/object.ts
302
- /**
303
- * Returns `true` when `value` is a plain (non-null, non-array) object.
304
238
  *
305
- * @example
306
- * ```ts
307
- * isPlainObject({}) // true
308
- * isPlainObject([]) // false
309
- * isPlainObject(null) // false
310
- * ```
311
- */
312
- function isPlainObject(value) {
313
- return typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
314
- }
315
- /**
316
- * Recursively merges `source` into `target`, combining nested plain objects.
317
- * Arrays and non-object values from `source` override the corresponding values in `target`.
239
+ * @example Word boundaries
240
+ * `pascalCase('hello-world') // 'HelloWorld'`
318
241
  *
319
- * @example
320
- * ```ts
321
- * mergeDeep({ a: { x: 1 } }, { a: { y: 2 } })
322
- * // { a: { x: 1, y: 2 } }
323
- * ```
242
+ * @example With a suffix
243
+ * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
324
244
  */
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;
245
+ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
246
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
333
247
  }
334
248
  //#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
- ]);
249
+ //#region ../../internals/utils/src/errors.ts
423
250
  /**
424
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
251
+ * Extracts a human-readable message from any thrown value.
425
252
  *
426
253
  * @example
427
254
  * ```ts
428
- * isValidVarName('status') // true
429
- * isValidVarName('class') // false (reserved word)
430
- * isValidVarName('42foo') // false (starts with digit)
255
+ * getErrorMessage(new Error('oops')) // 'oops'
256
+ * getErrorMessage('plain string') // 'plain string'
431
257
  * ```
432
258
  */
433
- function isValidVarName(name) {
434
- if (!name || reservedWords.has(name)) return false;
435
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
259
+ function getErrorMessage(value) {
260
+ return value instanceof Error ? value.message : String(value);
436
261
  }
437
262
  //#endregion
438
- //#region ../../internals/utils/src/urlPath.ts
263
+ //#region ../../internals/utils/src/runtime.ts
439
264
  /**
440
- * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
265
+ * Detects the JavaScript runtime executing the current process and exposes its name and version.
441
266
  *
442
- * @example
443
- * const p = new URLPath('/pet/{petId}')
444
- * p.URL // '/pet/:petId'
445
- * p.template // '`/pet/${petId}`'
267
+ * Prefer the shared {@link runtime} instance over constructing your own.
446
268
  */
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
- }
269
+ var Runtime = class {
482
270
  /**
483
- * Converts the OpenAPI path to a TypeScript template literal string.
271
+ * `true` when the current process is running under Bun.
484
272
  *
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.
493
- *
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.
273
+ * Detection keys off the global `Bun` object rather than `process.versions`,
274
+ * because Bun polyfills `process.versions.node` for Node compatibility and would
275
+ * otherwise look like Node.
504
276
  *
505
277
  * @example
506
278
  * ```ts
507
- * new URLPath('/pet/{petId}').params // { petId: 'petId' }
508
- * new URLPath('/pet').params // undefined
279
+ * if (runtime.isBun) {
280
+ * await Bun.write(path, data)
281
+ * }
509
282
  * ```
510
283
  */
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;
284
+ get isBun() {
285
+ return typeof Bun !== "undefined";
517
286
  }
518
287
  /**
519
- * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
288
+ * `true` when the current process is running under Deno.
520
289
  */
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;
290
+ get isDeno() {
291
+ return typeof globalThis.Deno !== "undefined";
538
292
  }
539
293
  /**
540
- * Converts the OpenAPI path to a TypeScript template literal string.
541
- * An optional `replacer` can transform each extracted parameter name before interpolation.
294
+ * `true` when the current process is running under Node.
542
295
  *
543
- * @example
544
- * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
296
+ * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
545
297
  */
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("")}\``;
298
+ get isNode() {
299
+ return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
552
300
  }
553
301
  /**
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.
302
+ * Name of the runtime executing the current process.
557
303
  *
558
304
  * @example
559
305
  * ```ts
560
- * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
561
- * // { petId: 'petId', tagId: 'tagId' }
306
+ * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
562
307
  * ```
563
308
  */
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;
309
+ get name() {
310
+ if (this.isBun) return "bun";
311
+ if (this.isDeno) return "deno";
312
+ return "node";
571
313
  }
572
- /** Converts the OpenAPI path to Express-style colon syntax.
314
+ /**
315
+ * Version of the active runtime, or an empty string when it cannot be read.
573
316
  *
574
317
  * @example
575
318
  * ```ts
576
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
319
+ * runtime.version // '1.3.11' under Bun, '22.22.2' under Node
577
320
  * ```
578
321
  */
579
- toURLPath() {
580
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
322
+ get version() {
323
+ if (this.isBun) return process.versions.bun ?? "";
324
+ if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
325
+ return process.versions?.node ?? "";
581
326
  }
582
327
  };
328
+ /**
329
+ * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
330
+ */
331
+ const runtime = new Runtime();
583
332
  //#endregion
584
- //#region src/guards.ts
333
+ //#region ../../internals/utils/src/fs.ts
585
334
  /**
586
- * Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
335
+ * Resolves to `true` when the file or directory at `path` exists.
336
+ * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
587
337
  *
588
338
  * @example
589
339
  * ```ts
590
- * if (isOpenApiV2Document(doc)) {
591
- * // doc is OpenAPIV2.Document
340
+ * if (await exists('./kubb.config.ts')) {
341
+ * const content = await read('./kubb.config.ts')
592
342
  * }
593
343
  * ```
594
344
  */
595
- function isOpenApiV2Document(doc) {
596
- return !!doc && isPlainObject(doc) && !("openapi" in doc);
345
+ async function exists(path) {
346
+ if (runtime.isBun) return Bun.file(path).exists();
347
+ return (0, node_fs_promises.access)(path).then(() => true, () => false);
597
348
  }
598
349
  /**
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).
350
+ * Reads the file at `path` as a UTF-8 string.
351
+ * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
603
352
  *
604
353
  * @example
605
354
  * ```ts
606
- * isNullable({ type: 'string', nullable: true }) // true
607
- * isNullable({ type: ['string', 'null'] }) // true
608
- * isNullable({ type: 'string' }) // false
355
+ * const source = await read('./src/Pet.ts')
609
356
  * ```
610
357
  */
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;
358
+ async function read(path) {
359
+ if (runtime.isBun) return Bun.file(path).text();
360
+ return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
617
361
  }
362
+ //#endregion
363
+ //#region src/load/source.ts
364
+ const urlRegExp = /^https?:\/+/i;
618
365
  /**
619
- * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
620
- *
621
- * @example
622
- * ```ts
623
- * isReference({ $ref: '#/components/schemas/Pet' }) // true
624
- * isReference({ type: 'string' }) // false
625
- * ```
366
+ * Node reports every connection failure as `TypeError: fetch failed` and keeps the useful part
367
+ * (`connect ECONNREFUSED 127.0.0.1:8000`) on `cause`, one level deeper again when a host resolves
368
+ * to several addresses and the attempts collect into an `AggregateError`.
626
369
  */
627
- function isReference(obj) {
628
- return !!obj && typeof obj === "object" && "$ref" in obj;
370
+ function describeFetchFailure(error) {
371
+ if (error instanceof AggregateError && error.errors.length > 0) return describeFetchFailure(error.errors[0]);
372
+ if (error instanceof Error && error.cause instanceof Error) return describeFetchFailure(error.cause) || error.message;
373
+ return getErrorMessage(error);
374
+ }
375
+ function helpForStatus(status) {
376
+ if (status === 401 || status === 403) return "The server refused the request. Kubb sends no credentials, so serve the document without authentication or download it and set `input` to the local file.";
377
+ if (status === 404) return "Check the URL. Open it in a browser or with `curl` to confirm it serves the OpenAPI document.";
378
+ if (status >= 500) return "The server failed while serving the document. Check that it is healthy, then run Kubb again.";
379
+ return "Open the URL in a browser or with `curl` to see what the server returns, then point `input` at a URL that serves the OpenAPI document.";
380
+ }
381
+ async function fetchSource(url) {
382
+ try {
383
+ return await fetch(url);
384
+ } catch (error) {
385
+ throw new _kubb_core.Diagnostics.Error({
386
+ code: _kubb_core.Diagnostics.code.inputUnreachable,
387
+ severity: "error",
388
+ message: `Cannot reach ${url.href}: ${describeFetchFailure(error)}`,
389
+ help: "Check that the host is running and reachable from this machine. For a local server, start it and confirm the port matches the one in `input`.",
390
+ location: { kind: "config" },
391
+ cause: error instanceof Error ? error : void 0
392
+ });
393
+ }
394
+ }
395
+ async function readSource(sourcePath) {
396
+ if (urlRegExp.test(sourcePath)) {
397
+ const url = new URL(sourcePath);
398
+ const response = await fetchSource(url);
399
+ if (!response.ok) {
400
+ const status = response.statusText ? `${response.status} ${response.statusText}` : String(response.status);
401
+ throw new _kubb_core.Diagnostics.Error({
402
+ code: _kubb_core.Diagnostics.code.inputRequestFailed,
403
+ severity: "error",
404
+ message: `The server at ${url.href} answered with HTTP ${status} instead of the OpenAPI document.`,
405
+ help: helpForStatus(response.status),
406
+ location: { kind: "config" }
407
+ });
408
+ }
409
+ return response.text();
410
+ }
411
+ return read(sourcePath);
629
412
  }
630
413
  /**
631
- * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object.
414
+ * Reads and parses one source file or URL referenced during bundling: YAML/JSON is parsed into an
415
+ * object, Markdown is returned as-is (bundled inline rather than dereferenced).
632
416
  *
633
- * @example
634
- * ```ts
635
- * isDiscriminator({ discriminator: { propertyName: 'type', mapping: {} } }) // true
636
- * isDiscriminator({ discriminator: 'type' }) // false (Swagger 2 string form)
637
- * ```
417
+ * JSON is valid YAML, so `yaml`'s `parse` handles both, but its general-purpose parser (comments,
418
+ * anchors, block scalars, multi-document streams) does much more work than `JSON.parse` needs to.
419
+ * `JSON.parse` runs first and fails fast on the first non-JSON character, so a real YAML document
420
+ * falls through to `parse` at negligible cost.
638
421
  */
639
- function isDiscriminator(obj) {
640
- const record = obj;
641
- return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
422
+ async function resolveSource(sourcePath) {
423
+ const data = await readSource(sourcePath);
424
+ if (sourcePath.toLowerCase().endsWith(".md")) return data;
425
+ try {
426
+ return JSON.parse(data);
427
+ } catch {
428
+ return (0, yaml.parse)(data);
429
+ }
430
+ }
431
+ /**
432
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
433
+ * URLs are skipped: a remote input reports `KUBB_INPUT_REQUEST_FAILED` or `KUBB_INPUT_UNREACHABLE`
434
+ * from the request itself. A malformed but readable file is left for `parseDocument` to surface
435
+ * its parse error instead.
436
+ */
437
+ async function assertInputExists(input) {
438
+ if (URL.canParse(input)) return;
439
+ if (!await exists(input)) throw new _kubb_core.Diagnostics.Error({
440
+ code: _kubb_core.Diagnostics.code.inputNotFound,
441
+ severity: "error",
442
+ message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
443
+ help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
444
+ location: { kind: "config" }
445
+ });
642
446
  }
643
447
  //#endregion
644
- //#region src/factory.ts
448
+ //#region src/load/normalize.ts
645
449
  /**
646
- * Loads and dereferences an OpenAPI document, returning the raw `Document`.
450
+ * True when `node` contains a `$ref` pointing outside the current document (a relative path,
451
+ * absolute path, or URL). An internal `#/...` fragment does not count.
647
452
  *
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`.
453
+ * `Object.values` reads array elements and object property values alike, so the same recursion
454
+ * walks both without a separate array branch.
455
+ */
456
+ function hasExternalRef(node) {
457
+ if (!node || typeof node !== "object") return false;
458
+ const ref = node.$ref;
459
+ if (typeof ref === "string" && !ref.startsWith("#")) return true;
460
+ return Object.values(node).some(hasExternalRef);
461
+ }
462
+ /**
463
+ * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
651
464
  *
652
- * @example
653
- * ```ts
654
- * const document = await parseDocument('./openapi.yaml')
655
- * const document = await parse(rawDocumentObject, { canBundle: false })
656
- * ```
465
+ * External file schemas are hoisted into named `components.schemas` entries, so a property
466
+ * pointing at `./schemas/User.yaml` ends up referencing `#/components/schemas/User`. Generators
467
+ * can then emit a named type with an import instead of inlining the shape. Sources are read with
468
+ * the Bun-aware `read` util for local YAML and JSON files, and with `fetch` for HTTP(S) URLs.
469
+ *
470
+ * A document with no `$ref` outside itself has nothing to bundle, so it skips `api-ref-bundler`
471
+ * and returns as parsed. `bundle` only rewrites external refs into internal ones; on an
472
+ * all-internal document it is a no-op that still walks the whole tree to confirm that, which
473
+ * costs real time on a large spec.
474
+ *
475
+ * @example Local file
476
+ * `const document = await bundleDocument('./openapi.yaml')`
477
+ *
478
+ * @example Remote URL
479
+ * `const document = await bundleDocument('https://example.com/openapi.yaml')`
657
480
  */
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;
481
+ async function bundleDocument(pathOrUrl) {
482
+ const cache = /* @__PURE__ */ new Map();
483
+ const resolver = (sourcePath) => {
484
+ const key = urlRegExp.test(sourcePath) ? new URL(sourcePath).href : sourcePath;
485
+ const cached = cache.get(key);
486
+ if (cached) return cached;
487
+ const result = resolveSource(sourcePath);
488
+ cache.set(key, result);
489
+ return result;
490
+ };
491
+ const root = await resolver(pathOrUrl);
492
+ if (typeof root === "object" && root !== null && !hasExternalRef(root)) return root;
493
+ return await (0, api_ref_bundler.bundle)(pathOrUrl, resolver);
676
494
  }
677
495
  /**
678
- * Deep-merges multiple OpenAPI documents into a single `Document`.
496
+ * Loads and bundles an OpenAPI document, returning the raw `Document`.
679
497
  *
680
- * Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
681
- * Throws when the input array is empty.
498
+ * A string is a file path or URL: it is bundled via `api-ref-bundler`, hoisting external file
499
+ * schemas into named `components.schemas` entries so generators can emit named types and imports.
500
+ * An object is treated as an already-parsed document. Swagger 2.0 and OpenAPI 3.0 documents are
501
+ * up-converted to OpenAPI 3.1 via `@scalar/openapi-upgrader`.
682
502
  *
683
503
  * @example
684
504
  * ```ts
685
- * const document = await mergeDocuments(['./pets.yaml', './orders.yaml'])
505
+ * const document = await parseDocument('./openapi.yaml')
506
+ * const document = await parseDocument(rawDocumentObject)
686
507
  * ```
687
508
  */
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));
509
+ async function parseDocument(pathOrApi) {
510
+ if (typeof pathOrApi === "string") return parseDocument(await bundleDocument(pathOrApi));
511
+ return (0, _scalar_openapi_upgrader.upgrade)(pathOrApi, "3.1");
704
512
  }
705
513
  /**
706
514
  * Creates a `Document` from an `AdapterSource`.
707
515
  *
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.
516
+ * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
517
+ * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
712
518
  *
713
519
  * @example
714
520
  * ```ts
@@ -716,17 +522,33 @@ async function mergeDocuments(pathOrApi) {
716
522
  * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
717
523
  * ```
718
524
  */
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));
525
+ async function parseFromConfig(source) {
526
+ if (source.type === "data") return parseDocument(typeof source.data === "string" ? (0, yaml.parse)(source.data) : structuredClone(source.data));
527
+ if (URL.canParse(source.path)) return parseDocument(source.path);
528
+ const resolved = node_path.default.resolve(node_path.default.dirname(source.path), source.path);
529
+ await assertInputExists(resolved);
530
+ return parseDocument(resolved);
531
+ }
532
+ /**
533
+ * Asserts the parsed input is an OpenAPI or Swagger document.
534
+ *
535
+ * {@link validateDocument} keeps spec violations non-fatal so imperfect but usable documents still
536
+ * generate. That leniency also swallowed input that is not a document at all, which then produced
537
+ * an empty build with a success exit code. A missing version field is the one failure that cannot
538
+ * be a usable document, so it is fatal regardless of the `validate` option.
539
+ */
540
+ function assertDocument(document) {
541
+ if (document && ("openapi" in document || "swagger" in document)) return;
542
+ throw new _kubb_core.Diagnostics.Error({
543
+ code: _kubb_core.Diagnostics.code.invalidDocument,
544
+ severity: "error",
545
+ message: "The resolved `input` is not an OpenAPI or Swagger document: it declares no `openapi` or `swagger` version.",
546
+ help: "Point `input` at a document that declares `openapi` or `swagger`. If you pass an object, pass the spec itself rather than a wrapper such as `{ path }` or `{ data }`.",
547
+ location: { kind: "config" }
548
+ });
727
549
  }
728
550
  /**
729
- * Validates an OpenAPI document using `oas-normalize` with colorized error output.
551
+ * Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
730
552
  *
731
553
  * @example
732
554
  * ```ts
@@ -734,273 +556,130 @@ function parseFromConfig(source) {
734
556
  * ```
735
557
  */
736
558
  async function validateDocument(document, { throwOnError = false } = {}) {
559
+ const { compileErrors, validate } = await import("@readme/openapi-parser");
737
560
  try {
738
- await new oas_normalize.default(document, {
739
- enablePaths: true,
740
- colorizeErrors: true
741
- }).validate({ parser: { validate: { errors: { colorize: true } } } });
561
+ const result = await validate(structuredClone(document), { validate: { errors: { colorize: true } } });
562
+ if (!result.valid) throw new Error(compileErrors(result));
742
563
  } catch (error) {
743
564
  if (throwOnError) throw error;
744
565
  }
745
566
  }
746
567
  //#endregion
747
- //#region src/refs.ts
748
- const _refCache = /* @__PURE__ */ new WeakMap();
568
+ //#region src/oas.ts
749
569
  /**
750
- * Resolves a local JSON pointer reference from a document.
570
+ * Returns `true` when a schema should be treated as nullable.
571
+ *
572
+ * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
573
+ * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
574
+ */
575
+ function isNullable(schema) {
576
+ if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
577
+ const schemaType = schema?.type;
578
+ if (schemaType === "null") return true;
579
+ if (Array.isArray(schemaType)) return schemaType.includes("null");
580
+ return false;
581
+ }
582
+ /**
583
+ * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
584
+ */
585
+ function isReference(obj) {
586
+ return !!obj && typeof obj === "object" && "$ref" in obj;
587
+ }
588
+ /**
589
+ * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
590
+ * excluding the Swagger 2 string form.
591
+ */
592
+ function isDiscriminator(obj) {
593
+ const record = obj;
594
+ return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
595
+ }
596
+ /**
597
+ * Returns `true` when a schema is a binary payload: an octet-stream string body.
598
+ */
599
+ function isBinary(schema) {
600
+ return schema.type === "string" && schema.contentMediaType === "application/octet-stream";
601
+ }
602
+ /**
603
+ * MIME type fragments that mark a media type as JSON-like.
751
604
  *
752
- * Accepts `#/...` refs. Returns `null` for empty or non-local refs.
753
- * Throws when the pointer cannot be resolved.
605
+ * A content type is JSON when it contains any of these substrings. The `+json` entry catches
606
+ * structured-syntax suffixes such as `application/vnd.api+json`.
607
+ */
608
+ const jsonMimeFragments = [
609
+ "application/json",
610
+ "application/x-json",
611
+ "text/json",
612
+ "text/x-json",
613
+ "+json"
614
+ ];
615
+ /**
616
+ * Returns `true` when a media type string is JSON-like.
754
617
  *
755
618
  * @example
756
619
  * ```ts
757
- * resolveRef<SchemaObject>(document, '#/components/schemas/Pet') // SchemaObject | null
620
+ * isJsonMimeType('application/json') // true
621
+ * isJsonMimeType('application/vnd.api+json') // true
622
+ * isJsonMimeType('multipart/form-data') // false
758
623
  * ```
759
624
  */
760
- function resolveRef(document, $ref) {
761
- const origRef = $ref;
762
- $ref = $ref.trim();
763
- if ($ref === "") return null;
764
- if ($ref.startsWith("#")) $ref = globalThis.decodeURIComponent($ref.substring(1));
765
- else return null;
766
- let docCache = _refCache.get(document);
767
- if (!docCache) {
768
- docCache = /* @__PURE__ */ new Map();
769
- _refCache.set(document, docCache);
770
- }
771
- if (docCache.has($ref)) return docCache.get($ref);
772
- 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}.`);
774
- docCache.set($ref, current);
775
- return current;
625
+ function isJsonMimeType(mimeType) {
626
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
776
627
  }
777
628
  /**
778
- * Resolves a `$ref` object while preserving the original `$ref` field on the result.
779
- *
780
- * Useful for parser flows that need both dereferenced fields and pointer
781
- * identity (for naming/import purposes). Non-reference values are returned as-is.
629
+ * Picks a media-type entry from a `content` map: the first JSON-like media type, falling back to
630
+ * the first declared one. Returns `false` when `content` has no entries.
782
631
  *
783
632
  * @example
784
633
  * ```ts
785
- * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
786
- * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
634
+ * pickContentEntry({ 'application/xml': xmlEntry, 'application/json': jsonEntry })
635
+ * // ['application/json', jsonEntry]
787
636
  * ```
788
637
  */
789
- function dereferenceWithRef(document, schema) {
790
- if (isReference(schema)) return {
791
- ...schema,
792
- ...resolveRef(document, schema.$ref),
793
- $ref: schema.$ref
794
- };
795
- return schema;
638
+ function pickContentEntry(content) {
639
+ const mediaTypes = Object.keys(content);
640
+ const available = mediaTypes.find(isJsonMimeType) ?? mediaTypes[0];
641
+ return available ? [available, content[available]] : false;
796
642
  }
797
643
  //#endregion
798
- //#region src/resolvers.ts
644
+ //#region src/model/components.ts
799
645
  /**
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.
646
+ * Extracts the inline schema from a media-type `content` map.
647
+ *
648
+ * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
649
+ * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
803
650
  *
804
651
  * @example
805
652
  * ```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'
811
- * ```
812
- */
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;
823
- }
824
- /**
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.
827
- */
828
- function getSchemaType(format) {
829
- return formatMap[format] ?? null;
830
- }
831
- /**
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'`.
834
- */
835
- function getPrimitiveType(type) {
836
- if (type === "number" || type === "integer" || type === "bigint") return type;
837
- if (type === "boolean") return "boolean";
838
- return "string";
839
- }
840
- /**
841
- * Narrows a content-type string to the `MediaType` union Kubb recognizes, or returns `null`.
842
- */
843
- function getMediaType(contentType) {
844
- return Object.values(_kubb_core.ast.mediaTypes).includes(contentType) ? contentType : null;
845
- }
846
- /**
847
- * Returns all parameters for an operation, merging path-level and operation-level entries.
848
- * Operation-level parameters override path-level ones with the same `in:name` key.
849
- * `$ref` parameters resolve via `dereferenceWithRef` for backward compatibility.
850
- *
851
- * @example
852
- * ```ts
853
- * getParameters(document, operation)
854
- * // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
855
- * ```
856
- */
857
- function getParameters(document, operation) {
858
- const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
859
- const operationParams = resolveParams(operation.schema?.parameters || []);
860
- const pathItem = document.paths?.[operation.path];
861
- const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : []);
862
- const paramMap = /* @__PURE__ */ new Map();
863
- for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
864
- for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
865
- return Array.from(paramMap.values());
866
- }
867
- function getResponseBody(responseBody, contentType) {
868
- if (!responseBody) return false;
869
- if (isReference(responseBody)) return false;
870
- const body = responseBody;
871
- if (!body.content) return false;
872
- if (contentType) {
873
- if (!(contentType in body.content)) return false;
874
- return body.content[contentType];
875
- }
876
- let availableContentType;
877
- 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;
889
- }
890
- /**
891
- * Returns the response schema for a given operation and HTTP status code.
892
- *
893
- * Returns an empty object `{}` when no response body schema is available.
894
- *
895
- * @example
896
- * ```ts
897
- * getResponseSchema(document, operation, 200) // SchemaObject
898
- * getResponseSchema(document, operation, '4XX') // {}
899
- * ```
900
- */
901
- 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);
910
- if (responseBody === false) return {};
911
- const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
912
- if (!schema) return {};
913
- return dereferenceWithRef(document, schema);
914
- }
915
- /**
916
- * Returns the request body schema for an operation, or `null` when absent.
917
- *
918
- * @example
919
- * ```ts
920
- * getRequestSchema(document, operation) // SchemaObject | null
921
- * ```
922
- */
923
- function getRequestSchema(document, operation, options = {}) {
924
- if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
925
- const requestBody = operation.getRequestBody(options.contentType);
926
- if (requestBody === false) return null;
927
- const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
928
- if (!schema) return null;
929
- return dereferenceWithRef(document, schema);
930
- }
931
- /**
932
- * Flattens a keyword-only `allOf` into its parent schema.
933
- *
934
- * Only flattens when every member is a plain fragment — no `$ref` and no structural keywords
935
- * (see `structuralKeys`). Outer schema values take precedence over fragment values.
936
- * Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
937
- *
938
- * @example
939
- * ```ts
940
- * flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
941
- * // { type: 'object', properties: {}, description: 'A pet' }
942
- *
943
- * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
944
- * // returned unchanged — contains a $ref
945
- * ```
946
- */
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
- function flattenSchema(schema) {
958
- if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
959
- const allOfFragments = schema.allOf;
960
- if (allOfFragments.some((item) => (0, oas_types.isRef)(item))) return schema;
961
- 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;
965
- return merged;
966
- }
967
- /**
968
- * Extracts the inline schema from a media-type `content` map.
969
- *
970
- * Prefers `preferredContentType` when given; otherwise uses the first key in the map.
971
- * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
972
- *
973
- * @example
974
- * ```ts
975
- * extractSchemaFromContent(operation.content, 'application/json')
976
- * // SchemaObject | null
653
+ * extractSchemaFromContent(operation.content, 'application/json')
654
+ * // SchemaObject | null
977
655
  * ```
978
656
  */
979
657
  function extractSchemaFromContent(content, preferredContentType) {
980
658
  if (!content) return null;
981
659
  const firstContentType = Object.keys(content)[0] ?? "application/json";
982
660
  const schema = content[preferredContentType ?? firstContentType]?.schema;
983
- if (schema && "$ref" in schema) return null;
661
+ if (isReference(schema)) return null;
984
662
  return schema ?? null;
985
663
  }
986
664
  /**
987
665
  * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
988
666
  */
989
- function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
667
+ function* collectRefs(schema) {
990
668
  if (Array.isArray(schema)) {
991
- for (const item of schema) collectRefs(item, refs);
992
- return refs;
669
+ for (const item of schema) yield* collectRefs(item);
670
+ return;
993
671
  }
994
672
  if (schema && typeof schema === "object") for (const key in schema) {
995
673
  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);
674
+ if (!(key === "$ref" && typeof value === "string")) {
675
+ yield* collectRefs(value);
676
+ continue;
677
+ }
678
+ if (value.startsWith("#/components/schemas/")) {
679
+ const name = value.slice(21);
680
+ if (name) yield name;
681
+ }
1002
682
  }
1003
- return refs;
1004
683
  }
1005
684
  /**
1006
685
  * Returns a copy of `schemas` topologically sorted by `$ref` dependency.
@@ -1016,7 +695,7 @@ function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
1016
695
  */
1017
696
  function sortSchemas(schemas) {
1018
697
  const deps = /* @__PURE__ */ new Map();
1019
- for (const [name, schema] of Object.entries(schemas)) deps.set(name, Array.from(collectRefs(schema)));
698
+ for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
1020
699
  const sorted = [];
1021
700
  const visited = /* @__PURE__ */ new Set();
1022
701
  function visit(name, stack) {
@@ -1037,13 +716,16 @@ const semanticSuffixes = {
1037
716
  responses: "Response",
1038
717
  requestBodies: "Request"
1039
718
  };
1040
- function getSemanticSuffix(source) {
1041
- return semanticSuffixes[source];
1042
- }
1043
- function resolveSchemaRef(document, schema) {
1044
- if (!isReference(schema)) return schema;
1045
- const resolved = resolveRef(document, schema.$ref);
1046
- return resolved && !isReference(resolved) ? resolved : schema;
719
+ /**
720
+ * Picks the collision suffix for one name-colliding schema: none when the name is unique,
721
+ * a semantic suffix (`Schema`, `Response`, `Request`) when the collision spans sources, otherwise
722
+ * a numeric suffix (`2`, `3`, …) for same-source collisions.
723
+ */
724
+ function collisionSuffix({ isSingle, hasMultipleSources, source, index }) {
725
+ if (isSingle) return "";
726
+ if (hasMultipleSources) return semanticSuffixes[source];
727
+ if (index === 0) return "";
728
+ return String(index + 1);
1047
729
  }
1048
730
  /**
1049
731
  * Collects component schemas from one or more sources and resolves name collisions.
@@ -1057,19 +739,24 @@ function resolveSchemaRef(document, schema) {
1057
739
  *
1058
740
  * @example
1059
741
  * ```ts
1060
- * const { schemas, nameMapping } = getSchemas(document, { contentType: 'application/json' })
742
+ * const { schemas, renames } = getSchemas(document, { contentType: 'application/json' }, refs)
1061
743
  * ```
1062
744
  */
1063
- function getSchemas(document, { contentType }) {
745
+ function getSchemas(document, { contentType }, refs) {
1064
746
  const components = document.components;
747
+ function resolveSchemaRef(schema) {
748
+ if (!isReference(schema)) return schema;
749
+ const resolved = refs.resolve(schema.$ref);
750
+ return resolved && !isReference(resolved) ? resolved : schema;
751
+ }
1065
752
  const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
1066
- schema: resolveSchemaRef(document, schema),
753
+ schema: resolveSchemaRef(schema),
1067
754
  source: "schemas",
1068
755
  originalName: name
1069
756
  })), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
1070
757
  const schema = extractSchemaFromContent(item.content, contentType);
1071
758
  return schema ? [{
1072
- schema: resolveSchemaRef(document, schema),
759
+ schema: resolveSchemaRef(schema),
1073
760
  source,
1074
761
  originalName: name
1075
762
  }] : [];
@@ -1082,32 +769,222 @@ function getSchemas(document, { contentType }) {
1082
769
  normalizedNames.set(key, bucket);
1083
770
  }
1084
771
  const schemas = {};
1085
- const nameMapping = /* @__PURE__ */ new Map();
772
+ const renames = /* @__PURE__ */ new Map();
1086
773
  for (const [, items] of normalizedNames) {
1087
774
  const isSingle = items.length === 1;
1088
- let hasMultipleSources = false;
1089
- if (!isSingle) {
1090
- const firstSource = items[0].source;
1091
- for (let i = 1; i < items.length; i++) if (items[i].source !== firstSource) {
1092
- hasMultipleSources = true;
1093
- break;
1094
- }
1095
- }
775
+ const hasMultipleSources = !isSingle && new Set(items.map((item) => item.source)).size > 1;
1096
776
  items.forEach((item, index) => {
1097
- const suffix = isSingle ? "" : hasMultipleSources ? getSemanticSuffix(item.source) : index === 0 ? "" : String(index + 1);
777
+ const suffix = collisionSuffix({
778
+ isSingle,
779
+ hasMultipleSources,
780
+ source: item.source,
781
+ index
782
+ });
1098
783
  const uniqueName = item.originalName + suffix;
1099
784
  schemas[uniqueName] = item.schema;
1100
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
785
+ if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
1101
786
  });
1102
787
  }
1103
788
  return {
1104
789
  schemas: sortSchemas(schemas),
1105
- nameMapping
790
+ renames
1106
791
  };
1107
792
  }
793
+ //#endregion
794
+ //#region src/model/server.ts
795
+ /**
796
+ * Reads the server URL from the document's `servers` array at `server.index`,
797
+ * interpolating any `server.variables` into the URL template.
798
+ *
799
+ * Returns `null` when `server.index` is omitted or out of range.
800
+ *
801
+ * @example Resolve the first server
802
+ * `resolveBaseUrl({ document, server: { index: 0 } })`
803
+ *
804
+ * @example Override a path variable
805
+ * `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
806
+ */
807
+ function resolveBaseUrl({ document, server }) {
808
+ const index = server?.index;
809
+ const entry = index !== void 0 ? document.servers?.at(index) : void 0;
810
+ return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
811
+ }
812
+ /**
813
+ * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
814
+ * Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
815
+ * Throws if an override value is not in the variable's `enum` list.
816
+ *
817
+ * @example
818
+ * ```ts
819
+ * resolveServerUrl(
820
+ * { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
821
+ * { env: 'prod' },
822
+ * )
823
+ * // 'https://prod.api.example.com'
824
+ * ```
825
+ */
826
+ function resolveServerUrl(server, overrides) {
827
+ if (!server.variables) return server.url;
828
+ let url = server.url;
829
+ for (const [key, variable] of Object.entries(server.variables)) {
830
+ const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
831
+ if (value === void 0) continue;
832
+ if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.Diagnostics.Error({
833
+ code: _kubb_core.Diagnostics.code.invalidServerVariable,
834
+ severity: "error",
835
+ message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
836
+ help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
837
+ location: {
838
+ kind: "document",
839
+ pointer: "#/servers"
840
+ }
841
+ });
842
+ url = url.replaceAll(`{${key}}`, value);
843
+ }
844
+ return url;
845
+ }
846
+ //#endregion
847
+ //#region src/operation.ts
848
+ /**
849
+ * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
850
+ * with no leading or trailing dash.
851
+ */
852
+ function slugify(value) {
853
+ return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
854
+ }
855
+ /**
856
+ * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
857
+ */
858
+ function getOperationId({ path, method, schema }) {
859
+ const { operationId } = schema;
860
+ if (typeof operationId === "string" && operationId.length > 0) return operationId;
861
+ return `${method}_${slugify(path).toLowerCase()}`;
862
+ }
863
+ /**
864
+ * Returns the declared response status codes, skipping `x-` extensions and non-object entries.
865
+ */
866
+ function getResponseStatusCodes({ schema }) {
867
+ const responses = schema.responses;
868
+ if (!responses || isReference(responses)) return [];
869
+ return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
870
+ }
871
+ /**
872
+ * Returns the response object for a status code, resolving a `$ref` through `refs`. `false` when absent.
873
+ */
874
+ function getResponseByStatusCode({ operation, refs, statusCode }) {
875
+ const responses = operation.schema.responses;
876
+ if (!responses || isReference(responses)) return false;
877
+ return refs.deref(responses[statusCode]) ?? false;
878
+ }
879
+ /**
880
+ * Resolves the operation's request body, dereferencing a `$ref` through `refs`. Returns `null`
881
+ * when the operation has no request body or it cannot be resolved.
882
+ */
883
+ function getRequestBody({ operation, refs }) {
884
+ return refs.deref(operation.schema.requestBody);
885
+ }
886
+ /**
887
+ * Resolves the request body (a `$ref` through `refs`) and returns its content map, or
888
+ * `undefined` when the operation has no request body.
889
+ */
890
+ function getRequestBodyContent({ operation, refs }) {
891
+ return getRequestBody({
892
+ operation,
893
+ refs
894
+ })?.content;
895
+ }
896
+ /**
897
+ * Returns the request body media type. With `mediaType` set, returns that entry or `false`.
898
+ * Otherwise picks the first JSON-like media type, then the first declared one, as a
899
+ * `[mediaType, object]` tuple.
900
+ */
901
+ function getRequestContent({ operation, refs, mediaType }) {
902
+ const content = getRequestBodyContent({
903
+ operation,
904
+ refs
905
+ });
906
+ if (!content) return false;
907
+ if (mediaType) return mediaType in content ? content[mediaType] : false;
908
+ return pickContentEntry(content);
909
+ }
910
+ /**
911
+ * Returns the primary request content type. Prefers a JSON-like media type (the last one wins
912
+ * when several are declared), then the first declared one, defaulting to `'application/json'`.
913
+ */
914
+ function getRequestContentType({ operation, refs }) {
915
+ const content = getRequestBodyContent({
916
+ operation,
917
+ refs
918
+ });
919
+ const mediaTypes = content ? Object.keys(content) : [];
920
+ let result = mediaTypes[0] ?? "application/json";
921
+ for (const mt of mediaTypes) if (isJsonMimeType(mt)) result = mt;
922
+ return result;
923
+ }
924
+ /**
925
+ * Builds an `Operation` for every supported HTTP method on every path, in document order.
926
+ * `x-` path keys and unresolvable path-item `$ref`s are skipped.
927
+ *
928
+ * @example
929
+ * ```ts
930
+ * for (const operation of getOperations(document, refs)) {
931
+ * parseOperation(options, operation)
932
+ * }
933
+ * ```
934
+ */
935
+ function getOperations(document, refs) {
936
+ const operations = [];
937
+ const paths = document.paths;
938
+ if (!paths) return operations;
939
+ for (const path of Object.keys(paths)) {
940
+ if (path.startsWith("x-")) continue;
941
+ const pathItem = refs.deref(paths[path]);
942
+ if (!pathItem) continue;
943
+ const item = pathItem;
944
+ for (const method of Object.keys(item)) {
945
+ if (!SUPPORTED_METHODS.has(method)) continue;
946
+ const schema = item[method];
947
+ if (!schema || typeof schema !== "object") continue;
948
+ operations.push({
949
+ path,
950
+ method,
951
+ schema,
952
+ pathItem
953
+ });
954
+ }
955
+ }
956
+ return operations;
957
+ }
958
+ //#endregion
959
+ //#region src/emit/schemaShape.ts
960
+ /**
961
+ * Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
962
+ * Formats not in `formatMap` (e.g., `int64`, `uint64`, `date-time`) are handled separately by parser options.
963
+ */
964
+ function getSchemaType(format) {
965
+ return formatMap[format] ?? null;
966
+ }
967
+ /**
968
+ * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
969
+ * `specialCasedFormats` that `convertFormat` handles directly (int64, uint64, date-time, date, time). False means the format falls back to
970
+ * the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
971
+ * diagnostic in step with the parser as `formatMap` grows.
972
+ */
973
+ function isHandledFormat(format) {
974
+ return getSchemaType(format) !== null || specialCasedFormats.has(format);
975
+ }
976
+ /**
977
+ * Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
978
+ * Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
979
+ */
980
+ function getPrimitiveType(type) {
981
+ if (type === "number" || type === "integer" || type === "bigint") return type;
982
+ if (type === "boolean") return "boolean";
983
+ return "string";
984
+ }
1108
985
  /**
1109
986
  * 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`.
987
+ * Returns `null` when `dateType: false`, so the format falls through to `string`.
1111
988
  */
1112
989
  function getDateType(options, format) {
1113
990
  if (!options.dateType) return null;
@@ -1139,135 +1016,198 @@ function getDateType(options, format) {
1139
1016
  };
1140
1017
  }
1141
1018
  /**
1142
- * Collects the shared metadata fields passed to every `createSchema` call.
1019
+ * Reads a schema's numeric `exclusiveMinimum`/`exclusiveMaximum` bounds (the OAS 3.1 numeric
1020
+ * form). Either key is `undefined` when absent or, for the legacy OAS 3.0 boolean form, not a
1021
+ * number.
1143
1022
  */
1144
- function buildSchemaNode(schema, name, nullable, defaultValue) {
1023
+ function getExclusiveBounds(schema) {
1145
1024
  return {
1146
- name,
1147
- nullable,
1148
- title: schema.title,
1149
- description: schema.description,
1150
- deprecated: schema.deprecated,
1151
- readOnly: schema.readOnly,
1152
- writeOnly: schema.writeOnly,
1153
- default: defaultValue,
1154
- example: schema.example
1025
+ exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1026
+ exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
1155
1027
  };
1156
1028
  }
1157
1029
  /**
1158
- * Returns all request body content type keys for an operation.
1159
- *
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.
1163
- *
1164
- * @example
1165
- * ```ts
1166
- * getRequestBodyContentTypes(document, operation)
1167
- * // ['application/json', 'multipart/form-data']
1168
- * ```
1030
+ * Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
1031
+ * labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
1032
+ * 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
1169
1033
  */
1170
- function getRequestBodyContentTypes(document, operation) {
1171
- if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
1172
- const body = operation.schema.requestBody;
1173
- if (!body) return [];
1174
- return body.content ? Object.keys(body.content) : [];
1034
+ function extractExamples(schema) {
1035
+ if (Array.isArray(schema.examples)) return schema.examples;
1036
+ return schema.example !== void 0 ? [schema.example] : void 0;
1175
1037
  }
1176
- //#endregion
1177
- //#region src/parser.ts
1178
1038
  /**
1179
- * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
1180
- *
1181
- * 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.
1039
+ * Returns `true` when `fragment` carries any JSON Schema keyword that makes it
1040
+ * structurally significant on its own (see `structuralKeys`).
1183
1041
  *
1184
- * @note This is a defensive measure for robustness with non-compliant specs.
1042
+ * A fragment with a structural keyword can't be safely merged into a parent schema.
1185
1043
  */
1186
- function normalizeArrayEnum(schema) {
1187
- const normalizedItems = {
1188
- ...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
1189
- enum: schema.enum
1190
- };
1191
- const { enum: _enum, ...schemaWithoutEnum } = schema;
1192
- return {
1193
- ...schemaWithoutEnum,
1194
- items: normalizedItems
1195
- };
1044
+ function hasStructuralKeywords(fragment) {
1045
+ return Object.keys(fragment).some((key) => structuralKeys.has(key));
1196
1046
  }
1197
1047
  /**
1198
- * Factory function that creates schema and operation converters for a given OpenAPI context.
1048
+ * Flattens a keyword-only `allOf` into its parent schema.
1199
1049
  *
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.
1050
+ * Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
1051
+ * (see `structuralKeys`). Outer schema values take precedence over fragment values.
1052
+ * Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
1203
1053
  *
1204
- * @note Not exported; called internally by `parseOas()` and `parseSchema()`.
1205
- */
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
1054
+ * @example
1055
+ * ```ts
1056
+ * flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
1057
+ * // { type: 'object', properties: {}, description: 'A pet' }
1058
+ * ```
1059
+ *
1060
+ * @example
1061
+ * ```ts
1062
+ * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
1063
+ * // returned unchanged, contains a $ref
1064
+ * ```
1065
+ */
1066
+ function flattenSchema(schema) {
1067
+ if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
1068
+ const allOfFragments = schema.allOf;
1069
+ if (allOfFragments.some((item) => isReference(item))) return schema;
1070
+ if (allOfFragments.some(hasStructuralKeywords)) return schema;
1071
+ const { allOf: _allOf, ...rest } = schema;
1072
+ const merged = rest;
1073
+ for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
1074
+ return merged;
1075
+ }
1076
+ //#endregion
1077
+ //#region src/emit/createNode.ts
1078
+ /**
1079
+ * Builds a schema node from a converter's base context plus its type-specific fields. Every
1080
+ * converter needs the same metadata fields (`title`, `description`, `examples`, ...) alongside
1081
+ * whatever makes its node distinct; this folds both into one call.
1082
+ */
1083
+ function createNode({ schema, name, nullable, defaultValue }, extras) {
1084
+ return _kubb_ast.ast.factory.createSchema({
1085
+ name,
1086
+ nullable,
1087
+ title: schema.title,
1088
+ description: schema.description,
1089
+ deprecated: schema.deprecated,
1090
+ readOnly: schema.readOnly,
1091
+ writeOnly: schema.writeOnly,
1092
+ default: defaultValue,
1093
+ examples: extractExamples(schema),
1094
+ format: schema.format,
1095
+ ...extras
1096
+ });
1097
+ }
1098
+ //#endregion
1099
+ //#region src/emit/discriminator/preserve.ts
1100
+ /**
1101
+ * Creates a single-property object schema used as a discriminator literal.
1102
+ *
1103
+ * @example
1104
+ * ```ts
1105
+ * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
1106
+ * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
1107
+ * ```
1108
+ */
1109
+ function createDiscriminantNode({ propertyName, value }) {
1110
+ return _kubb_ast.ast.factory.createSchema({
1111
+ type: "object",
1112
+ primitive: "object",
1113
+ properties: [_kubb_ast.ast.factory.createProperty({
1114
+ name: propertyName,
1115
+ schema: _kubb_ast.ast.factory.createSchema({
1116
+ type: "enum",
1117
+ primitive: "string",
1118
+ enumValues: [value]
1119
+ }),
1120
+ required: true
1121
+ })]
1122
+ });
1123
+ }
1124
+ /**
1125
+ * Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
1126
+ *
1127
+ * @example
1128
+ * ```ts
1129
+ * findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
1130
+ * ```
1131
+ */
1132
+ function findDiscriminator(mapping, ref) {
1133
+ if (!mapping || !ref) return null;
1134
+ return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
1135
+ }
1136
+ /**
1137
+ * Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
1138
+ * node with either the shared-properties slice carrying that value, or a synthetic discriminant
1139
+ * literal. The referenced child schema's own definition is left untouched — narrowing happens only
1140
+ * at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
1141
+ * which additionally patches the child schema's own definition in a post-pass).
1142
+ */
1143
+ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, rawOptions, name, refs }) {
1144
+ function pickDiscriminatorPropertyNode(node, propertyName) {
1145
+ const discriminatorProperty = _kubb_ast.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1146
+ if (!discriminatorProperty) return null;
1147
+ return _kubb_ast.ast.factory.createSchema({
1148
+ type: "object",
1149
+ primitive: "object",
1150
+ properties: [discriminatorProperty]
1238
1151
  });
1239
1152
  }
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)
1153
+ function implicitDiscriminantValue(member) {
1154
+ if (!discriminator || discriminator.mapping || !isReference(member)) return null;
1155
+ const value = (0, _kubb_kit.extractRefName)(member.$ref);
1156
+ if (!value) return null;
1157
+ const variant = refs.resolve(member.$ref, { report: false });
1158
+ if (!variant) return null;
1159
+ const propertyName = discriminator.propertyName;
1160
+ const seen = /* @__PURE__ */ new Set([member.$ref]);
1161
+ function constrains(v) {
1162
+ const prop = v.properties?.[propertyName];
1163
+ const resolved = prop && isReference(prop) ? refs.resolve(prop.$ref, { report: false }) : prop;
1164
+ if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
1165
+ const composition = v.allOf ?? v.oneOf ?? v.anyOf;
1166
+ if (!composition) return false;
1167
+ return composition.some((m) => {
1168
+ if (!isReference(m)) return constrains(m);
1169
+ if (seen.has(m.$ref)) return false;
1170
+ seen.add(m.$ref);
1171
+ const r = refs.resolve(m.$ref, { report: false });
1172
+ return r ? constrains(r) : false;
1265
1173
  });
1266
1174
  }
1267
- const filteredDiscriminantValues = [];
1268
- const allOfMembers = schema.allOf.filter((item) => {
1175
+ return constrains(variant) ? null : value;
1176
+ }
1177
+ return unionMembers.map((s) => {
1178
+ const ref = isReference(s) ? s.$ref : void 0;
1179
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
1180
+ const memberNode = parse({
1181
+ schema: s,
1182
+ name
1183
+ }, rawOptions);
1184
+ if (!discriminatorValue || !discriminator) return memberNode;
1185
+ const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(_kubb_ast.ast.applyMacros(sharedPropertiesNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
1186
+ propertyName: discriminator.propertyName,
1187
+ values: [discriminatorValue]
1188
+ })], { depth: "shallow" }), discriminator.propertyName) : void 0;
1189
+ return _kubb_ast.ast.factory.createSchema({
1190
+ type: "intersection",
1191
+ members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
1192
+ propertyName: discriminator.propertyName,
1193
+ value: discriminatorValue
1194
+ })]
1195
+ });
1196
+ });
1197
+ }
1198
+ /**
1199
+ * Filters the discriminated members out of an `allOf` list: an `allOf` member that `$ref`s a
1200
+ * discriminated union's parent, where this schema is itself one of that union's children, is
1201
+ * dropped from `members` and its discriminant value collected instead — the same synthetic
1202
+ * literal `narrowUnionMembers` produces, so the emitted node stays a plain intersection rather
1203
+ * than nesting the whole parent union one level deeper.
1204
+ */
1205
+ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
1206
+ const discriminantValues = [];
1207
+ return {
1208
+ members: allOfMembers.filter((item) => {
1269
1209
  if (!isReference(item) || !name) return true;
1270
- const deref = resolveRef(document, item.$ref);
1210
+ const deref = refs.resolve(item.$ref);
1271
1211
  if (!deref || !isDiscriminator(deref)) return true;
1272
1212
  const parentUnion = deref.oneOf ?? deref.anyOf;
1273
1213
  if (!parentUnion) return true;
@@ -1275,410 +1215,948 @@ function createSchemaParser(ctx) {
1275
1215
  const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1276
1216
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1277
1217
  if (inOneOf || inMapping) {
1278
- const discriminatorValue = _kubb_core.ast.findDiscriminator(deref.discriminator.mapping, childRef);
1279
- if (discriminatorValue) filteredDiscriminantValues.push({
1218
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
1219
+ if (discriminatorValue) discriminantValues.push({
1280
1220
  propertyName: deref.discriminator.propertyName,
1281
1221
  value: discriminatorValue
1282
1222
  });
1283
1223
  return false;
1284
1224
  }
1285
1225
  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] },
1226
+ }),
1227
+ discriminantValues
1228
+ };
1229
+ }
1230
+ //#endregion
1231
+ //#region src/emit/converters/composition.ts
1232
+ /**
1233
+ * Converts a `$ref` schema into a `RefSchemaNode`.
1234
+ *
1235
+ * The resolved schema is stored in `node.schema`. Usage-site sibling fields
1236
+ * (description, readOnly, nullable, etc.) are stored directly on the ref node.
1237
+ * Use `syncSchemaRef(node)` in printers to get a merged view of both.
1238
+ * Circular refs are detected in `refs.resolveNode` and leave `schema` as `null`.
1239
+ */
1240
+ function convertRef({ schema, name, nullable, defaultValue, rawOptions, document, parse, refs, renames }) {
1241
+ const refPath = schema.$ref;
1242
+ const resolvedSchema = refPath ? refs.resolveNode(refPath, parse, rawOptions) : null;
1243
+ const ctx = {
1244
+ schema,
1245
+ name,
1246
+ nullable,
1247
+ defaultValue
1248
+ };
1249
+ if (refPath && document.components && !refs.exists(refPath)) return createNode(ctx, { type: "unknown" });
1250
+ const targetName = renames?.get(schema.$ref);
1251
+ return createNode(ctx, {
1252
+ type: "ref",
1253
+ name: (0, _kubb_kit.extractRefName)(schema.$ref),
1254
+ ref: schema.$ref,
1255
+ ...targetName ? { targetName } : {},
1256
+ schema: resolvedSchema
1257
+ });
1258
+ }
1259
+ /**
1260
+ * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
1261
+ */
1262
+ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
1263
+ if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
1264
+ const [memberSchema] = schema.allOf;
1265
+ const memberNode = parse({
1266
+ schema: memberSchema,
1267
+ name
1268
+ }, rawOptions);
1269
+ const { kind: _kind, ...memberNodeProps } = memberNode;
1270
+ const mergedNullable = nullable || memberNode.nullable || void 0;
1271
+ const mergedDefault = schema.default === null && mergedNullable ? void 0 : schema.default ?? memberNode.default;
1272
+ return _kubb_ast.ast.factory.createSchema({
1273
+ ...memberNodeProps,
1274
+ name,
1275
+ title: schema.title ?? memberNode.title,
1276
+ description: schema.description ?? memberNode.description,
1277
+ deprecated: schema.deprecated ?? memberNode.deprecated,
1278
+ nullable: mergedNullable,
1279
+ readOnly: schema.readOnly ?? memberNode.readOnly,
1280
+ writeOnly: schema.writeOnly ?? memberNode.writeOnly,
1281
+ default: mergedDefault,
1282
+ examples: extractExamples(schema) ?? memberNode.examples,
1283
+ pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0),
1284
+ format: schema.format ?? memberNode.format
1285
+ });
1286
+ }
1287
+ const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
1288
+ allOfMembers: schema.allOf,
1289
+ name,
1290
+ refs
1291
+ });
1292
+ const allOfMembers = discriminatedAllOf.map((s) => parse({
1293
+ schema: s,
1294
+ name
1295
+ }, rawOptions));
1296
+ const syntheticStart = allOfMembers.length;
1297
+ if (Array.isArray(schema.required) && schema.required.length) {
1298
+ const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
1299
+ const missingRequired = schema.required.filter((key) => !outerKeys.has(key));
1300
+ if (missingRequired.length) {
1301
+ const resolvedMembers = schema.allOf.flatMap((item) => {
1302
+ if (!isReference(item)) return [item];
1303
+ const deref = refs.resolve(item.$ref);
1304
+ return deref && !isReference(deref) ? [deref] : [];
1305
+ });
1306
+ for (const key of missingRequired) for (const resolved of resolvedMembers) {
1307
+ const prop = resolved.properties?.[key];
1308
+ if (prop) {
1309
+ const memberSchema = {
1310
+ properties: { [key]: prop },
1300
1311
  required: [key]
1301
- } }, rawOptions));
1312
+ };
1313
+ allOfMembers.push(parse({
1314
+ schema: memberSchema,
1315
+ name
1316
+ }, rawOptions));
1302
1317
  break;
1303
1318
  }
1304
1319
  }
1305
1320
  }
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)
1318
- });
1319
1321
  }
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]
1331
- });
1332
- }
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,
1345
- name
1346
- }, 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({
1374
- type: "intersection",
1375
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1376
- members: [unionNode, sharedPropertiesNode]
1377
- });
1378
- }
1379
- return _kubb_core.ast.createSchema({
1380
- type: "union",
1381
- ...unionBase,
1382
- members: _kubb_core.ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s }, rawOptions)))
1383
- });
1322
+ if (schema.properties) {
1323
+ const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
1324
+ allOfMembers.push(parse({ schema: schemaWithoutAllOf }, rawOptions));
1384
1325
  }
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",
1326
+ for (const { propertyName, value } of discriminantValues) allOfMembers.push(createDiscriminantNode({
1327
+ propertyName,
1328
+ value
1329
+ }));
1330
+ return createNode({
1331
+ schema,
1332
+ name,
1333
+ nullable,
1334
+ defaultValue
1335
+ }, {
1336
+ type: "intersection",
1337
+ members: [...(0, _kubb_kit.mergeAdjacentObjectsLazy)(allOfMembers.slice(0, syntheticStart)), ...(0, _kubb_kit.mergeAdjacentObjectsLazy)(allOfMembers.slice(syntheticStart))]
1338
+ });
1339
+ }
1340
+ /**
1341
+ * Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
1342
+ */
1343
+ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
1344
+ const ctx = {
1345
+ schema,
1346
+ name,
1347
+ nullable,
1348
+ defaultValue
1349
+ };
1350
+ const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
1351
+ const strategy = schema.oneOf ? "one" : "any";
1352
+ const unionExtras = {
1353
+ discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1354
+ strategy
1355
+ };
1356
+ const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
1357
+ const { oneOf: _o, anyOf: _a, discriminator: _d, ...memberBaseSchema } = schema;
1358
+ const sharedPropertiesNode = schema.properties ? parse({
1359
+ schema: memberBaseSchema,
1360
+ name
1361
+ }, rawOptions) : void 0;
1362
+ if (sharedPropertiesNode || discriminator) {
1363
+ const members = narrowUnionMembers({
1364
+ unionMembers,
1365
+ discriminator,
1366
+ sharedPropertiesNode,
1367
+ parse,
1368
+ rawOptions,
1393
1369
  name,
1394
- title: schema.title,
1395
- description: schema.description,
1396
- deprecated: schema.deprecated
1397
- });
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)
1404
- });
1405
- }
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
1370
+ refs
1420
1371
  });
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
1372
+ const unionNode = createNode(ctx, {
1373
+ type: "union",
1374
+ ...unionExtras,
1375
+ members
1445
1376
  });
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
1377
+ if (!sharedPropertiesNode) return unionNode;
1378
+ return createNode(ctx, {
1379
+ type: "intersection",
1380
+ members: [unionNode, sharedPropertiesNode]
1452
1381
  });
1453
- if (specialType === "ipv4") return _kubb_core.ast.createSchema({
1454
- ...base,
1382
+ }
1383
+ const unionNode = createNode(ctx, {
1384
+ type: "union",
1385
+ ...unionExtras,
1386
+ members: unionMembers.map((s) => parse({
1387
+ schema: s,
1388
+ name
1389
+ }, rawOptions))
1390
+ });
1391
+ return _kubb_ast.ast.applyMacros(unionNode, [_kubb_kit.macroSimplifyUnion], { depth: "shallow" });
1392
+ }
1393
+ /**
1394
+ * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
1395
+ * Only called once the multi-type rule's `match` has confirmed more than one non-`null` type
1396
+ * remains; a single remaining type (e.g. `['string', 'null']`) is handled as that type instead,
1397
+ * with nullability already folded in.
1398
+ */
1399
+ function convertMultiType({ schema, name, nullable, defaultValue, rawOptions, parse }) {
1400
+ const types = schema.type;
1401
+ const nonNullTypes = types.filter((t) => t !== "null");
1402
+ return createNode({
1403
+ schema,
1404
+ name,
1405
+ nullable: types.includes("null") || nullable || void 0,
1406
+ defaultValue
1407
+ }, {
1408
+ type: "union",
1409
+ members: nonNullTypes.map((t) => {
1410
+ return parse({
1411
+ schema: {
1412
+ ...schema,
1413
+ type: t
1414
+ },
1415
+ name
1416
+ }, rawOptions);
1417
+ })
1418
+ });
1419
+ }
1420
+ //#endregion
1421
+ //#region src/emit/converters/scalar.ts
1422
+ /**
1423
+ * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
1424
+ *
1425
+ * This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
1426
+ * from the array to its items sub-schema, so they are valid for downstream processing.
1427
+ *
1428
+ * @note A defensive measure for non-compliant specs.
1429
+ */
1430
+ function normalizeArrayEnum(schema) {
1431
+ const normalizedItems = {
1432
+ ...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
1433
+ enum: schema.enum
1434
+ };
1435
+ const { enum: _enum, ...schemaWithoutEnum } = schema;
1436
+ return {
1437
+ ...schemaWithoutEnum,
1438
+ items: normalizedItems
1439
+ };
1440
+ }
1441
+ /**
1442
+ * Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
1443
+ * and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
1444
+ */
1445
+ function createNullNode(schema, name, nullable) {
1446
+ return _kubb_ast.ast.factory.createSchema({
1447
+ type: "null",
1448
+ primitive: "null",
1449
+ name,
1450
+ title: schema.title,
1451
+ description: schema.description,
1452
+ deprecated: schema.deprecated,
1453
+ nullable,
1454
+ format: schema.format
1455
+ });
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 createNode({
1465
+ schema,
1466
+ name,
1467
+ nullable,
1468
+ defaultValue
1469
+ }, {
1470
+ type: "enum",
1471
+ primitive: constPrimitive,
1472
+ enumValues: [constValue]
1473
+ });
1474
+ }
1475
+ /**
1476
+ * Converts a format-annotated schema into a special-type `SchemaNode`. Only called once the
1477
+ * `format` rule's `match` has confirmed the format is handled (see `isHandledFormat`) and, for
1478
+ * a date-ish format, that `dateType` is not `false`.
1479
+ */
1480
+ function convertFormat(context) {
1481
+ const { schema, name, nullable, defaultValue, options, type } = context;
1482
+ const ctx = {
1483
+ schema,
1484
+ name,
1485
+ nullable,
1486
+ defaultValue
1487
+ };
1488
+ if (type === "string" && numericFormats.has(schema.format)) return convertString(context);
1489
+ if (schema.format === "int64" || schema.format === "uint64") return createNode(ctx, {
1490
+ type: options.integerType === "bigint" ? "bigint" : "integer",
1491
+ primitive: "integer",
1492
+ min: schema.minimum,
1493
+ max: schema.maximum,
1494
+ ...getExclusiveBounds(schema)
1495
+ });
1496
+ if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
1497
+ const dateType = getDateType(options, schema.format);
1498
+ if (dateType.type === "datetime") return createNode(ctx, {
1455
1499
  primitive: "string",
1456
- type: "ipv4"
1500
+ type: "datetime",
1501
+ offset: dateType.offset,
1502
+ local: dateType.local
1457
1503
  });
1458
- if (specialType === "ipv6") return _kubb_core.ast.createSchema({
1459
- ...base,
1504
+ return createNode(ctx, {
1460
1505
  primitive: "string",
1461
- type: "ipv6"
1506
+ type: dateType.type,
1507
+ representation: dateType.representation
1462
1508
  });
1463
- if (specialType === "uuid" || specialType === "email") return _kubb_core.ast.createSchema({
1464
- ...base,
1465
- primitive: "string",
1466
- type: specialType,
1509
+ }
1510
+ const specialType = getSchemaType(schema.format);
1511
+ return createNode(ctx, {
1512
+ primitive: specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string",
1513
+ type: specialType,
1514
+ ...specialType === "url" || specialType === "uuid" || specialType === "email" ? {
1467
1515
  min: schema.minLength,
1468
1516
  max: schema.maxLength
1469
- });
1470
- return _kubb_core.ast.createSchema({
1471
- ...base,
1472
- primitive: specialPrimitive,
1473
- type: specialType
1517
+ } : {}
1518
+ });
1519
+ }
1520
+ /**
1521
+ * Converts an `enum` schema into an `EnumSchemaNode`.
1522
+ */
1523
+ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
1524
+ if (type === "array") return parse({
1525
+ schema: normalizeArrayEnum(schema),
1526
+ name
1527
+ }, rawOptions);
1528
+ const nullInEnum = schema.enum.includes(null);
1529
+ const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
1530
+ if (nullInEnum && filteredValues.length === 0) return createNullNode(schema, name);
1531
+ const enumNullable = nullable || nullInEnum || void 0;
1532
+ const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1533
+ const enumPrimitive = getPrimitiveType(type);
1534
+ const ctx = {
1535
+ schema,
1536
+ name,
1537
+ nullable: enumNullable,
1538
+ defaultValue: enumDefault
1539
+ };
1540
+ const enumExtras = {
1541
+ type: "enum",
1542
+ primitive: enumPrimitive
1543
+ };
1544
+ const extensionKey = enumExtensionKeys.find((key) => key in schema);
1545
+ const descriptionKey = enumDescriptionKeys.find((key) => key in schema);
1546
+ if (extensionKey || descriptionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
1547
+ let enumPrimitiveType = "string";
1548
+ if (enumPrimitive === "number" || enumPrimitive === "integer") enumPrimitiveType = "number";
1549
+ else if (enumPrimitive === "boolean") enumPrimitiveType = "boolean";
1550
+ const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
1551
+ const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
1552
+ const uniqueValues = [...new Set(filteredValues)];
1553
+ const seenNames = /* @__PURE__ */ new Set();
1554
+ return createNode(ctx, {
1555
+ ...enumExtras,
1556
+ primitive: enumPrimitiveType,
1557
+ namedEnumValues: uniqueValues.map((value, index) => ({
1558
+ name: String(rawEnumNames?.[index] ?? value),
1559
+ value,
1560
+ primitive: enumPrimitiveType,
1561
+ description: rawEnumDescriptions?.[index]
1562
+ })).filter((entry) => {
1563
+ if (seenNames.has(entry.name)) return false;
1564
+ seenNames.add(entry.name);
1565
+ return true;
1566
+ })
1474
1567
  });
1475
1568
  }
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
1569
+ return createNode(ctx, {
1570
+ ...enumExtras,
1571
+ enumValues: [...new Set(filteredValues)]
1572
+ });
1573
+ }
1574
+ /**
1575
+ * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1576
+ */
1577
+ function convertString({ schema, name, nullable, defaultValue }) {
1578
+ return createNode({
1579
+ schema,
1580
+ name,
1581
+ nullable,
1582
+ defaultValue
1583
+ }, {
1584
+ type: "string",
1585
+ primitive: "string",
1586
+ min: schema.minLength,
1587
+ max: schema.maxLength,
1588
+ pattern: schema.pattern
1589
+ });
1590
+ }
1591
+ /**
1592
+ * Converts a `type: 'number'` or `type: 'integer'` schema.
1593
+ */
1594
+ function convertNumeric({ schema, name, nullable, defaultValue }, type) {
1595
+ return createNode({
1596
+ schema,
1597
+ name,
1598
+ nullable,
1599
+ defaultValue
1600
+ }, {
1601
+ type,
1602
+ primitive: type,
1603
+ min: schema.minimum,
1604
+ max: schema.maximum,
1605
+ ...getExclusiveBounds(schema),
1606
+ multipleOf: schema.multipleOf
1607
+ });
1608
+ }
1609
+ /**
1610
+ * Converts a `type: 'boolean'` schema.
1611
+ */
1612
+ function convertBoolean({ schema, name, nullable, defaultValue }) {
1613
+ return createNode({
1614
+ schema,
1615
+ name,
1616
+ nullable,
1617
+ defaultValue
1618
+ }, {
1619
+ type: "boolean",
1620
+ primitive: "boolean"
1621
+ });
1622
+ }
1623
+ /**
1624
+ * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
1625
+ * into a `blob` node.
1626
+ */
1627
+ function convertBinary({ schema, name, nullable, defaultValue }) {
1628
+ return createNode({
1629
+ schema,
1630
+ name,
1631
+ nullable,
1632
+ defaultValue
1633
+ }, {
1634
+ type: "blob",
1635
+ primitive: "string"
1636
+ });
1637
+ }
1638
+ //#endregion
1639
+ //#region src/emit/converters/structural.ts
1640
+ /**
1641
+ * Resolves a `true` or empty-object map schema (`additionalProperties`/`patternProperties`) to
1642
+ * `options.unknownType`, otherwise parses it as a regular schema.
1643
+ */
1644
+ function resolveMapSchema(mapSchema, options, parse, rawOptions) {
1645
+ if (mapSchema === true || typeof mapSchema === "object" && Object.keys(mapSchema).length === 0) return _kubb_ast.ast.factory.createSchema({ type: options.unknownType });
1646
+ return parse({ schema: mapSchema }, rawOptions);
1647
+ }
1648
+ /**
1649
+ * Names the inline enums on a property's schema, and on each item when the property is a tuple, from
1650
+ * the parent and property name. Wraps `macroEnumName` at the property construction site.
1651
+ */
1652
+ function nameEnums(node, options) {
1653
+ const macro = (0, _kubb_kit.macroEnumName)(options);
1654
+ const named = _kubb_ast.ast.applyMacros(node, [macro], { depth: "shallow" });
1655
+ const tupleNode = _kubb_ast.ast.narrowSchema(named, "tuple");
1656
+ if (tupleNode?.items) {
1657
+ const namedItems = tupleNode.items.map((item) => _kubb_ast.ast.applyMacros(item, [macro], { depth: "shallow" }));
1658
+ if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
1659
+ ...tupleNode,
1660
+ items: namedItems
1501
1661
  };
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({
1523
- ...enumBase,
1524
- enumValues: [...new Set(filteredValues)]
1525
- });
1526
1662
  }
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)
1663
+ return named;
1664
+ }
1665
+ /**
1666
+ * Converts an object-like schema into an `ObjectSchemaNode`.
1667
+ */
1668
+ function convertObject({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
1669
+ const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
1670
+ const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
1671
+ const resolvedPropSchema = propSchema;
1672
+ const propNullable = isNullable(resolvedPropSchema);
1673
+ const schemaNode = nameEnums(parse({
1674
+ schema: resolvedPropSchema,
1675
+ name: (0, _kubb_kit.childName)(name, propName)
1676
+ }, rawOptions), {
1677
+ parentName: name,
1678
+ propName,
1679
+ enumSuffix: options.enumSuffix
1574
1680
  });
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)
1681
+ return _kubb_ast.ast.factory.createProperty({
1682
+ name: propName,
1683
+ schema: {
1684
+ ...schemaNode,
1685
+ nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
1686
+ },
1687
+ required
1602
1688
  });
1689
+ }) : [];
1690
+ const additionalProperties = schema.additionalProperties;
1691
+ let additionalPropertiesNode;
1692
+ if (additionalProperties === true) additionalPropertiesNode = true;
1693
+ else if (additionalProperties) additionalPropertiesNode = resolveMapSchema(additionalProperties, options, parse, rawOptions);
1694
+ else additionalPropertiesNode = additionalProperties;
1695
+ const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
1696
+ const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, resolveMapSchema(patternSchema, options, parse, rawOptions)])) : void 0;
1697
+ const objectNode = createNode({
1698
+ schema,
1699
+ name,
1700
+ nullable,
1701
+ defaultValue
1702
+ }, {
1703
+ type: "object",
1704
+ primitive: "object",
1705
+ properties,
1706
+ additionalProperties: additionalPropertiesNode,
1707
+ patternProperties,
1708
+ minProperties: schema.minProperties,
1709
+ maxProperties: schema.maxProperties
1710
+ });
1711
+ if (isDiscriminator(schema) && schema.discriminator.mapping) {
1712
+ const discPropName = schema.discriminator.propertyName;
1713
+ const values = Object.keys(schema.discriminator.mapping);
1714
+ const enumName = name ? (0, _kubb_kit.enumPropName)(name, discPropName, options.enumSuffix) : void 0;
1715
+ return _kubb_ast.ast.applyMacros(objectNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
1716
+ propertyName: discPropName,
1717
+ values,
1718
+ enumName
1719
+ })], { depth: "shallow" });
1720
+ }
1721
+ return objectNode;
1722
+ }
1723
+ /**
1724
+ * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
1725
+ */
1726
+ function convertTuple({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
1727
+ const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
1728
+ const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? _kubb_ast.ast.factory.createSchema({ type: options.unknownType }) : parse({ schema: schema.items }, rawOptions);
1729
+ return createNode({
1730
+ schema,
1731
+ name,
1732
+ nullable,
1733
+ defaultValue
1734
+ }, {
1735
+ type: "tuple",
1736
+ primitive: "array",
1737
+ items: tupleItems,
1738
+ rest,
1739
+ min: schema.minItems,
1740
+ max: schema.maxItems
1741
+ });
1742
+ }
1743
+ /**
1744
+ * Converts a `type: 'array'` schema into an `ArraySchemaNode`.
1745
+ */
1746
+ function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
1747
+ const rawItems = schema.items;
1748
+ const itemName = rawItems?.enum?.length && name ? (0, _kubb_kit.enumPropName)(null, name, options.enumSuffix) : name;
1749
+ const items = rawItems ? [parse({
1750
+ schema: rawItems,
1751
+ name: itemName
1752
+ }, rawOptions)] : [];
1753
+ return createNode({
1754
+ schema,
1755
+ name,
1756
+ nullable,
1757
+ defaultValue
1758
+ }, {
1759
+ type: "array",
1760
+ primitive: "array",
1761
+ items,
1762
+ min: schema.minItems,
1763
+ max: schema.maxItems,
1764
+ unique: schema.uniqueItems ?? void 0
1765
+ });
1766
+ }
1767
+ //#endregion
1768
+ //#region src/emit/parseSchema.ts
1769
+ /**
1770
+ * Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
1771
+ * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
1772
+ * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
1773
+ * match/convert/fall-through contract.
1774
+ */
1775
+ const schemaRules = [
1776
+ {
1777
+ match: ({ schema }) => isReference(schema),
1778
+ convert: convertRef
1779
+ },
1780
+ {
1781
+ match: ({ schema }) => !!schema.allOf?.length,
1782
+ convert: convertAllOf
1783
+ },
1784
+ {
1785
+ match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length),
1786
+ convert: convertUnion
1787
+ },
1788
+ {
1789
+ match: ({ schema }) => "const" in schema && schema.const !== void 0,
1790
+ convert: convertConst
1791
+ },
1792
+ {
1793
+ match: ({ schema, options }) => {
1794
+ if (!schema.format) return false;
1795
+ if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") return options.dateType !== false;
1796
+ return isHandledFormat(schema.format);
1797
+ },
1798
+ convert: convertFormat
1799
+ },
1800
+ {
1801
+ match: ({ schema }) => isBinary(schema),
1802
+ convert: convertBinary
1803
+ },
1804
+ {
1805
+ match: ({ schema }) => Array.isArray(schema.type) && schema.type.filter((t) => t !== "null").length > 1,
1806
+ convert: convertMultiType
1807
+ },
1808
+ {
1809
+ match: ({ schema, type }) => !type && (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0),
1810
+ convert: convertString
1811
+ },
1812
+ {
1813
+ match: ({ schema, type }) => !type && (schema.minimum !== void 0 || schema.maximum !== void 0),
1814
+ convert: (ctx) => convertNumeric(ctx, "number")
1815
+ },
1816
+ {
1817
+ match: ({ schema }) => !!schema.enum?.length,
1818
+ convert: convertEnum
1819
+ },
1820
+ {
1821
+ match: ({ schema, type }) => type === "object" || !!schema.properties || !!schema.additionalProperties || "patternProperties" in schema,
1822
+ convert: convertObject
1823
+ },
1824
+ {
1825
+ match: ({ schema }) => "prefixItems" in schema,
1826
+ convert: convertTuple
1827
+ },
1828
+ {
1829
+ match: ({ schema, type }) => type === "array" || "items" in schema,
1830
+ convert: convertArray
1831
+ },
1832
+ {
1833
+ match: ({ type }) => type === "string",
1834
+ convert: convertString
1835
+ },
1836
+ {
1837
+ match: ({ type }) => type === "number",
1838
+ convert: (ctx) => convertNumeric(ctx, "number")
1839
+ },
1840
+ {
1841
+ match: ({ type }) => type === "integer",
1842
+ convert: (ctx) => convertNumeric(ctx, "integer")
1843
+ },
1844
+ {
1845
+ match: ({ type }) => type === "boolean",
1846
+ convert: convertBoolean
1847
+ },
1848
+ {
1849
+ match: ({ type }) => type === "null",
1850
+ convert: ({ schema, name, nullable }) => createNullNode(schema, name, nullable)
1603
1851
  }
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
- });
1852
+ ];
1853
+ //#endregion
1854
+ //#region src/refs.ts
1855
+ const _refCache = /* @__PURE__ */ new WeakMap();
1856
+ /**
1857
+ * Walks a local `#/...` JSON pointer against `document`, memoized per document. `applicable` is
1858
+ * `false` for an empty or non-local ref (the caller should not treat that as a failed lookup).
1859
+ * Shared by `resolveRef`'s reporting walk and `createRefs().resolve`'s silent walk, so both use
1860
+ * the same trimming and caching instead of two separate implementations.
1861
+ */
1862
+ function walkPointer(document, $ref) {
1863
+ const trimmed = $ref.trim();
1864
+ if (trimmed === "" || !trimmed.startsWith("#")) return {
1865
+ applicable: false,
1866
+ value: null
1867
+ };
1868
+ const pointer = globalThis.decodeURIComponent(trimmed.substring(1));
1869
+ let docCache = _refCache.get(document);
1870
+ if (!docCache) {
1871
+ docCache = /* @__PURE__ */ new Map();
1872
+ _refCache.set(document, docCache);
1623
1873
  }
1874
+ if (docCache.has(pointer)) return {
1875
+ applicable: true,
1876
+ value: docCache.get(pointer)
1877
+ };
1878
+ const current = pointer.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
1879
+ if (current) docCache.set(pointer, current);
1880
+ return {
1881
+ applicable: true,
1882
+ value: current ?? null
1883
+ };
1884
+ }
1885
+ /**
1886
+ * Resolves a local JSON pointer reference from a document.
1887
+ *
1888
+ * Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
1889
+ * resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
1890
+ * build there is no sink to collect it, so it throws instead.
1891
+ *
1892
+ * @example
1893
+ * ```ts
1894
+ * resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
1895
+ * ```
1896
+ */
1897
+ function resolveRef(document, $ref) {
1898
+ const { applicable, value } = walkPointer(document, $ref);
1899
+ if (!applicable) return null;
1900
+ if (value) return value;
1901
+ const diagnostic = {
1902
+ code: _kubb_core.Diagnostics.code.refNotFound,
1903
+ severity: "error",
1904
+ message: `Could not find a definition for ${$ref}.`,
1905
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
1906
+ location: {
1907
+ kind: "schema",
1908
+ pointer: $ref,
1909
+ ref: $ref
1910
+ }
1911
+ };
1912
+ if (!_kubb_core.Diagnostics.report(diagnostic)) throw new _kubb_core.Diagnostics.Error(diagnostic);
1913
+ return null;
1914
+ }
1915
+ /**
1916
+ * Resolves a `$ref` object while preserving the original `$ref` field on the result.
1917
+ *
1918
+ * Useful for parser flows that need both dereferenced fields and pointer
1919
+ * identity (for naming/import purposes). Non-reference values are returned as-is.
1920
+ *
1921
+ * @example
1922
+ * ```ts
1923
+ * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
1924
+ * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
1925
+ * ```
1926
+ */
1927
+ function dereferenceWithRef(document, schema) {
1928
+ if (isReference(schema)) return {
1929
+ ...schema,
1930
+ ...resolveRef(document, schema.$ref),
1931
+ $ref: schema.$ref
1932
+ };
1933
+ return schema;
1934
+ }
1935
+ /**
1936
+ * Creates the `$ref` resolution service for one document.
1937
+ *
1938
+ * Replaces what used to be six overlapping resolvers (a reporting walk, a silent walk, an
1939
+ * existence check, and a resolve-then-parse-into-a-node step, each with its own cache) with one
1940
+ * pointer walk and one explicit `report` contract for a missing ref: `report: true` (the default)
1941
+ * reports a `refNotFound` diagnostic (or throws outside a build), `report: false` resolves to
1942
+ * `null` silently for a speculative lookup.
1943
+ *
1944
+ * @example
1945
+ * ```ts
1946
+ * const refs = createRefs(document)
1947
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet')
1948
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
1949
+ * refs.exists('#/components/schemas/Pet')
1950
+ * refs.resolveNode('#/components/schemas/Pet', parseSchema)
1951
+ * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
1952
+ * ```
1953
+ */
1954
+ function createRefs(document) {
1955
+ const resolvedNodeCache = /* @__PURE__ */ new Map();
1956
+ const existenceCache = /* @__PURE__ */ new Map();
1957
+ const resolvingRefs = /* @__PURE__ */ new Set();
1624
1958
  /**
1625
- * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1959
+ * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
1960
+ * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
1961
+ * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
1962
+ * for a speculative lookup where a missing ref is not an error.
1626
1963
  */
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
- });
1964
+ function resolve(refPath, options) {
1965
+ if (options?.report === false) {
1966
+ const { applicable, value } = walkPointer(document, refPath);
1967
+ return applicable ? value : null;
1968
+ }
1969
+ return resolveRef(document, refPath);
1636
1970
  }
1637
1971
  /**
1638
- * Converts a `type: 'number'` or `type: 'integer'` schema.
1972
+ * Returns `true` when a `$ref` path resolves to a component the document actually defines.
1973
+ * A circular ref still resolves to an existing target, so this stays `true` for cycles and only
1974
+ * goes `false` for a `$ref` that points at a component the spec never declares. Memoized.
1639
1975
  */
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
- });
1976
+ function exists(refPath) {
1977
+ if (!existenceCache.has(refPath)) existenceCache.set(refPath, !!resolve(refPath, { report: false }));
1978
+ return existenceCache.get(refPath) ?? false;
1651
1979
  }
1652
1980
  /**
1653
- * Converts a `type: 'boolean'` schema.
1981
+ * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
1982
+ * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
1983
+ * resolved (e.g. a minimal document in a unit test).
1654
1984
  */
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
- });
1985
+ function resolveNode(refPath, parse, rawOptions) {
1986
+ if (resolvingRefs.has(refPath)) return null;
1987
+ if (!resolvedNodeCache.has(refPath)) {
1988
+ let resolved = null;
1989
+ try {
1990
+ const referenced = resolve(refPath);
1991
+ if (referenced) {
1992
+ resolvingRefs.add(refPath);
1993
+ resolved = parse({ schema: referenced }, rawOptions);
1994
+ resolvingRefs.delete(refPath);
1995
+ }
1996
+ } catch {}
1997
+ resolvedNodeCache.set(refPath, resolved);
1998
+ }
1999
+ return resolvedNodeCache.get(refPath) ?? null;
1661
2000
  }
1662
2001
  /**
1663
- * Converts an explicit `type: 'null'` schema.
2002
+ * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
2003
+ * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
2004
+ * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
2005
+ * returned as-is.
2006
+ *
2007
+ * @example
2008
+ * ```ts
2009
+ * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
2010
+ * ```
1664
2011
  */
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
- });
2012
+ function deref(value) {
2013
+ if (!isReference(value)) return value ? value : null;
2014
+ const resolved = resolve(value.$ref);
2015
+ return resolved && !isReference(resolved) ? resolved : null;
1675
2016
  }
2017
+ return {
2018
+ resolve,
2019
+ exists,
2020
+ resolveNode,
2021
+ deref
2022
+ };
2023
+ }
2024
+ //#endregion
2025
+ //#region src/model/operations.ts
2026
+ /**
2027
+ * Returns all parameters for an operation, merging path-level and operation-level entries.
2028
+ * Operation-level parameters override path-level ones with the same `in:name` key.
2029
+ * Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
2030
+ *
2031
+ * @example
2032
+ * ```ts
2033
+ * getParameters({ document, operation })
2034
+ * // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
2035
+ * ```
2036
+ */
2037
+ function getParameters({ document, operation }) {
2038
+ const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
2039
+ const operationParams = resolveParams(operation.schema?.parameters || []);
2040
+ const pathLevelParams = resolveParams(operation.pathItem.parameters ?? []);
2041
+ const paramMap = /* @__PURE__ */ new Map();
2042
+ for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
2043
+ for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
2044
+ return Array.from(paramMap.values());
2045
+ }
2046
+ function getResponseBody(responseBody, contentType) {
2047
+ if (!responseBody) return false;
2048
+ if (isReference(responseBody)) return false;
2049
+ const body = responseBody;
2050
+ if (!body.content) return false;
2051
+ if (contentType) return contentType in body.content ? body.content[contentType] : false;
2052
+ const picked = pickContentEntry(body.content);
2053
+ return picked ? picked[1] : false;
2054
+ }
2055
+ /**
2056
+ * Returns the response schema for a given operation and HTTP status code.
2057
+ *
2058
+ * Returns an empty object `{}` when no response body schema is available.
2059
+ *
2060
+ * @example
2061
+ * ```ts
2062
+ * getResponseSchema({ document, operation, refs, statusCode: 200 }) // SchemaObject
2063
+ * getResponseSchema({ document, operation, refs, statusCode: '4XX' }) // {}
2064
+ * ```
2065
+ */
2066
+ function getResponseSchema({ document, operation, refs, statusCode, options = {} }) {
2067
+ const responseBody = getResponseBody(getResponseByStatusCode({
2068
+ operation,
2069
+ refs,
2070
+ statusCode
2071
+ }), options.contentType);
2072
+ if (responseBody === false) return {};
2073
+ const schema = responseBody.schema;
2074
+ if (!schema) return {};
2075
+ return dereferenceWithRef(document, schema);
2076
+ }
2077
+ /**
2078
+ * Returns the request body schema for an operation, or `null` when absent.
2079
+ *
2080
+ * @example
2081
+ * ```ts
2082
+ * getRequestSchema({ document, operation, refs }) // SchemaObject | null
2083
+ * ```
2084
+ */
2085
+ function getRequestSchema({ document, operation, refs, options = {} }) {
2086
+ const requestBody = getRequestContent({
2087
+ operation,
2088
+ refs,
2089
+ mediaType: options.contentType
2090
+ });
2091
+ if (requestBody === false) return null;
2092
+ const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
2093
+ const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
2094
+ if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
2095
+ type: "string",
2096
+ contentMediaType: "application/octet-stream"
2097
+ };
2098
+ if (!schema) return null;
2099
+ return dereferenceWithRef(document, schema);
2100
+ }
2101
+ /**
2102
+ * Returns all request body content type keys for an operation, resolving a `$ref` requestBody
2103
+ * through `refs`.
2104
+ *
2105
+ * @example
2106
+ * ```ts
2107
+ * getRequestBodyContentTypes(operation, refs)
2108
+ * // ['application/json', 'multipart/form-data']
2109
+ * ```
2110
+ */
2111
+ function getRequestBodyContentTypes(operation, refs) {
2112
+ const body = getRequestBody({
2113
+ operation,
2114
+ refs
2115
+ });
2116
+ return body?.content ? Object.keys(body.content) : [];
2117
+ }
2118
+ /**
2119
+ * Returns all response content type keys for an operation at a given status code, resolving the
2120
+ * response `$ref` through `refs`.
2121
+ *
2122
+ * @example
2123
+ * ```ts
2124
+ * getResponseBodyContentTypes(operation, refs, 200)
2125
+ * // ['application/json', 'application/xml']
2126
+ * ```
2127
+ */
2128
+ function getResponseBodyContentTypes(operation, refs, statusCode) {
2129
+ const responseObj = getResponseByStatusCode({
2130
+ operation,
2131
+ refs,
2132
+ statusCode
2133
+ });
2134
+ if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
2135
+ const body = responseObj;
2136
+ return body.content ? Object.keys(body.content) : [];
2137
+ }
2138
+ //#endregion
2139
+ //#region src/parser.ts
2140
+ /**
2141
+ * Creates the schema and operation converters bound to one OpenAPI document.
2142
+ *
2143
+ * Takes the `$ref` service for this document (shared with the rest of the pipeline, see
2144
+ * `adapter.ts`) and owns the `parseSchema` recursion seam, then dispatches each schema through
2145
+ * the ordered `schemaRules` table from `emit/parseSchema.ts`. Every converter is a standalone
2146
+ * function that recurses through the `parse` function passed to it, so this file only wires
2147
+ * state to the converters.
2148
+ *
2149
+ * @internal
2150
+ */
2151
+ function createSchemaParser(ctx) {
2152
+ const document = ctx.document;
2153
+ const refs = ctx.refs;
1676
2154
  /**
1677
- * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
2155
+ * Converts an OAS `SchemaObject` into a `SchemaNode`.
1678
2156
  *
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).
2157
+ * Builds the per-schema context, then walks the ordered {@link schemaRules} table and returns
2158
+ * the first converter that produces a node. When none match, falls back to the configured
2159
+ * `emptySchemaType`.
1682
2160
  */
1683
2161
  function parseSchema({ schema, name }, rawOptions) {
1684
2162
  const options = {
@@ -1691,88 +2169,64 @@ function createSchemaParser(ctx) {
1691
2169
  name
1692
2170
  }, rawOptions);
1693
2171
  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 = {
2172
+ const context = {
1697
2173
  schema,
1698
2174
  name,
1699
2175
  nullable,
1700
- defaultValue,
1701
- type,
2176
+ defaultValue: schema.default === null && nullable ? void 0 : schema.default,
2177
+ type: Array.isArray(schema.type) ? schema.type[0] : schema.type,
1702
2178
  rawOptions,
1703
- options
2179
+ options,
2180
+ parse: parseSchema,
2181
+ document,
2182
+ refs,
2183
+ renames: ctx.renames
1704
2184
  };
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;
1712
- }
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({
2185
+ for (const rule of schemaRules) if (rule.match(context)) return rule.convert(context);
2186
+ const emptyType = options.emptySchemaType;
2187
+ return _kubb_ast.ast.factory.createSchema({
1748
2188
  type: emptyType,
1749
2189
  name,
1750
2190
  title: schema.title,
1751
- description: schema.description
2191
+ description: schema.description,
2192
+ format: schema.format
1752
2193
  });
1753
2194
  }
1754
2195
  /**
1755
2196
  * Converts a dereferenced OAS parameter object into a `ParameterNode`.
1756
2197
  */
1757
- function parseParameter(options, param) {
2198
+ function parseParameter(options, param, parentName) {
1758
2199
  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"],
2200
+ const paramName = param["name"];
2201
+ const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : void 0;
2202
+ const schema = param["schema"] ? parseSchema({
2203
+ schema: param["schema"],
2204
+ name: schemaName
2205
+ }, options) : _kubb_ast.ast.factory.createSchema({ type: options.unknownType });
2206
+ const style = param["style"];
2207
+ const explode = param["explode"];
2208
+ return _kubb_ast.ast.factory.createParameter({
2209
+ name: paramName,
1762
2210
  in: param["in"],
1763
2211
  schema: {
1764
2212
  ...schema,
1765
2213
  description: param["description"] ?? schema.description
1766
2214
  },
1767
- required
2215
+ required,
2216
+ ...style !== void 0 ? { style } : {},
2217
+ ...explode !== void 0 ? { explode } : {}
1768
2218
  });
1769
2219
  }
1770
2220
  /**
1771
2221
  * Reads the inline `requestBody` metadata (description / required) that OAS exposes
1772
- * outside the schema itself. Returns an empty object when the request body is missing or a `$ref`.
2222
+ * outside the schema itself, resolving a `$ref` requestBody through `refs`. Returns an
2223
+ * empty object when the request body is missing or cannot be resolved.
1773
2224
  */
1774
2225
  function getRequestBodyMeta(operation) {
1775
- const body = operation.schema.requestBody;
2226
+ const body = getRequestBody({
2227
+ operation,
2228
+ refs
2229
+ });
1776
2230
  if (!body) return { required: false };
1777
2231
  return {
1778
2232
  description: body.description,
@@ -1780,73 +2234,109 @@ function createSchemaParser(ctx) {
1780
2234
  };
1781
2235
  }
1782
2236
  /**
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
2237
  * Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
1795
2238
  * `$ref` entries are skipped since their flags live on the dereferenced target.
1796
2239
  */
1797
2240
  function collectPropertyKeysByFlag(schema, flag) {
1798
- if (!schema?.properties) return void 0;
2241
+ if (!schema?.properties) return null;
1799
2242
  const keys = [];
1800
2243
  for (const key in schema.properties) {
1801
2244
  const prop = schema.properties[key];
1802
2245
  if (prop && !isReference(prop) && prop[flag]) keys.push(key);
1803
2246
  }
1804
- return keys.length ? keys : void 0;
2247
+ return keys.length ? keys : null;
1805
2248
  }
1806
2249
  /**
1807
2250
  * Converts an OAS `Operation` into an `OperationNode`.
1808
2251
  */
1809
2252
  function parseOperation(options, operation) {
1810
- const parameters = getParameters(document, operation).map((param) => parseParameter(options, param));
1811
- const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
2253
+ const operationId = getOperationId(operation);
2254
+ const operationName = operationId ? pascalCase(operationId) : void 0;
2255
+ const parameters = getParameters({
2256
+ document,
2257
+ operation
2258
+ }).map((param) => parseParameter(options, param, operationName));
2259
+ const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(operation, refs);
1812
2260
  const requestBodyMeta = getRequestBodyMeta(operation);
2261
+ const requestBodyName = operationName ? `${operationName}Request` : void 0;
1813
2262
  const content = allContentTypes.flatMap((ct) => {
1814
- const schema = getRequestSchema(document, operation, { contentType: ct });
2263
+ const schema = getRequestSchema({
2264
+ document,
2265
+ operation,
2266
+ refs,
2267
+ options: { contentType: ct }
2268
+ });
1815
2269
  if (!schema) return [];
1816
- return [{
2270
+ return [_kubb_ast.ast.factory.createContent({
1817
2271
  contentType: ct,
1818
- schema: _kubb_core.ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
2272
+ schema: _kubb_ast.ast.optionality(parseSchema({
2273
+ schema,
2274
+ name: requestBodyName
2275
+ }, options), requestBodyMeta.required),
1819
2276
  keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
1820
- }];
2277
+ })];
1821
2278
  });
1822
2279
  const requestBody = content.length > 0 || requestBodyMeta.description ? {
1823
2280
  description: requestBodyMeta.description,
1824
2281
  required: requestBodyMeta.required || void 0,
1825
2282
  content: content.length > 0 ? content : void 0
1826
2283
  } : 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({
2284
+ const responses = getResponseStatusCodes(operation).map((statusCode) => {
2285
+ const responseObj = getResponseByStatusCode({
2286
+ operation,
2287
+ refs,
2288
+ statusCode
2289
+ });
2290
+ const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
2291
+ const description = typeof responseObj === "object" && responseObj !== null ? responseObj.description : void 0;
2292
+ const parseEntrySchema = (contentType) => {
2293
+ const raw = getResponseSchema({
2294
+ document,
2295
+ operation,
2296
+ refs,
2297
+ statusCode,
2298
+ options: { contentType }
2299
+ });
2300
+ return {
2301
+ schema: raw && Object.keys(raw).length > 0 ? parseSchema({
2302
+ schema: raw,
2303
+ name: responseName
2304
+ }, options) : _kubb_ast.ast.factory.createSchema({ type: options.emptySchemaType }),
2305
+ keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
2306
+ };
2307
+ };
2308
+ const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(operation, refs, statusCode)).map((contentType) => _kubb_ast.ast.factory.createContent({
2309
+ contentType,
2310
+ ...parseEntrySchema(contentType)
2311
+ }));
2312
+ if (content.length === 0) content.push(_kubb_ast.ast.factory.createContent({
2313
+ contentType: getRequestContentType({
2314
+ operation,
2315
+ refs
2316
+ }) || "application/json",
2317
+ ...parseEntrySchema(ctx.contentType)
2318
+ }));
2319
+ return _kubb_ast.ast.factory.createResponse({
1834
2320
  statusCode,
1835
2321
  description,
1836
- schema,
1837
- mediaType,
1838
- keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
2322
+ content
1839
2323
  });
1840
2324
  });
1841
- const urlPath = new URLPath(operation.path);
1842
- return _kubb_core.ast.createOperation({
1843
- operationId: operation.getOperationId(),
2325
+ const pickDoc = (key) => {
2326
+ const own = operation.schema[key];
2327
+ if (typeof own === "string") return own;
2328
+ const fallback = operation.pathItem[key];
2329
+ return typeof fallback === "string" ? fallback : void 0;
2330
+ };
2331
+ return _kubb_ast.ast.factory.createOperation({
2332
+ operationId,
2333
+ protocol: "http",
1844
2334
  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,
2335
+ path: operation.path,
2336
+ tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
2337
+ summary: pickDoc("summary") || void 0,
2338
+ description: pickDoc("description") || void 0,
2339
+ deprecated: operation.schema.deprecated || void 0,
1850
2340
  parameters,
1851
2341
  requestBody,
1852
2342
  responses
@@ -1858,59 +2348,131 @@ function createSchemaParser(ctx) {
1858
2348
  parseParameter
1859
2349
  };
1860
2350
  }
2351
+ //#endregion
2352
+ //#region src/promoteEnums.ts
1861
2353
  /**
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
- * ```
2354
+ * Collects inline enums to lift to the top level, keyed by the name the parser derived for them
2355
+ * (e.g. `PetStatusEnum`). An enum already defined as a top-level component is left as-is, and a
2356
+ * name that recurs maps to the first definition so each name yields one shared type.
1877
2357
  */
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
2358
+ function collectInlineEnums(roots, topLevelNames) {
2359
+ const promoted = /* @__PURE__ */ new Map();
2360
+ for (const root of roots) {
2361
+ const isSchemaRoot = root.kind === "Schema";
2362
+ for (const node of _kubb_ast.ast.collect(root, { schema: (schemaNode) => schemaNode })) {
2363
+ if (node.type !== "enum" || !node.name) continue;
2364
+ if (isSchemaRoot && node === root) continue;
2365
+ if (topLevelNames.has(node.name)) continue;
2366
+ if (!promoted.has(node.name)) promoted.set(node.name, {
2367
+ ...node,
2368
+ optional: void 0,
2369
+ nullish: void 0
2370
+ });
2371
+ }
2372
+ }
2373
+ return promoted;
2374
+ }
2375
+ /**
2376
+ * Replaces every promoted inline enum in `node` with a `ref` to its lifted definition, keeping the
2377
+ * occurrence's usage-slot and documentation fields.
2378
+ */
2379
+ function refPromotedEnums(node, promoted) {
2380
+ if (promoted.size === 0) return node;
2381
+ return _kubb_ast.ast.transform(node, { schema(schemaNode) {
2382
+ if (schemaNode.type !== "enum" || !schemaNode.name || !promoted.has(schemaNode.name)) return void 0;
2383
+ return _kubb_ast.ast.factory.createSchema({
2384
+ type: "ref",
2385
+ name: schemaNode.name,
2386
+ ref: `${SCHEMA_REF_PREFIX}${schemaNode.name}`,
2387
+ optional: schemaNode.optional,
2388
+ nullish: schemaNode.nullish,
2389
+ readOnly: schemaNode.readOnly,
2390
+ writeOnly: schemaNode.writeOnly,
2391
+ deprecated: schemaNode.deprecated,
2392
+ description: schemaNode.description,
2393
+ default: schemaNode.default,
2394
+ examples: schemaNode.examples
2395
+ });
2396
+ } });
2397
+ }
2398
+ //#endregion
2399
+ //#region src/schemaDiagnostics.ts
2400
+ /**
2401
+ * Scans one freshly converted top-level schema in a single walk, so the post-convert pass never
2402
+ * sweeps the same nodes twice. It reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`,
2403
+ * `KUBB_DEPRECATED`) and returns the names of every schema the node references, ready to feed the
2404
+ * circular-dependency graph.
2405
+ *
2406
+ * Walks the node the parser produced, threading the RFC 6901 pointer as it descends so a nested
2407
+ * field reports against its full path (`#/components/schemas/Pet/properties/owner/properties/name`).
2408
+ * Refs are recorded by name and not followed, so the resolved schema is reported under its own walk.
2409
+ * Reports land in the active build run, are a no-op outside one, and repeats are deduped by the build.
2410
+ */
2411
+ function scanSchema({ node, name }) {
2412
+ const refs = /* @__PURE__ */ new Set();
2413
+ visit(node, `#/components/schemas/${escapePointerToken(name)}`, refs);
2414
+ return refs;
2415
+ }
2416
+ /**
2417
+ * Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
2418
+ * property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
2419
+ */
2420
+ function escapePointerToken(token) {
2421
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
2422
+ }
2423
+ function visit(node, pointer, refs) {
2424
+ if (node.type === "ref") {
2425
+ const refName = (0, _kubb_ast.resolveRefName)(node);
2426
+ if (refName) refs.add(refName);
2427
+ }
2428
+ if (node.deprecated) _kubb_core.Diagnostics.report({
2429
+ code: _kubb_core.Diagnostics.code.deprecated,
2430
+ severity: "info",
2431
+ message: "This schema is marked as deprecated.",
2432
+ location: {
2433
+ kind: "schema",
2434
+ pointer
2435
+ }
1888
2436
  });
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
- };
2437
+ if (typeof node.format === "string" && !isHandledFormat(node.format)) _kubb_core.Diagnostics.report({
2438
+ code: _kubb_core.Diagnostics.code.unsupportedFormat,
2439
+ severity: "warning",
2440
+ message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
2441
+ help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
2442
+ location: {
2443
+ kind: "schema",
2444
+ pointer
2445
+ }
2446
+ });
2447
+ if (node.type === "object") {
2448
+ for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`, refs);
2449
+ if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`, refs);
2450
+ return;
2451
+ }
2452
+ if (node.type === "array") {
2453
+ for (const item of node.items ?? []) visit(item, `${pointer}/items`, refs);
2454
+ return;
2455
+ }
2456
+ if (node.type === "tuple") {
2457
+ for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`, refs);
2458
+ return;
2459
+ }
2460
+ if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`, refs);
1902
2461
  }
1903
2462
  //#endregion
1904
2463
  //#region src/adapter.ts
1905
2464
  /**
1906
- * Stable string identifier for the OAS adapter used in Kubb's adapter registry.
2465
+ * The `name` of `@kubb/adapter-oas`, used to identify this adapter in a Kubb config.
1907
2466
  */
1908
2467
  const adapterOasName = "oas";
1909
2468
  /**
1910
- * Creates the default OpenAPI / Swagger adapter for Kubb.
2469
+ * Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.1 specifications. Reads the
2470
+ * spec from `input` (a file path, URL, inline content, or parsed object), validates
2471
+ * it, resolves the base URL, and converts every schema and operation into the
2472
+ * universal AST that every downstream plugin consumes.
1911
2473
  *
1912
- * Parses the spec, optionally validates it, resolves the base URL, and converts
1913
- * everything into an `InputNode` that downstream plugins consume.
2474
+ * Configure once on `defineConfig`. The adapter's choices (date representation,
2475
+ * integer width, server URL) apply to every plugin in the build.
1914
2476
  *
1915
2477
  * @example
1916
2478
  * ```ts
@@ -1919,102 +2481,149 @@ const adapterOasName = "oas";
1919
2481
  * import { pluginTs } from '@kubb/plugin-ts'
1920
2482
  *
1921
2483
  * export default defineConfig({
1922
- * adapter: adapterOas({ dateType: 'date', serverIndex: 0 }),
1923
- * input: { path: './openapi.yaml' },
2484
+ * input: './petStore.yaml',
2485
+ * output: { path: './src/gen' },
2486
+ * adapter: adapterOas({
2487
+ * server: { index: 0 },
2488
+ * discriminator: 'propagate',
2489
+ * dateType: 'date',
2490
+ * }),
1924
2491
  * plugins: [pluginTs()],
1925
2492
  * })
1926
2493
  * ```
1927
2494
  */
1928
2495
  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;
2496
+ 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;
2497
+ const parserOptions = {
2498
+ ...DEFAULT_PARSER_OPTIONS,
2499
+ dateType,
2500
+ integerType,
2501
+ unknownType,
2502
+ emptySchemaType,
2503
+ enumSuffix
2504
+ };
2505
+ let parsedDocument = null;
2506
+ const inputCache = /* @__PURE__ */ new WeakMap();
2507
+ function parseInput({ document, refs, schemas, parser }) {
2508
+ const { parseSchema, parseOperation } = parser;
2509
+ const parsedByName = /* @__PURE__ */ new Map();
2510
+ const refAliasMap = /* @__PURE__ */ new Map();
2511
+ const enumNames = [];
2512
+ const discriminatorParentNodes = [];
2513
+ const refGraph = /* @__PURE__ */ new Map();
2514
+ for (const [name, schema] of Object.entries(schemas)) {
2515
+ const node = parseSchema({
2516
+ schema,
2517
+ name
2518
+ }, parserOptions);
2519
+ parsedByName.set(name, node);
2520
+ const refs = scanSchema({
2521
+ node,
2522
+ name
2523
+ });
2524
+ if (node.name) refGraph.set(node.name, refs);
2525
+ if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2526
+ if ((0, _kubb_ast.narrowSchema)(node, "enum") && node.name) enumNames.push(node.name);
2527
+ if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2528
+ }
2529
+ const circularNames = [...(0, _kubb_ast.findCircularSchemasFromGraph)(refGraph)];
2530
+ const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
2531
+ const operationNodes = [];
2532
+ for (const operation of getOperations(document, refs)) {
2533
+ const operationNode = parseOperation(parserOptions, operation);
2534
+ if (operationNode) operationNodes.push(operationNode);
2535
+ }
2536
+ let promotedEnums = null;
2537
+ if (enums === "root") {
2538
+ promotedEnums = collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas)));
2539
+ for (const name of promotedEnums.keys()) enumNames.push(name);
2540
+ }
2541
+ const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
2542
+ for (const name of Object.keys(schemas)) {
2543
+ const alias = refAliasMap.get(name);
2544
+ let node;
2545
+ if (alias?.name && parsedByName.has(alias.name)) node = {
2546
+ ...parsedByName.get(alias.name),
2547
+ name
2548
+ };
2549
+ else {
2550
+ const parsed = parsedByName.get(name);
2551
+ const child = discriminatorChildMap?.get(name);
2552
+ node = child ? patchDiscriminatorNode(parsed, child) : parsed;
2553
+ }
2554
+ schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
2555
+ }
2556
+ const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;
2557
+ return _kubb_ast.ast.factory.createInput({
2558
+ schemas: schemaNodes,
2559
+ operations,
2560
+ meta: {
2561
+ title: document.info?.title,
2562
+ description: document.info?.description,
2563
+ version: document.info?.version,
2564
+ baseURL: resolveBaseUrl({
2565
+ document,
2566
+ server
2567
+ }),
2568
+ circularNames,
2569
+ enumNames
2570
+ }
2571
+ });
2572
+ }
1933
2573
  return {
1934
2574
  name: "oas",
1935
2575
  get options() {
1936
2576
  return {
1937
2577
  validate,
1938
2578
  contentType,
1939
- serverIndex,
1940
- serverVariables,
2579
+ server,
1941
2580
  discriminator,
2581
+ enums,
1942
2582
  dateType,
1943
2583
  integerType,
1944
2584
  unknownType,
1945
2585
  emptySchemaType,
1946
- enumSuffix,
1947
- nameMapping
2586
+ enumSuffix
1948
2587
  };
1949
2588
  },
1950
2589
  get document() {
1951
2590
  return parsedDocument;
1952
2591
  },
1953
- get inputNode() {
1954
- return inputNode;
1955
- },
1956
2592
  async validate(input, options) {
1957
- await validateDocument(await parseDocument(input), options);
1958
- },
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
- });
2593
+ await assertInputExists(input);
2594
+ const document = await parseDocument(input);
2595
+ assertDocument(document);
2596
+ await validateDocument(document, options);
1972
2597
  },
1973
2598
  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
- }
1997
- });
1998
- return inputNode;
2599
+ const cached = inputCache.get(source);
2600
+ if (cached) return cached;
2601
+ const promise = (async () => {
2602
+ const document = await parseFromConfig(source);
2603
+ assertDocument(document);
2604
+ if (validate) await validateDocument(document);
2605
+ parsedDocument = document;
2606
+ const refs = createRefs(document);
2607
+ const { schemas, renames } = getSchemas(document, { contentType }, refs);
2608
+ return parseInput({
2609
+ document,
2610
+ refs,
2611
+ schemas,
2612
+ parser: createSchemaParser({
2613
+ document,
2614
+ refs,
2615
+ contentType,
2616
+ renames
2617
+ })
2618
+ });
2619
+ })();
2620
+ inputCache.set(source, promise);
2621
+ return promise;
1999
2622
  }
2000
2623
  };
2001
2624
  });
2002
2625
  //#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
2626
  exports.adapterOas = adapterOas;
2017
2627
  exports.adapterOasName = adapterOasName;
2018
- exports.mergeDocuments = mergeDocuments;
2019
2628
 
2020
2629
  //# sourceMappingURL=index.cjs.map