@kubb/adapter-oas 5.0.0-alpha.9 → 5.0.0-beta.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,45 +21,217 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- let node_path = require("node:path");
25
- node_path = __toESM(node_path);
26
- let _kubb_ast = require("@kubb/ast");
27
24
  let _kubb_core = require("@kubb/core");
25
+ let node_path = require("node:path");
26
+ node_path = __toESM(node_path, 1);
28
27
  let _redocly_openapi_core = require("@redocly/openapi-core");
29
- let _stoplight_yaml = require("@stoplight/yaml");
30
- _stoplight_yaml = __toESM(_stoplight_yaml);
31
- let oas_types = require("oas/types");
32
28
  let oas_normalize = require("oas-normalize");
33
- oas_normalize = __toESM(oas_normalize);
34
- let remeda = require("remeda");
29
+ oas_normalize = __toESM(oas_normalize, 1);
35
30
  let swagger2openapi = require("swagger2openapi");
36
- swagger2openapi = __toESM(swagger2openapi);
37
- let jsonpointer = require("jsonpointer");
38
- jsonpointer = __toESM(jsonpointer);
31
+ swagger2openapi = __toESM(swagger2openapi, 1);
39
32
  let oas = require("oas");
40
- oas = __toESM(oas);
33
+ oas = __toESM(oas, 1);
34
+ let oas_types = require("oas/types");
41
35
  let oas_utils = require("oas/utils");
42
- //#region src/oas/resolveServerUrl.ts
36
+ //#region src/constants.ts
43
37
  /**
44
- * Resolves an OpenAPI server URL by substituting `{variable}` placeholders.
38
+ * Default parser options applied when no explicit options are provided.
45
39
  *
46
- * Resolution priority per variable:
47
- * 1. `overrides[key]` — caller-supplied value.
48
- * 2. `variable.default` spec-defined default, coerced to `string`.
49
- * 3. Variable is left unreplaced when neither is available.
40
+ * @example
41
+ * ```ts
42
+ * import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
50
43
  *
51
- * Throws when an `overrides` value is not present in the variable's `enum` list.
44
+ * const parser = createOasParser(oas)
45
+ * const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
46
+ * ```
52
47
  */
53
- function resolveServerUrl(server, overrides) {
54
- if (!server.variables) return server.url;
55
- let url = server.url;
56
- for (const [key, variable] of Object.entries(server.variables)) {
57
- const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
58
- if (value === void 0) continue;
59
- 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(", ")}.`);
60
- url = url.replaceAll(`{${key}}`, value);
48
+ const DEFAULT_PARSER_OPTIONS = {
49
+ dateType: "string",
50
+ integerType: "number",
51
+ unknownType: "any",
52
+ emptySchemaType: "any",
53
+ enumSuffix: "enum"
54
+ };
55
+ /**
56
+ * JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
57
+ *
58
+ * Used when building or parsing `$ref` strings.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
63
+ * ```
64
+ */
65
+ const SCHEMA_REF_PREFIX = "#/components/schemas/";
66
+ /**
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.
76
+ */
77
+ const MERGE_DEFAULT_VERSION = "1.0.0";
78
+ /**
79
+ * Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
80
+ *
81
+ * A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
82
+ * 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
+ */
92
+ const structuralKeys = new Set([
93
+ "properties",
94
+ "items",
95
+ "additionalProperties",
96
+ "oneOf",
97
+ "anyOf",
98
+ "allOf",
99
+ "not"
100
+ ]);
101
+ /**
102
+ * Static map from OAS `format` strings to Kubb `SchemaType` values.
103
+ *
104
+ * 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
+ * ```
117
+ */
118
+ const formatMap = {
119
+ uuid: "uuid",
120
+ email: "email",
121
+ "idn-email": "email",
122
+ uri: "url",
123
+ "uri-reference": "url",
124
+ url: "url",
125
+ ipv4: "ipv4",
126
+ ipv6: "ipv6",
127
+ hostname: "url",
128
+ "idn-hostname": "url",
129
+ binary: "blob",
130
+ byte: "blob",
131
+ int32: "integer",
132
+ float: "number",
133
+ double: "number"
134
+ };
135
+ /**
136
+ * 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
+ */
145
+ const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
146
+ /**
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()`.
149
+ */
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
+ ]);
155
+ //#endregion
156
+ //#region src/discriminator.ts
157
+ /**
158
+ * Injects discriminator enum values into child schemas so they know which value identifies them.
159
+ *
160
+ * Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
161
+ * enum value each union member is mapped to, then adds (or replaces) that property on the matching
162
+ * child object schema.
163
+ *
164
+ * Returns a new `InputNode` — the original is never mutated.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const { root } = parseOas(document, options)
169
+ * const next = applyDiscriminatorInheritance(root)
170
+ * ```
171
+ */
172
+ function applyDiscriminatorInheritance(root) {
173
+ const childMap = /* @__PURE__ */ new Map();
174
+ for (const schema of root.schemas) {
175
+ let unionNode = _kubb_core.ast.narrowSchema(schema, "union");
176
+ if (!unionNode) {
177
+ const intersectionMembers = _kubb_core.ast.narrowSchema(schema, "intersection")?.members;
178
+ if (intersectionMembers) for (const m of intersectionMembers) {
179
+ const u = _kubb_core.ast.narrowSchema(m, "union");
180
+ if (u) {
181
+ unionNode = u;
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue;
187
+ const { discriminatorPropertyName, members } = unionNode;
188
+ for (const member of members) {
189
+ const intersectionNode = _kubb_core.ast.narrowSchema(member, "intersection");
190
+ if (!intersectionNode?.members) continue;
191
+ let refNode;
192
+ let objNode;
193
+ for (const m of intersectionNode.members) {
194
+ refNode ??= _kubb_core.ast.narrowSchema(m, "ref");
195
+ objNode ??= _kubb_core.ast.narrowSchema(m, "object");
196
+ }
197
+ if (!refNode?.name || !objNode) continue;
198
+ const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName);
199
+ const enumNode = prop ? _kubb_core.ast.narrowSchema(prop.schema, "enum") : void 0;
200
+ if (!enumNode?.enumValues?.length) continue;
201
+ const enumValues = enumNode.enumValues.filter((v) => v !== null);
202
+ if (!enumValues.length) continue;
203
+ 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
+ });
209
+ }
61
210
  }
62
- return url;
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
+ } });
63
235
  }
64
236
  //#endregion
65
237
  //#region ../../internals/utils/src/casing.ts
@@ -81,10 +253,19 @@ function toCamelOrPascal(text, pascal) {
81
253
  * Splits `text` on `.` and applies `transformPart` to each segment.
82
254
  * The last segment receives `isLast = true`, all earlier segments receive `false`.
83
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.
84
265
  */
85
266
  function applyToFileParts(text, transformPart) {
86
- const parts = text.split(".");
87
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
267
+ const parts = text.split(/\.(?=[a-zA-Z])/);
268
+ return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/");
88
269
  }
89
270
  /**
90
271
  * Converts `text` to camelCase.
@@ -117,17 +298,141 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
117
298
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
118
299
  }
119
300
  //#endregion
301
+ //#region ../../internals/utils/src/object.ts
302
+ /**
303
+ * Returns `true` when `value` is a plain (non-null, non-array) object.
304
+ *
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`.
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * mergeDeep({ a: { x: 1 } }, { a: { y: 2 } })
322
+ * // { a: { x: 1, y: 2 } }
323
+ * ```
324
+ */
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;
333
+ }
334
+ //#endregion
120
335
  //#region ../../internals/utils/src/reserved.ts
121
336
  /**
337
+ * JavaScript and Java reserved words.
338
+ * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
339
+ */
340
+ const reservedWords = new Set([
341
+ "abstract",
342
+ "arguments",
343
+ "boolean",
344
+ "break",
345
+ "byte",
346
+ "case",
347
+ "catch",
348
+ "char",
349
+ "class",
350
+ "const",
351
+ "continue",
352
+ "debugger",
353
+ "default",
354
+ "delete",
355
+ "do",
356
+ "double",
357
+ "else",
358
+ "enum",
359
+ "eval",
360
+ "export",
361
+ "extends",
362
+ "false",
363
+ "final",
364
+ "finally",
365
+ "float",
366
+ "for",
367
+ "function",
368
+ "goto",
369
+ "if",
370
+ "implements",
371
+ "import",
372
+ "in",
373
+ "instanceof",
374
+ "int",
375
+ "interface",
376
+ "let",
377
+ "long",
378
+ "native",
379
+ "new",
380
+ "null",
381
+ "package",
382
+ "private",
383
+ "protected",
384
+ "public",
385
+ "return",
386
+ "short",
387
+ "static",
388
+ "super",
389
+ "switch",
390
+ "synchronized",
391
+ "this",
392
+ "throw",
393
+ "throws",
394
+ "transient",
395
+ "true",
396
+ "try",
397
+ "typeof",
398
+ "var",
399
+ "void",
400
+ "volatile",
401
+ "while",
402
+ "with",
403
+ "yield",
404
+ "Array",
405
+ "Date",
406
+ "hasOwnProperty",
407
+ "Infinity",
408
+ "isFinite",
409
+ "isNaN",
410
+ "isPrototypeOf",
411
+ "length",
412
+ "Math",
413
+ "name",
414
+ "NaN",
415
+ "Number",
416
+ "Object",
417
+ "prototype",
418
+ "String",
419
+ "toString",
420
+ "undefined",
421
+ "valueOf"
422
+ ]);
423
+ /**
122
424
  * Returns `true` when `name` is a syntactically valid JavaScript variable name.
425
+ *
426
+ * @example
427
+ * ```ts
428
+ * isValidVarName('status') // true
429
+ * isValidVarName('class') // false (reserved word)
430
+ * isValidVarName('42foo') // false (starts with digit)
431
+ * ```
123
432
  */
124
433
  function isValidVarName(name) {
125
- try {
126
- new Function(`var ${name}`);
127
- } catch {
128
- return false;
129
- }
130
- return true;
434
+ if (!name || reservedWords.has(name)) return false;
435
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
131
436
  }
132
437
  //#endregion
133
438
  //#region ../../internals/utils/src/urlPath.ts
@@ -140,18 +445,33 @@ function isValidVarName(name) {
140
445
  * p.template // '`/pet/${petId}`'
141
446
  */
142
447
  var URLPath = class {
143
- /** The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`. */
448
+ /**
449
+ * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
450
+ */
144
451
  path;
145
452
  #options;
146
453
  constructor(path, options = {}) {
147
454
  this.path = path;
148
455
  this.#options = options;
149
456
  }
150
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`. */
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
+ */
151
464
  get URL() {
152
465
  return this.toURLPath();
153
466
  }
154
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`). */
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
+ */
155
475
  get isURL() {
156
476
  try {
157
477
  return !!new URL(this.path).href;
@@ -169,11 +489,25 @@ var URLPath = class {
169
489
  get template() {
170
490
  return this.toTemplateString();
171
491
  }
172
- /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set. */
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
+ */
173
500
  get object() {
174
501
  return this.toObject();
175
502
  }
176
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters. */
503
+ /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
504
+ *
505
+ * @example
506
+ * ```ts
507
+ * new URLPath('/pet/{petId}').params // { petId: 'petId' }
508
+ * new URLPath('/pet').params // undefined
509
+ * ```
510
+ */
177
511
  get params() {
178
512
  return this.getParams();
179
513
  }
@@ -181,7 +515,9 @@ var URLPath = class {
181
515
  const param = isValidVarName(raw) ? raw : camelCase(raw);
182
516
  return this.#options.casing === "camelcase" ? camelCase(param) : param;
183
517
  }
184
- /** Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name. */
518
+ /**
519
+ * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
520
+ */
185
521
  #eachParam(fn) {
186
522
  for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
187
523
  const raw = match[1];
@@ -218,6 +554,12 @@ var URLPath = class {
218
554
  * Extracts all `{param}` segments from the path and returns them as a key-value map.
219
555
  * An optional `replacer` transforms each parameter name in both key and value positions.
220
556
  * Returns `undefined` when no path parameters are found.
557
+ *
558
+ * @example
559
+ * ```ts
560
+ * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
561
+ * // { petId: 'petId', tagId: 'tagId' }
562
+ * ```
221
563
  */
222
564
  getParams(replacer) {
223
565
  const params = {};
@@ -227,469 +569,44 @@ var URLPath = class {
227
569
  });
228
570
  return Object.keys(params).length > 0 ? params : void 0;
229
571
  }
230
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`. */
572
+ /** Converts the OpenAPI path to Express-style colon syntax.
573
+ *
574
+ * @example
575
+ * ```ts
576
+ * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
577
+ * ```
578
+ */
231
579
  toURLPath() {
232
580
  return this.path.replace(/\{([^}]+)\}/g, ":$1");
233
581
  }
234
582
  };
235
583
  //#endregion
236
- //#region src/constants.ts
584
+ //#region src/guards.ts
237
585
  /**
238
- * OpenAPI version string written into merged document stubs.
239
- */
240
- const MERGE_OPENAPI_VERSION = "3.0.0";
241
- /**
242
- * Fallback `info.title` used when merging multiple API documents.
243
- */
244
- const MERGE_DEFAULT_TITLE = "Merged API";
245
- /**
246
- * Fallback `info.version` used when merging multiple API documents.
247
- */
248
- const MERGE_DEFAULT_VERSION = "1.0.0";
249
- /**
250
- * JSON Schema keywords that indicate structural composition.
251
- * A schema fragment containing any of these keys must not be inlined into its
252
- * parent during `allOf` flattening — it carries semantic meaning of its own.
253
- */
254
- const structuralKeys = new Set([
255
- "properties",
256
- "items",
257
- "additionalProperties",
258
- "oneOf",
259
- "anyOf",
260
- "allOf",
261
- "not"
262
- ]);
263
- /**
264
- * Maps OAS/JSON Schema `format` strings to their Kubb `SchemaType` equivalents.
586
+ * Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
265
587
  *
266
- * Only formats that need a type different from the raw OAS `type` are listed.
267
- * `int64`, `date-time`, `date`, and `time` are handled separately because their
268
- * mapping depends on runtime parser options.
269
- *
270
- * Note: `ipv4`, `ipv6`, and `hostname` map to `'url'` — the closest supported
271
- * scalar type in the Kubb AST, even though these are not strictly URLs.
272
- */
273
- const formatMap = {
274
- uuid: "uuid",
275
- email: "email",
276
- "idn-email": "email",
277
- uri: "url",
278
- "uri-reference": "url",
279
- url: "url",
280
- ipv4: "url",
281
- ipv6: "url",
282
- hostname: "url",
283
- "idn-hostname": "url",
284
- binary: "blob",
285
- byte: "blob",
286
- int32: "integer",
287
- float: "number",
288
- double: "number"
289
- };
290
- /**
291
- * Exhaustive list of media types that Kubb recognizes.
292
- */
293
- const knownMediaTypes = new Set([
294
- "application/json",
295
- "application/xml",
296
- "application/x-www-form-urlencoded",
297
- "application/octet-stream",
298
- "application/pdf",
299
- "application/zip",
300
- "application/graphql",
301
- "multipart/form-data",
302
- "text/plain",
303
- "text/html",
304
- "text/csv",
305
- "text/xml",
306
- "image/png",
307
- "image/jpeg",
308
- "image/gif",
309
- "image/webp",
310
- "image/svg+xml",
311
- "audio/mpeg",
312
- "video/mp4"
313
- ]);
314
- /**
315
- * Vendor extension keys used to attach human-readable labels to enum values.
316
- * Checked in priority order: the first key found wins.
317
- */
318
- const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
319
- /**
320
- * Scalar primitive schema types used for union member simplification.
321
- */
322
- const SCALAR_PRIMITIVE_TYPES = new Set([
323
- "string",
324
- "number",
325
- "integer",
326
- "bigint",
327
- "boolean"
328
- ]);
329
- //#endregion
330
- //#region src/oas/Oas.ts
331
- /**
332
- * Prefix used to create synthetic `$ref` values for anonymous (inline) discriminator schemas.
333
- * The suffix is the schema index within the discriminator's `oneOf`/`anyOf` array.
334
- * @example `#kubb-inline-0`
335
- */
336
- const KUBB_INLINE_REF_PREFIX = "#kubb-inline-";
337
- var Oas = class extends oas.default {
338
- #options = { discriminator: "strict" };
339
- document;
340
- constructor(document) {
341
- super(document, void 0);
342
- this.document = document;
343
- }
344
- setOptions(options) {
345
- this.#options = {
346
- ...this.#options,
347
- ...options
348
- };
349
- if (this.#options.discriminator === "inherit") this.#applyDiscriminatorInheritance();
350
- }
351
- get options() {
352
- return this.#options;
353
- }
354
- get($ref) {
355
- const origRef = $ref;
356
- $ref = $ref.trim();
357
- if ($ref === "") return null;
358
- if ($ref.startsWith("#")) $ref = globalThis.decodeURIComponent($ref.substring(1));
359
- else return null;
360
- const current = jsonpointer.default.get(this.api, $ref);
361
- if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
362
- return current;
363
- }
364
- getKey($ref) {
365
- const key = $ref.split("/").pop();
366
- return key === "" ? void 0 : key;
367
- }
368
- set($ref, value) {
369
- $ref = $ref.trim();
370
- if ($ref === "") return false;
371
- if ($ref.startsWith("#")) {
372
- $ref = globalThis.decodeURIComponent($ref.substring(1));
373
- jsonpointer.default.set(this.api, $ref, value);
374
- }
375
- }
376
- #setDiscriminator(schema) {
377
- const { mapping = {}, propertyName } = schema.discriminator;
378
- if (this.#options.discriminator === "inherit") Object.entries(mapping).forEach(([mappingKey, mappingValue]) => {
379
- if (mappingValue) {
380
- const childSchema = this.get(mappingValue);
381
- if (!childSchema) return;
382
- if (!childSchema.properties) childSchema.properties = {};
383
- const property = childSchema.properties[propertyName];
384
- if (childSchema.properties) {
385
- childSchema.properties[propertyName] = {
386
- ...childSchema.properties ? childSchema.properties[propertyName] : {},
387
- enum: [...property?.enum?.filter((value) => value !== mappingKey) ?? [], mappingKey]
388
- };
389
- childSchema.required = typeof childSchema.required === "boolean" ? childSchema.required : [...new Set([...childSchema.required ?? [], propertyName])];
390
- this.set(mappingValue, childSchema);
391
- }
392
- }
393
- });
394
- }
395
- getDiscriminator(schema) {
396
- if (!isDiscriminator(schema) || !schema) return null;
397
- const { mapping = {}, propertyName } = schema.discriminator;
398
- /**
399
- * Helper to extract discriminator value from a schema.
400
- * Checks in order:
401
- * 1. Extension property matching propertyName (e.g., x-linode-ref-name)
402
- * 2. Property with const value
403
- * 3. Property with single enum value
404
- * 4. Title as fallback
405
- */
406
- const getDiscriminatorValue = (schema) => {
407
- if (!schema) return null;
408
- if (propertyName.startsWith("x-")) {
409
- const extensionValue = schema[propertyName];
410
- if (extensionValue && typeof extensionValue === "string") return extensionValue;
411
- }
412
- const propertySchema = schema.properties?.[propertyName];
413
- if (propertySchema && "const" in propertySchema && propertySchema.const !== void 0) return String(propertySchema.const);
414
- if (propertySchema && propertySchema.enum?.length === 1) return String(propertySchema.enum[0]);
415
- return schema.title || null;
416
- };
417
- /**
418
- * Process oneOf/anyOf items to build mapping.
419
- * Handles both $ref and inline schemas.
420
- */
421
- const processSchemas = (schemas, existingMapping) => {
422
- schemas.forEach((schemaItem, index) => {
423
- if (isReference(schemaItem)) {
424
- const key = this.getKey(schemaItem.$ref);
425
- try {
426
- const discriminatorValue = getDiscriminatorValue(this.get(schemaItem.$ref));
427
- const canAdd = key && !Object.values(existingMapping).includes(schemaItem.$ref);
428
- if (canAdd && discriminatorValue) existingMapping[discriminatorValue] = schemaItem.$ref;
429
- else if (canAdd) existingMapping[key] = schemaItem.$ref;
430
- } catch (_error) {
431
- if (key && !Object.values(existingMapping).includes(schemaItem.$ref)) existingMapping[key] = schemaItem.$ref;
432
- }
433
- } else {
434
- const discriminatorValue = getDiscriminatorValue(schemaItem);
435
- if (discriminatorValue) existingMapping[discriminatorValue] = `${KUBB_INLINE_REF_PREFIX}${index}`;
436
- }
437
- });
438
- };
439
- if (schema.oneOf) processSchemas(schema.oneOf, mapping);
440
- if (schema.anyOf) processSchemas(schema.anyOf, mapping);
441
- return {
442
- ...schema.discriminator,
443
- mapping
444
- };
445
- }
446
- dereferenceWithRef(schema) {
447
- if (isReference(schema)) return {
448
- ...schema,
449
- ...this.get(schema.$ref),
450
- $ref: schema.$ref
451
- };
452
- return schema;
453
- }
454
- #applyDiscriminatorInheritance() {
455
- const components = this.api.components;
456
- if (!components?.schemas) return;
457
- const visited = /* @__PURE__ */ new WeakSet();
458
- const enqueue = (value) => {
459
- if (!value) return;
460
- if (Array.isArray(value)) {
461
- for (const item of value) enqueue(item);
462
- return;
463
- }
464
- if (typeof value === "object") visit(value);
465
- };
466
- const visit = (schema) => {
467
- if (!schema || typeof schema !== "object") return;
468
- if (isReference(schema)) {
469
- visit(this.get(schema.$ref));
470
- return;
471
- }
472
- const schemaObject = schema;
473
- if (visited.has(schemaObject)) return;
474
- visited.add(schemaObject);
475
- if (isDiscriminator(schemaObject)) this.#setDiscriminator(schemaObject);
476
- if ("allOf" in schemaObject) enqueue(schemaObject.allOf);
477
- if ("oneOf" in schemaObject) enqueue(schemaObject.oneOf);
478
- if ("anyOf" in schemaObject) enqueue(schemaObject.anyOf);
479
- if ("not" in schemaObject) enqueue(schemaObject.not);
480
- if ("items" in schemaObject) enqueue(schemaObject.items);
481
- if ("prefixItems" in schemaObject) enqueue(schemaObject.prefixItems);
482
- if (schemaObject.properties) enqueue(Object.values(schemaObject.properties));
483
- if (schemaObject.additionalProperties && typeof schemaObject.additionalProperties === "object") enqueue(schemaObject.additionalProperties);
484
- };
485
- for (const schema of Object.values(components.schemas)) visit(schema);
486
- }
487
- /**
488
- * Oas does not have a getResponseBody(contentType)
489
- */
490
- #getResponseBodyFactory(responseBody) {
491
- function hasResponseBody(res = responseBody) {
492
- return !!res;
493
- }
494
- return (contentType) => {
495
- if (!hasResponseBody(responseBody)) return false;
496
- if (isReference(responseBody)) return false;
497
- if (!responseBody.content) return false;
498
- if (contentType) {
499
- if (!(contentType in responseBody.content)) return false;
500
- return responseBody.content[contentType];
501
- }
502
- let availableContentType;
503
- const contentTypes = Object.keys(responseBody.content);
504
- contentTypes.forEach((mt) => {
505
- if (!availableContentType && oas_utils.matchesMimeType.json(mt)) availableContentType = mt;
506
- });
507
- if (!availableContentType) contentTypes.forEach((mt) => {
508
- if (!availableContentType) availableContentType = mt;
509
- });
510
- if (availableContentType) return [
511
- availableContentType,
512
- responseBody.content[availableContentType],
513
- ...responseBody.description ? [responseBody.description] : []
514
- ];
515
- return false;
516
- };
517
- }
518
- getResponseSchema(operation, statusCode) {
519
- if (operation.schema.responses) Object.keys(operation.schema.responses).forEach((key) => {
520
- const schema = operation.schema.responses[key];
521
- const $ref = isReference(schema) ? schema.$ref : void 0;
522
- if (schema && $ref) operation.schema.responses[key] = this.get($ref);
523
- });
524
- const getResponseBody = this.#getResponseBodyFactory(operation.getResponseByStatusCode(statusCode));
525
- const { contentType } = this.#options;
526
- const responseBody = getResponseBody(contentType);
527
- if (responseBody === false) return {};
528
- const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
529
- if (!schema) return {};
530
- return this.dereferenceWithRef(schema);
531
- }
532
- getRequestSchema(operation) {
533
- const { contentType } = this.#options;
534
- if (operation.schema.requestBody) operation.schema.requestBody = this.dereferenceWithRef(operation.schema.requestBody);
535
- const requestBody = operation.getRequestBody(contentType);
536
- if (requestBody === false) return;
537
- const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
538
- if (!schema) return;
539
- return this.dereferenceWithRef(schema);
540
- }
541
- /**
542
- * Returns all resolved parameters for an operation, merging path-level and operation-level
543
- * parameters and deduplicating by `in:name` (operation-level takes precedence).
544
- *
545
- * oas v31+ filters out `$ref` parameters in `getParameters()`, so this method accesses the
546
- * raw `operation.schema.parameters` and path-item parameters directly and resolves `$ref`
547
- * pointers via `dereferenceWithRef` to preserve backward compatibility.
548
- */
549
- getParameters(operation) {
550
- const resolveParams = (params) => params.map((p) => this.dereferenceWithRef(p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
551
- const operationParams = resolveParams(operation.schema?.parameters || []);
552
- const pathItem = this.api?.paths?.[operation.path];
553
- const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : []);
554
- const paramMap = /* @__PURE__ */ new Map();
555
- for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
556
- for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
557
- return Array.from(paramMap.values());
558
- }
559
- getParametersSchema(operation, inKey) {
560
- const { contentType = operation.getContentType() } = this.#options;
561
- const params = this.getParameters(operation).filter((v) => v.in === inKey);
562
- if (!params.length) return null;
563
- return params.reduce((schema, pathParameters) => {
564
- const property = pathParameters.content?.[contentType]?.schema ?? pathParameters.schema;
565
- const required = typeof schema.required === "boolean" ? schema.required : [...schema.required || [], pathParameters.required ? pathParameters.name : void 0].filter(Boolean);
566
- const getDefaultStyle = (location) => {
567
- if (location === "query") return "form";
568
- if (location === "path") return "simple";
569
- return "simple";
570
- };
571
- const style = pathParameters.style || getDefaultStyle(inKey);
572
- const explode = pathParameters.explode !== void 0 ? pathParameters.explode : style === "form";
573
- if (inKey === "query" && style === "form" && explode === true && property?.type === "object" && property?.additionalProperties && !property?.properties) return {
574
- ...schema,
575
- description: pathParameters.description || schema.description,
576
- deprecated: schema.deprecated,
577
- example: property.example || schema.example,
578
- additionalProperties: property.additionalProperties
579
- };
580
- return {
581
- ...schema,
582
- description: schema.description,
583
- deprecated: schema.deprecated,
584
- example: schema.example,
585
- required,
586
- properties: {
587
- ...schema.properties,
588
- [pathParameters.name]: {
589
- description: pathParameters.description,
590
- ...property
591
- }
592
- }
593
- };
594
- }, {
595
- type: "object",
596
- required: [],
597
- properties: {}
598
- });
599
- }
600
- async validate() {
601
- return validate(this.api);
602
- }
603
- flattenSchema(schema) {
604
- return flattenSchema(schema);
605
- }
606
- /**
607
- * Get schemas from OpenAPI components (schemas, responses, requestBodies).
608
- * Returns schemas in dependency order along with name mapping for collision resolution.
609
- */
610
- getSchemas(options = {}) {
611
- const contentType = options.contentType ?? this.#options.contentType;
612
- const includes = options.includes ?? [
613
- "schemas",
614
- "requestBodies",
615
- "responses"
616
- ];
617
- const shouldResolveCollisions = options.collisionDetection ?? this.#options.collisionDetection ?? false;
618
- const components = this.getDefinition().components;
619
- const schemasWithMeta = [];
620
- if (includes.includes("schemas")) {
621
- const componentSchemas = components?.schemas || {};
622
- for (const [name, schemaObject] of Object.entries(componentSchemas)) {
623
- let schema = schemaObject;
624
- if (isReference(schemaObject)) {
625
- const resolved = this.get(schemaObject.$ref);
626
- if (resolved && !isReference(resolved)) schema = resolved;
627
- }
628
- schemasWithMeta.push({
629
- schema,
630
- source: "schemas",
631
- originalName: name
632
- });
633
- }
634
- }
635
- if (includes.includes("responses")) {
636
- const responses = components?.responses || {};
637
- for (const [name, response] of Object.entries(responses)) {
638
- const schema = extractSchemaFromContent(response.content, contentType);
639
- if (schema) {
640
- let resolvedSchema = schema;
641
- if (isReference(schema)) {
642
- const resolved = this.get(schema.$ref);
643
- if (resolved && !isReference(resolved)) resolvedSchema = resolved;
644
- }
645
- schemasWithMeta.push({
646
- schema: resolvedSchema,
647
- source: "responses",
648
- originalName: name
649
- });
650
- }
651
- }
652
- }
653
- if (includes.includes("requestBodies")) {
654
- const requestBodies = components?.requestBodies || {};
655
- for (const [name, request] of Object.entries(requestBodies)) {
656
- const schema = extractSchemaFromContent(request.content, contentType);
657
- if (schema) {
658
- let resolvedSchema = schema;
659
- if (isReference(schema)) {
660
- const resolved = this.get(schema.$ref);
661
- if (resolved && !isReference(resolved)) resolvedSchema = resolved;
662
- }
663
- schemasWithMeta.push({
664
- schema: resolvedSchema,
665
- source: "requestBodies",
666
- originalName: name
667
- });
668
- }
669
- }
670
- }
671
- const { schemas, nameMapping } = shouldResolveCollisions ? resolveCollisions(schemasWithMeta) : legacyResolve(schemasWithMeta);
672
- return {
673
- schemas: sortSchemas(schemas),
674
- nameMapping
675
- };
676
- }
677
- };
678
- //#endregion
679
- //#region src/oas/utils.ts
680
- /**
681
- * Narrows `doc` to a Swagger 2.0 document.
682
- * Swagger 2.0 documents do not have an `openapi` version key.
588
+ * @example
589
+ * ```ts
590
+ * if (isOpenApiV2Document(doc)) {
591
+ * // doc is OpenAPIV2.Document
592
+ * }
593
+ * ```
683
594
  */
684
595
  function isOpenApiV2Document(doc) {
685
- return !!doc && (0, remeda.isPlainObject)(doc) && !("openapi" in doc);
596
+ return !!doc && isPlainObject(doc) && !("openapi" in doc);
686
597
  }
687
598
  /**
688
599
  * Returns `true` when a schema should be treated as nullable.
689
600
  *
690
- * Covers three nullable signals across OAS versions:
691
- * - OAS 3.0: `nullable: true` or the vendor extension `x-nullable: true`.
692
- * - OAS 3.1 / JSON Schema: `type: 'null'` or `type: ['null', ...]` (multi-type array).
601
+ * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
602
+ * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
603
+ *
604
+ * @example
605
+ * ```ts
606
+ * isNullable({ type: 'string', nullable: true }) // true
607
+ * isNullable({ type: ['string', 'null'] }) // true
608
+ * isNullable({ type: 'string' }) // false
609
+ * ```
693
610
  */
694
611
  function isNullable(schema) {
695
612
  if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
@@ -699,34 +616,51 @@ function isNullable(schema) {
699
616
  return false;
700
617
  }
701
618
  /**
702
- * Narrows `obj` to an OpenAPI `$ref` pointer object.
703
- * Delegates to the `oas` package's own `isRef` helper.
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
+ * ```
704
626
  */
705
627
  function isReference(obj) {
706
- return !!obj && (0, oas_types.isRef)(obj);
628
+ return !!obj && typeof obj === "object" && "$ref" in obj;
707
629
  }
708
630
  /**
709
- * Returns `true` when `obj` is a schema that carries a structured OAS 3.x `discriminator`
710
- * object — as opposed to a plain-string discriminator found in some Swagger 2 specs.
631
+ * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object.
632
+ *
633
+ * @example
634
+ * ```ts
635
+ * isDiscriminator({ discriminator: { propertyName: 'type', mapping: {} } }) // true
636
+ * isDiscriminator({ discriminator: 'type' }) // false (Swagger 2 string form)
637
+ * ```
711
638
  */
712
639
  function isDiscriminator(obj) {
713
640
  const record = obj;
714
641
  return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
715
642
  }
643
+ //#endregion
644
+ //#region src/factory.ts
716
645
  /**
717
- * Loads, dereferences, and wraps a raw OpenAPI document into an `Oas` instance.
646
+ * Loads and dereferences an OpenAPI document, returning the raw `Document`.
718
647
  *
719
- * When given a file path string with `canBundle: true` (the default), Redocly's
720
- * bundler resolves all external `$ref`s first. Swagger 2.0 documents are
721
- * automatically up-converted to OpenAPI 3.0 via `swagger2openapi`.
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`.
651
+ *
652
+ * @example
653
+ * ```ts
654
+ * const document = await parseDocument('./openapi.yaml')
655
+ * const document = await parse(rawDocumentObject, { canBundle: false })
656
+ * ```
722
657
  */
723
- async function parse(pathOrApi, { oasClass = Oas, canBundle = true, enablePaths = true } = {}) {
724
- if (typeof pathOrApi === "string" && canBundle) return parse((await (0, _redocly_openapi_core.bundle)({
658
+ async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true } = {}) {
659
+ if (typeof pathOrApi === "string" && canBundle) return parseDocument((await (0, _redocly_openapi_core.bundle)({
725
660
  ref: pathOrApi,
726
661
  config: await (0, _redocly_openapi_core.loadConfig)(),
727
662
  base: pathOrApi
728
663
  })).bundle.parsed, {
729
- oasClass,
730
664
  canBundle,
731
665
  enablePaths
732
666
  });
@@ -736,26 +670,28 @@ async function parse(pathOrApi, { oasClass = Oas, canBundle = true, enablePaths
736
670
  }).load();
737
671
  if (isOpenApiV2Document(document)) {
738
672
  const { openapi } = await swagger2openapi.default.convertObj(document, { anchors: true });
739
- return new oasClass(openapi);
673
+ return openapi;
740
674
  }
741
- return new oasClass(document);
675
+ return document;
742
676
  }
743
677
  /**
744
- * Deep-merges multiple OpenAPI documents into a single `Oas` instance.
678
+ * Deep-merges multiple OpenAPI documents into a single `Document`.
745
679
  *
746
- * Each document is parsed independently (without path dereferencing) and then
747
- * recursively merged using `remeda`'s `mergeDeep`. The result is re-parsed so
748
- * the returned `Oas` instance is fully initialized.
680
+ * Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
681
+ * Throws when the input array is empty.
749
682
  *
750
- * Throws when the input array is empty — at least one document is required.
683
+ * @example
684
+ * ```ts
685
+ * const document = await mergeDocuments(['./pets.yaml', './orders.yaml'])
686
+ * ```
751
687
  */
752
- async function merge(pathOrApi, { oasClass = Oas } = {}) {
753
- const instances = await Promise.all(pathOrApi.map((p) => parse(p, {
754
- oasClass,
688
+ async function mergeDocuments(pathOrApi) {
689
+ const documents = [];
690
+ for (const p of pathOrApi) documents.push(await parseDocument(p, {
755
691
  enablePaths: false,
756
692
  canBundle: false
757
- })));
758
- if (instances.length === 0) throw new Error("No OAS instances provided for merging.");
693
+ }));
694
+ if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
759
695
  const seed = {
760
696
  openapi: MERGE_OPENAPI_VERSION,
761
697
  info: {
@@ -765,82 +701,311 @@ async function merge(pathOrApi, { oasClass = Oas } = {}) {
765
701
  paths: {},
766
702
  components: { schemas: {} }
767
703
  };
768
- return parse(instances.reduce((acc, current) => (0, remeda.mergeDeep)(acc, current.document), seed), { oasClass });
704
+ return parseDocument(documents.reduce((acc, current) => mergeDeep(acc, current), seed));
769
705
  }
770
706
  /**
771
- * Constructs an `Oas` instance from a Kubb `Config` object.
707
+ * Creates a `Document` from an `AdapterSource`.
772
708
  *
773
- * Handles all three input forms supported by `Config`:
774
- * - `data` — an inline YAML string, JSON string, or pre-parsed object.
775
- * - Array — multiple file paths that are deep-merged via {@link merge}.
776
- * - `path` — a local file path or a remote URL.
709
+ * Handles all three source types:
710
+ * - `{ type: 'path' }` — resolves and bundles a local file path or remote URL.
711
+ * - `{ type: 'paths' }` merges multiple file paths into a single document.
712
+ * - `{ type: 'data' }` — parses an inline string (YAML/JSON) or raw object.
713
+ *
714
+ * @example
715
+ * ```ts
716
+ * const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
717
+ * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
718
+ * ```
777
719
  */
778
- function parseFromConfig(config, oasClass = Oas) {
779
- if ("data" in config.input) {
780
- if (typeof config.input.data === "object") return parse(structuredClone(config.input.data), { oasClass });
781
- try {
782
- return parse(_stoplight_yaml.default.parse(config.input.data), { oasClass });
783
- } catch {
784
- return parse(config.input.data, { oasClass });
720
+ function parseFromConfig(source) {
721
+ if (source.type === "data") {
722
+ if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
723
+ return parseDocument(source.data, { canBundle: false });
724
+ }
725
+ if (source.type === "paths") return mergeDocuments(source.paths);
726
+ if (new URLPath(source.path).isURL) return parseDocument(source.path);
727
+ return parseDocument(node_path.default.resolve(node_path.default.dirname(source.path), source.path));
728
+ }
729
+ /**
730
+ * Validates an OpenAPI document using `oas-normalize` with colorized error output.
731
+ *
732
+ * @example
733
+ * ```ts
734
+ * await validateDocument(document)
735
+ * ```
736
+ */
737
+ async function validateDocument(document, { throwOnError = false } = {}) {
738
+ try {
739
+ await new oas_normalize.default(document, {
740
+ enablePaths: true,
741
+ colorizeErrors: true
742
+ }).validate({ parser: { validate: { errors: { colorize: true } } } });
743
+ } catch (error) {
744
+ if (throwOnError) throw error;
745
+ }
746
+ }
747
+ //#endregion
748
+ //#region src/refs.ts
749
+ /**
750
+ * Resolves a local JSON pointer reference from a document.
751
+ *
752
+ * Accepts `#/...` refs. Returns `null` for empty or non-local refs.
753
+ * Throws when the pointer cannot be resolved.
754
+ *
755
+ * @example
756
+ * ```ts
757
+ * resolveRef<SchemaObject>(document, '#/components/schemas/Pet') // SchemaObject | null
758
+ * ```
759
+ */
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
+ const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
767
+ if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
768
+ return current;
769
+ }
770
+ /**
771
+ * Resolves a `$ref` object while preserving the original `$ref` field on the result.
772
+ *
773
+ * Useful for parser flows that need both dereferenced fields and pointer
774
+ * identity (for naming/import purposes). Non-reference values are returned as-is.
775
+ *
776
+ * @example
777
+ * ```ts
778
+ * dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
779
+ * // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
780
+ * ```
781
+ */
782
+ function dereferenceWithRef(document, schema) {
783
+ if (isReference(schema)) return {
784
+ ...schema,
785
+ ...resolveRef(document, schema.$ref),
786
+ $ref: schema.$ref
787
+ };
788
+ return schema;
789
+ }
790
+ //#endregion
791
+ //#region src/resolvers.ts
792
+ /**
793
+ * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
794
+ * Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
795
+ * Throws if an override value is not in the variable's `enum` list.
796
+ *
797
+ * @example
798
+ * ```ts
799
+ * resolveServerUrl(
800
+ * { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
801
+ * { env: 'prod' },
802
+ * )
803
+ * // 'https://prod.api.example.com'
804
+ * ```
805
+ */
806
+ function resolveServerUrl(server, overrides) {
807
+ if (!server.variables) return server.url;
808
+ let url = server.url;
809
+ for (const [key, variable] of Object.entries(server.variables)) {
810
+ const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
811
+ if (value === void 0) continue;
812
+ 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(", ")}.`);
813
+ url = url.replaceAll(`{${key}}`, value);
814
+ }
815
+ return url;
816
+ }
817
+ /**
818
+ * Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
819
+ * Formats not in `formatMap` (e.g., `int64`, `date-time`) are handled separately by parser options.
820
+ */
821
+ function getSchemaType(format) {
822
+ return formatMap[format] ?? null;
823
+ }
824
+ /**
825
+ * Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
826
+ * Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
827
+ */
828
+ function getPrimitiveType(type) {
829
+ if (type === "number" || type === "integer" || type === "bigint") return type;
830
+ if (type === "boolean") return "boolean";
831
+ return "string";
832
+ }
833
+ /**
834
+ * Narrows a content-type string to the `MediaType` union Kubb recognizes, or returns `null`.
835
+ */
836
+ function getMediaType(contentType) {
837
+ return Object.values(_kubb_core.ast.mediaTypes).includes(contentType) ? contentType : null;
838
+ }
839
+ /**
840
+ * Returns all parameters for an operation, merging path-level and operation-level entries.
841
+ * Operation-level parameters override path-level ones with the same `in:name` key.
842
+ * `$ref` parameters resolve via `dereferenceWithRef` for backward compatibility.
843
+ *
844
+ * @example
845
+ * ```ts
846
+ * getParameters(document, operation)
847
+ * // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
848
+ * ```
849
+ */
850
+ function getParameters(document, operation) {
851
+ const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
852
+ const operationParams = resolveParams(operation.schema?.parameters || []);
853
+ const pathItem = document.paths?.[operation.path];
854
+ const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : []);
855
+ const paramMap = /* @__PURE__ */ new Map();
856
+ for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
857
+ for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
858
+ return Array.from(paramMap.values());
859
+ }
860
+ function getResponseBody(responseBody, contentType) {
861
+ if (!responseBody) return false;
862
+ if (isReference(responseBody)) return false;
863
+ const body = responseBody;
864
+ if (!body.content) return false;
865
+ if (contentType) {
866
+ if (!(contentType in body.content)) return false;
867
+ return body.content[contentType];
868
+ }
869
+ let availableContentType;
870
+ const contentTypes = Object.keys(body.content);
871
+ for (const mt of contentTypes) if (oas_utils.matchesMimeType.json(mt)) {
872
+ availableContentType = mt;
873
+ break;
874
+ }
875
+ if (!availableContentType) availableContentType = contentTypes[0];
876
+ if (availableContentType) return [
877
+ availableContentType,
878
+ body.content[availableContentType],
879
+ ...body.description ? [body.description] : []
880
+ ];
881
+ return false;
882
+ }
883
+ /**
884
+ * Returns the response schema for a given operation and HTTP status code.
885
+ *
886
+ * Returns an empty object `{}` when no response body schema is available.
887
+ *
888
+ * @example
889
+ * ```ts
890
+ * getResponseSchema(document, operation, 200) // SchemaObject
891
+ * getResponseSchema(document, operation, '4XX') // {}
892
+ * ```
893
+ */
894
+ function getResponseSchema(document, operation, statusCode, options = {}) {
895
+ if (operation.schema.responses) {
896
+ const responses = operation.schema.responses;
897
+ for (const key in responses) {
898
+ const schema = responses[key];
899
+ if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
785
900
  }
786
901
  }
787
- if (Array.isArray(config.input)) return merge(config.input.map((input) => node_path.default.resolve(config.root, input.path)), { oasClass });
788
- if (new URLPath(config.input.path).isURL) return parse(config.input.path, { oasClass });
789
- return parse(node_path.default.resolve(config.root, config.input.path), { oasClass });
902
+ const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType);
903
+ if (responseBody === false) return {};
904
+ const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
905
+ if (!schema) return {};
906
+ return dereferenceWithRef(document, schema);
907
+ }
908
+ /**
909
+ * Returns the request body schema for an operation, or `null` when absent.
910
+ *
911
+ * @example
912
+ * ```ts
913
+ * getRequestSchema(document, operation) // SchemaObject | null
914
+ * ```
915
+ */
916
+ function getRequestSchema(document, operation, options = {}) {
917
+ if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
918
+ const requestBody = operation.getRequestBody(options.contentType);
919
+ if (requestBody === false) return null;
920
+ const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
921
+ if (!schema) return null;
922
+ return dereferenceWithRef(document, schema);
790
923
  }
791
924
  /**
792
- * Flattens a single-member or keyword-only `allOf` into its parent schema.
925
+ * Flattens a keyword-only `allOf` into its parent schema.
926
+ *
927
+ * Only flattens when every member is a plain fragment — no `$ref` and no structural keywords
928
+ * (see `structuralKeys`). Outer schema values take precedence over fragment values.
929
+ * Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
793
930
  *
794
- * Flattening is only performed when every `allOf` member is a "plain fragment" —
795
- * i.e. contains no structural composition keywords (see `structuralKeys`) and no
796
- * `$ref`. When any member carries structural meaning, the schema is returned unchanged.
931
+ * @example
932
+ * ```ts
933
+ * flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
934
+ * // { type: 'object', properties: {}, description: 'A pet' }
797
935
  *
798
- * Keyword values from allOf members are merged into the parent on a first-write basis:
799
- * outer schema values always win over fragment values.
936
+ * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
937
+ * // returned unchanged contains a $ref
938
+ * ```
800
939
  */
940
+ /**
941
+ * Returns `true` when `fragment` carries any JSON Schema keyword that makes it
942
+ * structurally significant on its own (see `structuralKeys`).
943
+ *
944
+ * A fragment with a structural keyword can't be safely merged into a parent schema.
945
+ */
946
+ function hasStructuralKeywords(fragment) {
947
+ for (const key in fragment) if (structuralKeys.has(key)) return true;
948
+ return false;
949
+ }
801
950
  function flattenSchema(schema) {
802
951
  if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
803
- if (schema.allOf.some((item) => (0, oas_types.isRef)(item))) return schema;
804
- const isPlainFragment = (item) => !Object.keys(item).some((key) => structuralKeys.has(key));
805
- if (!schema.allOf.every((item) => isPlainFragment(item))) return schema;
952
+ const allOfFragments = schema.allOf;
953
+ if (allOfFragments.some((item) => (0, oas_types.isRef)(item))) return schema;
954
+ if (allOfFragments.some(hasStructuralKeywords)) return schema;
806
955
  const merged = { ...schema };
807
956
  delete merged.allOf;
808
- for (const fragment of schema.allOf) for (const [key, value] of Object.entries(fragment)) if (merged[key] === void 0) merged[key] = value;
957
+ for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) if (merged[key] === void 0) merged[key] = value;
809
958
  return merged;
810
959
  }
811
960
  /**
812
- * Validates an OpenAPI document using `oas-normalize`.
813
- * Enables path validation and colorized error output.
961
+ * Extracts the inline schema from a media-type `content` map.
962
+ *
963
+ * Prefers `preferredContentType` when given; otherwise uses the first key in the map.
964
+ * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
965
+ *
966
+ * @example
967
+ * ```ts
968
+ * extractSchemaFromContent(operation.content, 'application/json')
969
+ * // SchemaObject | null
970
+ * ```
814
971
  */
815
- async function validate(document) {
816
- return new oas_normalize.default(document, {
817
- enablePaths: true,
818
- colorizeErrors: true
819
- }).validate({ parser: { validate: { errors: { colorize: true } } } });
972
+ function extractSchemaFromContent(content, preferredContentType) {
973
+ if (!content) return null;
974
+ const firstContentType = Object.keys(content)[0] ?? "application/json";
975
+ const schema = content[preferredContentType ?? firstContentType]?.schema;
976
+ if (schema && "$ref" in schema) return null;
977
+ return schema ?? null;
820
978
  }
821
979
  /**
822
- * Walks a schema tree and collects the names of all `#/components/schemas/<name>` refs.
823
- * Used by `sortSchemas` to build the dependency graph.
980
+ * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
824
981
  */
825
982
  function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
826
983
  if (Array.isArray(schema)) {
827
984
  for (const item of schema) collectRefs(item, refs);
828
985
  return refs;
829
986
  }
830
- if (schema && typeof schema === "object") for (const [key, value] of Object.entries(schema)) if (key === "$ref" && typeof value === "string") {
831
- const match = value.match(/^#\/components\/schemas\/(.+)$/);
832
- if (match) refs.add(match[1]);
833
- } else collectRefs(value, refs);
987
+ if (schema && typeof schema === "object") for (const key in schema) {
988
+ const value = schema[key];
989
+ if (key === "$ref" && typeof value === "string") {
990
+ if (value.startsWith("#/components/schemas/")) {
991
+ const name = value.slice(21);
992
+ if (name) refs.add(name);
993
+ }
994
+ } else collectRefs(value, refs);
995
+ }
834
996
  return refs;
835
997
  }
836
998
  /**
837
999
  * Returns a copy of `schemas` topologically sorted by `$ref` dependency.
838
1000
  *
839
- * Schemas with no references come first; schemas that are referenced by others
840
- * come before those that reference them. This ensures code generators emit
841
- * referenced types before the types that depend on them.
1001
+ * Referenced schemas appear before the schemas that depend on them, so code generators
1002
+ * can emit types in the correct order. Cycles are silently skipped.
842
1003
  *
843
- * Cycles are silently skipped via the `stack` guard inside `visit`.
1004
+ * @example
1005
+ * ```ts
1006
+ * const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
1007
+ * // Pet appears before Order when Order.$ref points at Pet
1008
+ * ```
844
1009
  */
845
1010
  function sortSchemas(schemas) {
846
1011
  const deps = /* @__PURE__ */ new Map();
@@ -855,393 +1020,230 @@ function sortSchemas(schemas) {
855
1020
  visited.add(name);
856
1021
  sorted.push(name);
857
1022
  }
858
- for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
859
- const result = {};
860
- for (const name of sorted) result[name] = schemas[name];
861
- return result;
862
- }
863
- /**
864
- * Extracts the inline schema from a media-type `content` map.
865
- *
866
- * Prefers `preferredContentType` when provided; otherwise falls back to the
867
- * first key in the map. Returns `null` when:
868
- * - `content` is absent.
869
- * - The target content-type has no `schema` entry.
870
- * - The schema is a `$ref` (callers resolve refs separately).
871
- */
872
- function extractSchemaFromContent(content, preferredContentType) {
873
- if (!content) return null;
874
- const firstContentType = Object.keys(content)[0] ?? "application/json";
875
- const schema = content[preferredContentType ?? firstContentType]?.schema;
876
- if (schema && "$ref" in schema) return null;
877
- return schema ?? null;
1023
+ for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
1024
+ const result = {};
1025
+ for (const name of sorted) result[name] = schemas[name];
1026
+ return result;
878
1027
  }
879
- /**
880
- * Returns the PascalCase suffix appended to a component name when resolving
881
- * cross-source name collisions (schemas vs. responses vs. requestBodies).
882
- */
1028
+ const semanticSuffixes = {
1029
+ schemas: "Schema",
1030
+ responses: "Response",
1031
+ requestBodies: "Request"
1032
+ };
883
1033
  function getSemanticSuffix(source) {
884
- switch (source) {
885
- case "schemas": return "Schema";
886
- case "responses": return "Response";
887
- case "requestBodies": return "Request";
888
- }
1034
+ return semanticSuffixes[source];
889
1035
  }
890
- /**
891
- * Builds `GetSchemasResult` without any collision detection.
892
- * Each schema is registered under its original component name and its full
893
- * `#/components/<source>/<name>` ref path.
894
- */
895
- function legacyResolve(schemasWithMeta) {
896
- const schemas = {};
897
- const nameMapping = /* @__PURE__ */ new Map();
898
- for (const item of schemasWithMeta) {
899
- schemas[item.originalName] = item.schema;
900
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, item.originalName);
901
- }
902
- return {
903
- schemas,
904
- nameMapping
905
- };
1036
+ function resolveSchemaRef(document, schema) {
1037
+ if (!isReference(schema)) return schema;
1038
+ const resolved = resolveRef(document, schema.$ref);
1039
+ return resolved && !isReference(resolved) ? resolved : schema;
906
1040
  }
907
1041
  /**
908
- * Builds `GetSchemasResult` with automatic name-collision resolution.
1042
+ * Collects component schemas from one or more sources and resolves name collisions.
1043
+ *
1044
+ * Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
1045
+ * topologically sorted by `$ref` dependency so generators emit types in the correct order.
909
1046
  *
910
1047
  * When two or more schemas normalize to the same PascalCase name:
911
- * - If they share the same source, a numeric suffix (`2`, `3`, …) is appended.
912
- * - If they come from different sources (schemas / responses / requestBodies),
913
- * a semantic suffix (`Schema`, `Response`, `Request`) is appended.
1048
+ * - Same source numeric suffix (`2`, `3`, …).
1049
+ * - Different sources semantic suffix (`Schema`, `Response`, `Request`).
914
1050
  *
915
- * Non-colliding schemas are left unchanged.
1051
+ * @example
1052
+ * ```ts
1053
+ * const { schemas, nameMapping } = getSchemas(document, { contentType: 'application/json' })
1054
+ * ```
916
1055
  */
917
- function resolveCollisions(schemasWithMeta) {
918
- const schemas = {};
919
- const nameMapping = /* @__PURE__ */ new Map();
1056
+ function getSchemas(document, { contentType }) {
1057
+ const components = document.components;
1058
+ const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
1059
+ schema: resolveSchemaRef(document, schema),
1060
+ source: "schemas",
1061
+ originalName: name
1062
+ })), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
1063
+ const schema = extractSchemaFromContent(item.content, contentType);
1064
+ return schema ? [{
1065
+ schema: resolveSchemaRef(document, schema),
1066
+ source,
1067
+ originalName: name
1068
+ }] : [];
1069
+ }))];
920
1070
  const normalizedNames = /* @__PURE__ */ new Map();
921
- for (const item of schemasWithMeta) {
922
- const normalized = pascalCase(item.originalName);
923
- const bucket = normalizedNames.get(normalized) ?? [];
1071
+ for (const item of candidates) {
1072
+ const key = pascalCase(item.originalName);
1073
+ const bucket = normalizedNames.get(key) ?? [];
924
1074
  bucket.push(item);
925
- normalizedNames.set(normalized, bucket);
1075
+ normalizedNames.set(key, bucket);
926
1076
  }
1077
+ const schemas = {};
1078
+ const nameMapping = /* @__PURE__ */ new Map();
927
1079
  for (const [, items] of normalizedNames) {
928
- if (items.length === 1) {
929
- const item = items[0];
930
- schemas[item.originalName] = item.schema;
931
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, item.originalName);
932
- continue;
1080
+ const isSingle = items.length === 1;
1081
+ let hasMultipleSources = false;
1082
+ if (!isSingle) {
1083
+ const firstSource = items[0].source;
1084
+ for (let i = 1; i < items.length; i++) if (items[i].source !== firstSource) {
1085
+ hasMultipleSources = true;
1086
+ break;
1087
+ }
933
1088
  }
934
- if (new Set(items.map((item) => item.source)).size === 1) items.forEach((item, index) => {
935
- const uniqueName = item.originalName + (index === 0 ? "" : (index + 1).toString());
936
- schemas[uniqueName] = item.schema;
937
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
938
- });
939
- else items.forEach((item) => {
940
- const uniqueName = item.originalName + getSemanticSuffix(item.source);
1089
+ items.forEach((item, index) => {
1090
+ const suffix = isSingle ? "" : hasMultipleSources ? getSemanticSuffix(item.source) : index === 0 ? "" : String(index + 1);
1091
+ const uniqueName = item.originalName + suffix;
941
1092
  schemas[uniqueName] = item.schema;
942
1093
  nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
943
1094
  });
944
1095
  }
945
1096
  return {
946
- schemas,
1097
+ schemas: sortSchemas(schemas),
947
1098
  nameMapping
948
1099
  };
949
1100
  }
950
- //#endregion
951
- //#region src/utils.ts
952
- /**
953
- * Extracts the schema name from a `$ref` string.
954
- * For `#/components/schemas/Order` this returns `'Order'`.
955
- * Falls back to the full ref string when no slash is present.
956
- */
957
- function extractRefName($ref) {
958
- return $ref.split("/").at(-1) ?? $ref;
959
- }
960
1101
  /**
961
- * Replaces the discriminator property's schema inside an `ObjectSchemaNode`
962
- * with an enum of the given `values`.
963
- *
964
- * - When `enumName` is provided the enum is named, which lets printers emit a
965
- * standalone enum declaration + type reference (e.g. `PetTypeEnum`).
966
- * - When `enumName` is omitted the enum stays anonymous, so printers inline it
967
- * as a literal union (e.g. `'dog'`).
968
- *
969
- * Returns the node unchanged when it is not an object or lacks the target property.
970
- */
971
- function applyDiscriminatorEnum({ node, propertyName, values, enumName }) {
972
- if (node.type !== "object" || !node.properties?.length) return node;
973
- if (!node.properties.some((prop) => prop.name === propertyName)) return node;
974
- return (0, _kubb_ast.createSchema)({
975
- ...node,
976
- properties: node.properties.map((prop) => {
977
- if (prop.name !== propertyName) return prop;
978
- const enumSchema = (0, _kubb_ast.createSchema)({
979
- type: "enum",
980
- primitive: "string",
981
- enumValues: values,
982
- name: enumName,
983
- readOnly: prop.schema.readOnly,
984
- writeOnly: prop.schema.writeOnly
985
- });
986
- return (0, _kubb_ast.createProperty)({
987
- ...prop,
988
- schema: enumSchema
989
- });
990
- })
991
- });
992
- }
993
- /**
994
- * Merges consecutive anonymous (`name`-less) `ObjectSchemaNode`s in `members` into a
995
- * single object by combining their `properties` arrays.
996
- *
997
- * Only adjacent pairs are merged — non-object or named nodes act as boundaries.
998
- * This collapses patterns like `Address & { streetNumber } & { streetName }` into
999
- * `Address & { streetNumber; streetName }`.
1102
+ * Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
1103
+ * Returns `null` when `dateType: false`, signalling the format should fall through to `string`.
1000
1104
  */
1001
- function mergeAdjacentAnonymousObjects(members) {
1002
- return members.reduce((acc, member) => {
1003
- const obj = (0, _kubb_ast.narrowSchema)(member, "object");
1004
- if (obj && !obj.name) {
1005
- const prev = acc[acc.length - 1];
1006
- const prevObj = prev ? (0, _kubb_ast.narrowSchema)(prev, "object") : null;
1007
- if (prevObj && !prevObj.name) {
1008
- acc[acc.length - 1] = (0, _kubb_ast.createSchema)({
1009
- ...prevObj,
1010
- properties: [...prevObj.properties ?? [], ...obj.properties ?? []]
1011
- });
1012
- return acc;
1013
- }
1014
- }
1015
- acc.push(member);
1016
- return acc;
1017
- }, []);
1105
+ function getDateType(options, format) {
1106
+ if (!options.dateType) return null;
1107
+ if (format === "date-time") {
1108
+ if (options.dateType === "date") return {
1109
+ type: "date",
1110
+ representation: "date"
1111
+ };
1112
+ if (options.dateType === "stringOffset") return {
1113
+ type: "datetime",
1114
+ offset: true
1115
+ };
1116
+ if (options.dateType === "stringLocal") return {
1117
+ type: "datetime",
1118
+ local: true
1119
+ };
1120
+ return {
1121
+ type: "datetime",
1122
+ offset: false
1123
+ };
1124
+ }
1125
+ if (format === "date") return {
1126
+ type: "date",
1127
+ representation: options.dateType === "date" ? "date" : "string"
1128
+ };
1129
+ return {
1130
+ type: "time",
1131
+ representation: options.dateType === "date" ? "date" : "string"
1132
+ };
1018
1133
  }
1019
1134
  /**
1020
- * Simplifies a union member list by removing `enum` nodes whose `primitive` type is
1021
- * already represented by a broader scalar node in the same union.
1022
- *
1023
- * For example `['placed', 'approved'] | string` collapses to `string` because
1024
- * `string` subsumes all string literals. `'' | string` similarly becomes `string`.
1025
- *
1026
- * Only scalar primitives (`string`, `number`, `integer`, `bigint`, `boolean`) are
1027
- * considered — object, array, and ref members are left untouched.
1028
- *
1029
- * Const-derived enums (those without an `enumType`, produced from an OpenAPI `const`
1030
- * keyword) are **never** removed — `'accepted' | string` must stay as-is because the
1031
- * literal is intentional.
1135
+ * Collects the shared metadata fields passed to every `createSchema` call.
1032
1136
  */
1033
- function simplifyUnionMembers(members) {
1034
- const scalarPrimitives = new Set(members.filter((m) => SCALAR_PRIMITIVE_TYPES.has(m.type)).map((m) => m.type));
1035
- if (!scalarPrimitives.size) return members;
1036
- return members.filter((m) => {
1037
- if (m.type !== "enum") return true;
1038
- const prim = m.primitive;
1039
- if (!prim) return true;
1040
- if (!m.enumType) return true;
1041
- if (scalarPrimitives.has(prim)) return false;
1042
- if ((prim === "integer" || prim === "number") && (scalarPrimitives.has("integer") || scalarPrimitives.has("number"))) return false;
1043
- return true;
1044
- });
1137
+ function buildSchemaNode(schema, name, nullable, defaultValue) {
1138
+ return {
1139
+ name,
1140
+ nullable,
1141
+ title: schema.title,
1142
+ description: schema.description,
1143
+ deprecated: schema.deprecated,
1144
+ readOnly: schema.readOnly,
1145
+ writeOnly: schema.writeOnly,
1146
+ default: defaultValue,
1147
+ example: schema.example
1148
+ };
1045
1149
  }
1046
1150
  /**
1047
- * `nameMapping`, and calls `resolve` to obtain the `{ name, path }` pair for
1048
- * each import. When `oas` is supplied, only `$ref`s that are resolvable in the
1049
- * spec are included; omit it to skip the existence check.
1151
+ * Returns all request body content type keys for an operation.
1050
1152
  *
1051
- * This function is the pure, state-free alternative to `OasParser.getImports`.
1052
- * Because it receives `nameMapping` explicitly it can be called without holding
1053
- * a reference to the parser or the OAS instance.
1153
+ * The requestBody is dereferenced **in-place** when it is a `$ref` — the same mutation
1154
+ * that `getRequestSchema` already performs so that the returned list accurately reflects
1155
+ * the available content types even for referenced bodies.
1054
1156
  *
1055
1157
  * @example
1056
1158
  * ```ts
1057
- * // Use adapter state directly — no parser reference needed
1058
- * const imports = getImports({
1059
- * node: schemaNode,
1060
- * nameMapping: adapter.options.nameMapping,
1061
- * resolve: (schemaName) => ({
1062
- * name: schemaManager.getName(schemaName, { type: 'type' }),
1063
- * path: schemaManager.getFile(schemaName).path,
1064
- * }),
1065
- * })
1159
+ * getRequestBodyContentTypes(document, operation)
1160
+ * // ['application/json', 'multipart/form-data']
1066
1161
  * ```
1067
1162
  */
1068
- function getImports({ node, nameMapping, resolve }) {
1069
- return (0, _kubb_ast.collect)(node, { schema(schemaNode) {
1070
- if (schemaNode.type !== "ref" || !schemaNode.ref) return;
1071
- const rawName = extractRefName(schemaNode.ref);
1072
- const result = resolve(nameMapping.get(rawName) ?? rawName);
1073
- if (!result) return;
1074
- return {
1075
- name: [result.name],
1076
- path: result.path
1077
- };
1078
- } });
1163
+ function getRequestBodyContentTypes(document, operation) {
1164
+ if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
1165
+ const body = operation.schema.requestBody;
1166
+ if (!body) return [];
1167
+ return body.content ? Object.keys(body.content) : [];
1079
1168
  }
1080
1169
  //#endregion
1081
1170
  //#region src/parser.ts
1082
1171
  /**
1083
- * Default values for all `Options` fields.
1084
- */
1085
- const DEFAULT_OPTIONS = {
1086
- dateType: "string",
1087
- integerType: "number",
1088
- unknownType: "any",
1089
- emptySchemaType: "any",
1090
- enumSuffix: "enum"
1091
- };
1092
- /**
1093
- * Looks up the Kubb `SchemaType` for a given OAS `format` string.
1094
- * Returns `undefined` for formats not in `formatMap` (e.g. `int64`, `date-time`),
1095
- * which are handled separately because their output depends on parser options.
1096
- */
1097
- function formatToSchemaType(format) {
1098
- return formatMap[format];
1099
- }
1100
- /**
1101
- * Maps an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
1102
- * Numeric types (`number`, `integer`, `bigint`) are returned unchanged;
1103
- * `boolean` maps to `'boolean'`; everything else defaults to `'string'`.
1104
- */
1105
- function getPrimitiveType(type) {
1106
- if (type === "number" || type === "integer" || type === "bigint") return type;
1107
- if (type === "boolean") return "boolean";
1108
- return "string";
1109
- }
1110
- /**
1111
- * Narrows a raw content-type string to the `MediaType` union recognized by Kubb.
1112
- * Returns `undefined` for content types not present in `KNOWN_MEDIA_TYPES`.
1172
+ * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
1173
+ *
1174
+ * This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
1175
+ * from the array to its items sub-schema, making them valid for downstream processing.
1176
+ *
1177
+ * @note This is a defensive measure for robustness with non-compliant specs.
1113
1178
  */
1114
- function toMediaType(contentType) {
1115
- return knownMediaTypes.has(contentType) ? contentType : void 0;
1179
+ function normalizeArrayEnum(schema) {
1180
+ const normalizedItems = {
1181
+ ...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
1182
+ enum: schema.enum
1183
+ };
1184
+ const { enum: _enum, ...schemaWithoutEnum } = schema;
1185
+ return {
1186
+ ...schemaWithoutEnum,
1187
+ items: normalizedItems
1188
+ };
1116
1189
  }
1117
1190
  /**
1118
- * Creates an OAS parser that converts an OpenAPI/Swagger spec into
1119
- * the `@kubb/ast` tree.
1191
+ * Factory function that creates schema and operation converters for a given OpenAPI context.
1120
1192
  *
1121
- * Options are passed per-call to `parse` or `convertSchema` rather than
1122
- * at construction time, keeping the factory lightweight.
1193
+ * Returns closures that share mutable state (`resolvingRefs` set for cycle detection).
1194
+ * Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
1195
+ * made possible by hoisting of function declarations.
1123
1196
  *
1124
- * This is the **kubb-parser** stage of the compilation lifecycle:
1125
- * OpenAPI / Swagger → Kubb AST
1126
- *
1127
- * No code is generated here; the resulting tree is spec-agnostic and can
1128
- * be consumed by any downstream plugin (plugin-ts, plugin-zod, …).
1129
- *
1130
- * @example
1131
- * ```ts
1132
- * const parser = createOasParser(oas)
1133
- * const root = parser.parse({ emptySchemaType: 'unknown' })
1134
- * ```
1197
+ * @note Not exported; called internally by `parseOas()` and `parseSchema()`.
1135
1198
  */
1136
- function createOasParser(oas, { contentType, collisionDetection } = {}) {
1137
- const { schemas: schemaObjects, nameMapping } = oas.getSchemas({
1138
- contentType,
1139
- collisionDetection
1140
- });
1141
- /**
1142
- * Maps an `'any' | 'unknown' | 'void'` option string to the corresponding `SchemaType` constant.
1143
- * Used for both `unknownType` (unannotated schemas) and `emptySchemaType` (empty `{}` schemas).
1144
- */
1145
- function resolveTypeOption(value) {
1146
- if (value === "any") return _kubb_ast.schemaTypes.any;
1147
- if (value === "void") return _kubb_ast.schemaTypes.void;
1148
- return _kubb_ast.schemaTypes.unknown;
1149
- }
1150
- /**
1151
- * Resolves the AST type and datetime modifiers for a date/time format, honoring the `dateType` option.
1152
- * Returns `undefined` when `dateType` is `false`, meaning the format should fall through to `string`.
1153
- */
1154
- function getDateType(options, format) {
1155
- if (!options.dateType) return;
1156
- if (format === "date-time") {
1157
- if (options.dateType === "date") return {
1158
- type: "date",
1159
- representation: "date"
1160
- };
1161
- if (options.dateType === "stringOffset") return {
1162
- type: "datetime",
1163
- offset: true
1164
- };
1165
- if (options.dateType === "stringLocal") return {
1166
- type: "datetime",
1167
- local: true
1168
- };
1169
- return {
1170
- type: "datetime",
1171
- offset: false
1172
- };
1173
- }
1174
- if (format === "date") return {
1175
- type: "date",
1176
- representation: options.dateType === "date" ? "date" : "string"
1177
- };
1178
- return {
1179
- type: "time",
1180
- representation: options.dateType === "date" ? "date" : "string"
1181
- };
1182
- }
1199
+ function createSchemaParser(ctx) {
1200
+ const document = ctx.document;
1183
1201
  /**
1184
- * Shared metadata fields included in every `createSchema` call.
1185
- * Centralizes the common properties so sub-handlers don't repeat them.
1202
+ * Tracks `$ref` paths that are currently being resolved to prevent infinite
1203
+ * recursion when schemas contain circular references (e.g. `Pet → parent → Pet`).
1186
1204
  */
1187
- function buildSchemaBase(schema, name, nullable, defaultValue) {
1188
- return {
1189
- name,
1190
- nullable,
1191
- title: schema.title,
1192
- description: schema.description,
1193
- deprecated: schema.deprecated,
1194
- readOnly: schema.readOnly,
1195
- writeOnly: schema.writeOnly,
1196
- default: defaultValue,
1197
- example: schema.example
1198
- };
1199
- }
1205
+ const resolvingRefs = /* @__PURE__ */ new Set();
1200
1206
  /**
1201
- * Converts a `$ref` schema pointer into a `RefSchemaNode`.
1207
+ * Converts a `$ref` schema into a `RefSchemaNode`.
1202
1208
  *
1203
- * In OAS 3.0 siblings of `$ref` are technically ignored by the spec, but Kubb intentionally
1204
- * preserves them so that annotations like `pattern`, `description`, and `nullable` are
1205
- * reflected in generated JSDoc and type modifiers.
1209
+ * The resolved schema is stored in `node.schema`. Usage-site sibling fields
1210
+ * (description, readOnly, nullable, etc.) are stored directly on the ref node.
1211
+ * Use `syncSchemaRef(node)` in printers to get a merged view of both.
1212
+ * Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
1206
1213
  */
1207
- function convertRef({ schema, nullable, defaultValue }) {
1208
- return (0, _kubb_ast.createSchema)({
1214
+ function convertRef({ schema, name, nullable, defaultValue, rawOptions }) {
1215
+ let resolvedSchema;
1216
+ const refPath = schema.$ref;
1217
+ if (refPath && !resolvingRefs.has(refPath)) try {
1218
+ const referenced = resolveRef(document, refPath);
1219
+ if (referenced) {
1220
+ resolvingRefs.add(refPath);
1221
+ resolvedSchema = parseSchema({ schema: referenced }, rawOptions);
1222
+ resolvingRefs.delete(refPath);
1223
+ }
1224
+ } catch {}
1225
+ return _kubb_core.ast.createSchema({
1226
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
1209
1227
  type: "ref",
1210
- name: extractRefName(schema.$ref),
1228
+ name: _kubb_core.ast.extractRefName(schema.$ref),
1211
1229
  ref: schema.$ref,
1212
- nullable,
1213
- description: schema.description,
1214
- deprecated: schema.deprecated,
1215
- readOnly: schema.readOnly,
1216
- writeOnly: schema.writeOnly,
1217
- pattern: schema.type === "string" ? schema.pattern : void 0,
1218
- example: schema.example,
1219
- default: defaultValue
1230
+ schema: resolvedSchema
1220
1231
  });
1221
1232
  }
1222
1233
  /**
1223
- * Converts a `allOf` schema into either a flattened member node (single-member `allOf`)
1224
- * or an `IntersectionSchemaNode` (multi-member `allOf`).
1225
- *
1226
- * Single-member `allOf` without sibling structural keys is the common OAS 3.0 pattern for
1227
- * annotating a `$ref` or primitive with extra constraints; it is flattened to avoid
1228
- * producing needless intersection wrappers.
1229
- *
1230
- * The flatten path is skipped when the outer schema carries structural keys that cannot be
1231
- * merged into annotation fields: `properties`, `required`, or `additionalProperties`.
1232
- * Those cases must become an intersection so the constraints are preserved.
1233
- *
1234
- * Circular references through discriminator parents are detected and skipped to prevent
1235
- * infinite recursion during code generation.
1234
+ * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
1236
1235
  */
1237
- function convertAllOf({ schema, name, nullable, defaultValue, options }) {
1236
+ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions }) {
1238
1237
  if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
1239
1238
  const [memberSchema] = schema.allOf;
1240
- const memberNode = convertSchema({ schema: memberSchema }, options);
1239
+ const memberNode = parseSchema({
1240
+ schema: memberSchema,
1241
+ name: null
1242
+ }, rawOptions);
1241
1243
  const { kind: _kind, ...memberNodeProps } = memberNode;
1242
1244
  const mergedNullable = nullable || memberNode.nullable || void 0;
1243
1245
  const mergedDefault = schema.default === null && mergedNullable ? void 0 : schema.default ?? memberNode.default;
1244
- return (0, _kubb_ast.createSchema)({
1246
+ return _kubb_core.ast.createSchema({
1245
1247
  ...memberNodeProps,
1246
1248
  name,
1247
1249
  title: schema.title ?? memberNode.title,
@@ -1255,17 +1257,26 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1255
1257
  pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0)
1256
1258
  });
1257
1259
  }
1260
+ const filteredDiscriminantValues = [];
1258
1261
  const allOfMembers = schema.allOf.filter((item) => {
1259
1262
  if (!isReference(item) || !name) return true;
1260
- const deref = oas.get(item.$ref);
1263
+ const deref = resolveRef(document, item.$ref);
1261
1264
  if (!deref || !isDiscriminator(deref)) return true;
1262
1265
  const parentUnion = deref.oneOf ?? deref.anyOf;
1263
1266
  if (!parentUnion) return true;
1264
- const childRef = `#/components/schemas/${name}`;
1267
+ const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1265
1268
  const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1266
1269
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1267
- return !inOneOf && !inMapping;
1268
- }).map((s) => convertSchema({ schema: s }, options));
1270
+ if (inOneOf || inMapping) {
1271
+ const discriminatorValue = _kubb_core.ast.findDiscriminator(deref.discriminator.mapping, childRef);
1272
+ if (discriminatorValue) filteredDiscriminantValues.push({
1273
+ propertyName: deref.discriminator.propertyName,
1274
+ value: discriminatorValue
1275
+ });
1276
+ return false;
1277
+ }
1278
+ return true;
1279
+ }).map((s) => parseSchema({ schema: s }, rawOptions));
1269
1280
  const syntheticStart = allOfMembers.length;
1270
1281
  if (Array.isArray(schema.required) && schema.required.length) {
1271
1282
  const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
@@ -1273,106 +1284,126 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1273
1284
  if (missingRequired.length) {
1274
1285
  const resolvedMembers = schema.allOf.flatMap((item) => {
1275
1286
  if (!isReference(item)) return [item];
1276
- const deref = oas.get(item.$ref);
1287
+ const deref = resolveRef(document, item.$ref);
1277
1288
  return deref && !isReference(deref) ? [deref] : [];
1278
1289
  });
1279
1290
  for (const key of missingRequired) for (const resolved of resolvedMembers) if (resolved.properties?.[key]) {
1280
- allOfMembers.push(convertSchema({ schema: {
1291
+ allOfMembers.push(parseSchema({ schema: {
1281
1292
  properties: { [key]: resolved.properties[key] },
1282
1293
  required: [key]
1283
- } }, options));
1294
+ } }, rawOptions));
1284
1295
  break;
1285
1296
  }
1286
1297
  }
1287
1298
  }
1288
1299
  if (schema.properties) {
1289
1300
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
1290
- allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf }, options));
1301
+ allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions));
1291
1302
  }
1292
- return (0, _kubb_ast.createSchema)({
1303
+ for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(_kubb_core.ast.createDiscriminantNode({
1304
+ propertyName,
1305
+ value
1306
+ }));
1307
+ return _kubb_core.ast.createSchema({
1293
1308
  type: "intersection",
1294
- members: [...allOfMembers.slice(0, syntheticStart), ...mergeAdjacentAnonymousObjects(allOfMembers.slice(syntheticStart))],
1295
- ...buildSchemaBase(schema, name, nullable, defaultValue)
1309
+ members: [..._kubb_core.ast.mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ..._kubb_core.ast.mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
1310
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1296
1311
  });
1297
1312
  }
1298
1313
  /**
1299
1314
  * Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
1300
- *
1301
- * Both keywords are treated identically — their members are concatenated into a single union.
1302
- * When sibling `properties` are present alongside `oneOf`/`anyOf`, each union member is
1303
- * individually intersected with the shared properties node to match the OAS pattern of
1304
- * adding common fields next to a discriminated union.
1305
1315
  */
1306
- function convertUnion({ schema, name, nullable, defaultValue, options }) {
1316
+ function convertUnion({ schema, name, nullable, defaultValue, rawOptions }) {
1317
+ function pickDiscriminatorPropertyNode(node, propertyName) {
1318
+ const discriminatorProperty = _kubb_core.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1319
+ if (!discriminatorProperty) return null;
1320
+ return _kubb_core.ast.createSchema({
1321
+ type: "object",
1322
+ primitive: "object",
1323
+ properties: [discriminatorProperty]
1324
+ });
1325
+ }
1307
1326
  const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
1327
+ const strategy = schema.oneOf ? "one" : "any";
1308
1328
  const unionBase = {
1309
- ...buildSchemaBase(schema, name, nullable, defaultValue),
1310
- discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0
1329
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
1330
+ discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1331
+ strategy
1311
1332
  };
1312
- if (schema.properties) {
1333
+ const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
1334
+ const sharedPropertiesNode = schema.properties ? (() => {
1313
1335
  const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema;
1314
- const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
1315
- const memberBaseSchema = discriminator ? Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== "discriminator")) : schemaWithoutUnion;
1316
- return (0, _kubb_ast.createSchema)({
1336
+ return parseSchema({
1337
+ schema: discriminator ? Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== "discriminator")) : schemaWithoutUnion,
1338
+ name
1339
+ }, rawOptions);
1340
+ })() : void 0;
1341
+ if (sharedPropertiesNode || discriminator?.mapping) {
1342
+ const members = unionMembers.map((s) => {
1343
+ const ref = isReference(s) ? s.$ref : void 0;
1344
+ const discriminatorValue = _kubb_core.ast.findDiscriminator(discriminator?.mapping, ref);
1345
+ const memberNode = parseSchema({ schema: s }, rawOptions);
1346
+ if (!discriminatorValue || !discriminator) return memberNode;
1347
+ const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(_kubb_core.ast.setDiscriminatorEnum({
1348
+ node: sharedPropertiesNode,
1349
+ propertyName: discriminator.propertyName,
1350
+ values: [discriminatorValue]
1351
+ }), discriminator.propertyName) : void 0;
1352
+ return _kubb_core.ast.createSchema({
1353
+ type: "intersection",
1354
+ members: [memberNode, narrowedDiscriminatorNode ?? _kubb_core.ast.createDiscriminantNode({
1355
+ propertyName: discriminator.propertyName,
1356
+ value: discriminatorValue
1357
+ })]
1358
+ });
1359
+ });
1360
+ const unionNode = _kubb_core.ast.createSchema({
1317
1361
  type: "union",
1318
1362
  ...unionBase,
1319
- members: unionMembers.map((s) => {
1320
- const ref = isReference(s) ? s.$ref : void 0;
1321
- const discriminatorValue = discriminator?.mapping && ref ? Object.entries(discriminator.mapping).find(([, v]) => v === ref)?.[0] : void 0;
1322
- let propertiesNode = convertSchema({
1323
- schema: memberBaseSchema,
1324
- name
1325
- }, options);
1326
- if (discriminatorValue && discriminator) propertiesNode = applyDiscriminatorEnum({
1327
- node: propertiesNode,
1328
- propertyName: discriminator.propertyName,
1329
- values: [discriminatorValue]
1330
- });
1331
- return (0, _kubb_ast.createSchema)({
1332
- type: "intersection",
1333
- members: [convertSchema({ schema: s }, options), propertiesNode]
1334
- });
1335
- })
1363
+ members
1364
+ });
1365
+ if (!sharedPropertiesNode) return unionNode;
1366
+ return _kubb_core.ast.createSchema({
1367
+ type: "intersection",
1368
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
1369
+ members: [unionNode, sharedPropertiesNode]
1336
1370
  });
1337
1371
  }
1338
- return (0, _kubb_ast.createSchema)({
1372
+ return _kubb_core.ast.createSchema({
1339
1373
  type: "union",
1340
1374
  ...unionBase,
1341
- members: simplifyUnionMembers(unionMembers.map((s) => convertSchema({ schema: s }, options)))
1375
+ members: _kubb_core.ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s }, rawOptions)))
1342
1376
  });
1343
1377
  }
1344
1378
  /**
1345
- * Converts an OAS 3.1 `const` schema into either a null scalar or a single-value `EnumSchemaNode`.
1346
- * `const: null` maps to a null scalar; any other value becomes a one-item enum so that generators
1347
- * can produce a precise literal type.
1379
+ * Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
1348
1380
  */
1349
1381
  function convertConst({ schema, name, nullable, defaultValue }) {
1350
1382
  const constValue = schema.const;
1351
- if (constValue === null) return (0, _kubb_ast.createSchema)({
1383
+ if (constValue === null) return _kubb_core.ast.createSchema({
1352
1384
  type: "null",
1353
1385
  primitive: "null",
1354
1386
  name,
1355
1387
  title: schema.title,
1356
1388
  description: schema.description,
1357
- deprecated: schema.deprecated,
1358
- nullable
1389
+ deprecated: schema.deprecated
1359
1390
  });
1360
- return (0, _kubb_ast.createSchema)({
1391
+ const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
1392
+ return _kubb_core.ast.createSchema({
1361
1393
  type: "enum",
1362
- primitive: getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string"),
1394
+ primitive: constPrimitive,
1363
1395
  enumValues: [constValue],
1364
- ...buildSchemaBase(schema, name, nullable, defaultValue)
1396
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1365
1397
  });
1366
1398
  }
1367
1399
  /**
1368
- * Handles `format`-based special types (date/time, uuid, email, blob, etc.).
1369
- * Returns `undefined` when the format should fall through to string handling
1370
- * (i.e. `format: 'date-time'` with `dateType: false`).
1400
+ * Converts a format-annotated schema into a special-type `SchemaNode`.
1401
+ * Returns `null` when the format should fall through to string handling (`dateType: false`).
1371
1402
  */
1372
- function convertFormat({ schema, name, nullable, defaultValue, mergedOptions }) {
1373
- const base = buildSchemaBase(schema, name, nullable, defaultValue);
1374
- if (schema.format === "int64") return (0, _kubb_ast.createSchema)({
1375
- type: mergedOptions.integerType === "bigint" ? "bigint" : "integer",
1403
+ function convertFormat({ schema, name, nullable, defaultValue, options }) {
1404
+ const base = buildSchemaNode(schema, name, nullable, defaultValue);
1405
+ if (schema.format === "int64") return _kubb_core.ast.createSchema({
1406
+ type: options.integerType === "bigint" ? "bigint" : "integer",
1376
1407
  primitive: "integer",
1377
1408
  ...base,
1378
1409
  min: schema.minimum,
@@ -1381,36 +1412,55 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1381
1412
  exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
1382
1413
  });
1383
1414
  if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
1384
- const dateType = getDateType(mergedOptions, schema.format);
1385
- if (!dateType) return void 0;
1386
- if (dateType.type === "datetime") return (0, _kubb_ast.createSchema)({
1415
+ const dateType = getDateType(options, schema.format);
1416
+ if (!dateType) return null;
1417
+ if (dateType.type === "datetime") return _kubb_core.ast.createSchema({
1387
1418
  ...base,
1388
1419
  primitive: "string",
1389
1420
  type: "datetime",
1390
1421
  offset: dateType.offset,
1391
1422
  local: dateType.local
1392
1423
  });
1393
- return (0, _kubb_ast.createSchema)({
1424
+ return _kubb_core.ast.createSchema({
1394
1425
  ...base,
1395
1426
  primitive: "string",
1396
1427
  type: dateType.type,
1397
1428
  representation: dateType.representation
1398
1429
  });
1399
1430
  }
1400
- const specialType = formatToSchemaType(schema.format);
1401
- if (!specialType) return void 0;
1431
+ const specialType = getSchemaType(schema.format);
1432
+ if (!specialType) return null;
1402
1433
  const specialPrimitive = specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string";
1403
- if (specialType === "number" || specialType === "integer" || specialType === "bigint") return (0, _kubb_ast.createSchema)({
1434
+ if (specialType === "number" || specialType === "integer" || specialType === "bigint") return _kubb_core.ast.createSchema({
1404
1435
  ...base,
1405
1436
  primitive: specialPrimitive,
1406
1437
  type: specialType
1407
1438
  });
1408
- if (specialType === "url") return (0, _kubb_ast.createSchema)({
1439
+ if (specialType === "url") return _kubb_core.ast.createSchema({
1440
+ ...base,
1441
+ primitive: "string",
1442
+ type: "url",
1443
+ min: schema.minLength,
1444
+ max: schema.maxLength
1445
+ });
1446
+ if (specialType === "ipv4") return _kubb_core.ast.createSchema({
1447
+ ...base,
1448
+ primitive: "string",
1449
+ type: "ipv4"
1450
+ });
1451
+ if (specialType === "ipv6") return _kubb_core.ast.createSchema({
1452
+ ...base,
1453
+ primitive: "string",
1454
+ type: "ipv6"
1455
+ });
1456
+ if (specialType === "uuid" || specialType === "email") return _kubb_core.ast.createSchema({
1409
1457
  ...base,
1410
1458
  primitive: "string",
1411
- type: "url"
1459
+ type: specialType,
1460
+ min: schema.minLength,
1461
+ max: schema.maxLength
1412
1462
  });
1413
- return (0, _kubb_ast.createSchema)({
1463
+ return _kubb_core.ast.createSchema({
1414
1464
  ...base,
1415
1465
  primitive: specialPrimitive,
1416
1466
  type: specialType
@@ -1418,36 +1468,20 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1418
1468
  }
1419
1469
  /**
1420
1470
  * Converts an `enum` schema into an `EnumSchemaNode`.
1421
- *
1422
- * Handles several edge cases:
1423
- * - `{ type: 'array', enum }` (technically invalid OAS) — the enum is normalized into `items`.
1424
- * - `null` in enum values (OAS 3.0 nullable enum convention) — stripped and reflected as `nullable`.
1425
- * - `x-enumNames` / `x-enum-varnames` vendor extensions — produce named enum variants with explicit labels.
1426
- * - Numeric and boolean enums require a const-map representation because most generators cannot
1427
- * use string-enum syntax for non-string values.
1428
1471
  */
1429
- function convertEnum({ schema, name, nullable, type, options }) {
1430
- if (type === "array") {
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 convertSchema({
1437
- schema: {
1438
- ...schemaWithoutEnum,
1439
- items: normalizedItems
1440
- },
1441
- name
1442
- }, options);
1443
- }
1472
+ function convertEnum({ schema, name, nullable, type, rawOptions }) {
1473
+ if (type === "array") return parseSchema({
1474
+ schema: normalizeArrayEnum(schema),
1475
+ name
1476
+ }, rawOptions);
1444
1477
  const nullInEnum = schema.enum.includes(null);
1445
1478
  const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
1446
1479
  const enumNullable = nullable || nullInEnum || void 0;
1447
1480
  const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1481
+ const enumPrimitive = getPrimitiveType(type);
1448
1482
  const enumBase = {
1449
1483
  type: "enum",
1450
- primitive: getPrimitiveType(type),
1484
+ primitive: enumPrimitive,
1451
1485
  name,
1452
1486
  title: schema.title,
1453
1487
  description: schema.description,
@@ -1459,81 +1493,56 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1459
1493
  example: schema.example
1460
1494
  };
1461
1495
  const extensionKey = enumExtensionKeys.find((key) => key in schema);
1462
- if (extensionKey) {
1463
- const rawNames = schema[extensionKey];
1464
- const uniqueNames = [...new Set(rawNames)];
1465
- const enumType = getPrimitiveType(type) === "number" || getPrimitiveType(type) === "integer" ? "number" : getPrimitiveType(type) === "boolean" ? "boolean" : "string";
1466
- return (0, _kubb_ast.createSchema)({
1496
+ if (extensionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
1497
+ const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
1498
+ const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
1499
+ const uniqueValues = [...new Set(filteredValues)];
1500
+ const seenNames = /* @__PURE__ */ new Set();
1501
+ return _kubb_core.ast.createSchema({
1467
1502
  ...enumBase,
1468
- enumType,
1469
- namedEnumValues: uniqueNames.map((label, index) => ({
1470
- name: String(label),
1471
- value: filteredValues[index] ?? label,
1472
- format: enumType
1473
- }))
1503
+ primitive: enumPrimitiveType,
1504
+ namedEnumValues: uniqueValues.map((value, index) => ({
1505
+ name: String(rawEnumNames?.[index] ?? value),
1506
+ value,
1507
+ primitive: enumPrimitiveType
1508
+ })).filter((entry) => {
1509
+ if (seenNames.has(entry.name)) return false;
1510
+ seenNames.add(entry.name);
1511
+ return true;
1512
+ })
1474
1513
  });
1475
1514
  }
1476
- if (type === "number" || type === "integer") return (0, _kubb_ast.createSchema)({
1477
- ...enumBase,
1478
- enumType: "number",
1479
- namedEnumValues: [...new Set(filteredValues)].map((value) => ({
1480
- name: String(value),
1481
- value,
1482
- format: "number"
1483
- }))
1484
- });
1485
- if (type === "boolean") return (0, _kubb_ast.createSchema)({
1486
- ...enumBase,
1487
- enumType: "boolean",
1488
- namedEnumValues: [...new Set(filteredValues)].map((value) => ({
1489
- name: String(value),
1490
- value,
1491
- format: "boolean"
1492
- }))
1493
- });
1494
- return (0, _kubb_ast.createSchema)({
1515
+ return _kubb_core.ast.createSchema({
1495
1516
  ...enumBase,
1496
1517
  enumValues: [...new Set(filteredValues)]
1497
1518
  });
1498
1519
  }
1499
1520
  /**
1500
- * Converts an object-like schema (`type: 'object'`, `properties`, `additionalProperties`,
1501
- * or `patternProperties`) into an `ObjectSchemaNode`.
1502
- *
1503
- * When a `discriminator` is present, the discriminator property's schema is replaced with an
1504
- * enum of the mapping keys so generators can produce a precise literal-union type for it.
1505
- *
1506
- * Property optionality follows OAS semantics:
1507
- * - required + not nullable → `required: true`
1508
- * - not required + not nullable → `optional: true`
1509
- * - not required + nullable → `nullish: true`
1521
+ * Converts an object-like schema into an `ObjectSchemaNode`.
1510
1522
  */
1511
- function convertObject({ schema, name, nullable, defaultValue, options, mergedOptions }) {
1523
+ function convertObject({ schema, name, nullable, defaultValue, rawOptions, options }) {
1512
1524
  const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
1513
1525
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
1514
1526
  const resolvedPropSchema = propSchema;
1515
1527
  const propNullable = isNullable(resolvedPropSchema);
1516
- const basePropName = name ? pascalCase([name, propName].join(" ")) : void 0;
1517
- const propNode = convertSchema({
1528
+ const propNode = parseSchema({
1518
1529
  schema: resolvedPropSchema,
1519
- name: basePropName
1520
- }, options);
1521
- const isEnumNode = !!(0, _kubb_ast.narrowSchema)(propNode, "enum");
1522
- const derivedPropName = isEnumNode && name ? pascalCase([
1523
- name,
1524
- propName,
1525
- mergedOptions.enumSuffix
1526
- ].filter(Boolean).join(" ")) : basePropName;
1527
- return (0, _kubb_ast.createProperty)({
1530
+ name: _kubb_core.ast.childName(name, propName)
1531
+ }, rawOptions);
1532
+ let schemaNode = _kubb_core.ast.setEnumName(propNode, name, propName, options.enumSuffix);
1533
+ const tupleNode = _kubb_core.ast.narrowSchema(schemaNode, "tuple");
1534
+ if (tupleNode?.items) {
1535
+ const namedItems = tupleNode.items.map((item) => _kubb_core.ast.setEnumName(item, name, propName, options.enumSuffix));
1536
+ if (namedItems.some((item, i) => item !== tupleNode.items[i])) schemaNode = {
1537
+ ...tupleNode,
1538
+ items: namedItems
1539
+ };
1540
+ }
1541
+ return _kubb_core.ast.createProperty({
1528
1542
  name: propName,
1529
1543
  schema: {
1530
- ...isEnumNode && derivedPropName !== basePropName ? {
1531
- ...propNode,
1532
- name: derivedPropName
1533
- } : propNode,
1534
- nullable: propNullable || void 0,
1535
- optional: !required && !propNullable ? true : void 0,
1536
- nullish: !required && propNullable ? true : void 0
1544
+ ...schemaNode,
1545
+ nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
1537
1546
  },
1538
1547
  required
1539
1548
  });
@@ -1541,115 +1550,113 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1541
1550
  const additionalProperties = schema.additionalProperties;
1542
1551
  let additionalPropertiesNode;
1543
1552
  if (additionalProperties === true) additionalPropertiesNode = true;
1544
- else if (additionalProperties && Object.keys(additionalProperties).length > 0) additionalPropertiesNode = convertSchema({ schema: additionalProperties }, options);
1545
- else if (additionalProperties === false) additionalPropertiesNode = void 0;
1546
- else if (additionalProperties) additionalPropertiesNode = (0, _kubb_ast.createSchema)({ type: resolveTypeOption(mergedOptions.unknownType) });
1553
+ else if (additionalProperties && Object.keys(additionalProperties).length > 0) additionalPropertiesNode = parseSchema({ schema: additionalProperties }, rawOptions);
1554
+ else if (additionalProperties === false) additionalPropertiesNode = false;
1555
+ else if (additionalProperties) additionalPropertiesNode = _kubb_core.ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1547
1556
  const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
1548
- const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? (0, _kubb_ast.createSchema)({ type: resolveTypeOption(mergedOptions.unknownType) }) : convertSchema({ schema: patternSchema }, options)])) : void 0;
1549
- const objectNode = (0, _kubb_ast.createSchema)({
1557
+ 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;
1558
+ const objectNode = _kubb_core.ast.createSchema({
1550
1559
  type: "object",
1551
1560
  primitive: "object",
1552
1561
  properties,
1553
1562
  additionalProperties: additionalPropertiesNode,
1554
1563
  patternProperties,
1555
- ...buildSchemaBase(schema, name, nullable, defaultValue)
1564
+ minProperties: schema.minProperties,
1565
+ maxProperties: schema.maxProperties,
1566
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1556
1567
  });
1557
1568
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
1558
1569
  const discPropName = schema.discriminator.propertyName;
1559
- return applyDiscriminatorEnum({
1570
+ const values = Object.keys(schema.discriminator.mapping);
1571
+ const enumName = name ? _kubb_core.ast.enumPropName(name, discPropName, options.enumSuffix) : void 0;
1572
+ return _kubb_core.ast.setDiscriminatorEnum({
1560
1573
  node: objectNode,
1561
1574
  propertyName: discPropName,
1562
- values: Object.keys(schema.discriminator.mapping),
1563
- enumName: name ? pascalCase([
1564
- name,
1565
- discPropName,
1566
- mergedOptions.enumSuffix
1567
- ].filter(Boolean).join(" ")) : void 0
1575
+ values,
1576
+ enumName
1568
1577
  });
1569
1578
  }
1570
1579
  return objectNode;
1571
1580
  }
1572
1581
  /**
1573
1582
  * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
1574
- *
1575
- * Each `prefixItems` element maps to a positional tuple slot. An optional `items` schema
1576
- * after the prefix items is mapped to the rest parameter of the tuple.
1577
1583
  */
1578
- function convertTuple({ schema, name, nullable, defaultValue, options }) {
1579
- return (0, _kubb_ast.createSchema)({
1584
+ function convertTuple({ schema, name, nullable, defaultValue, rawOptions }) {
1585
+ const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item }, rawOptions));
1586
+ const rest = schema.items ? parseSchema({ schema: schema.items }, rawOptions) : _kubb_core.ast.createSchema({ type: "any" });
1587
+ return _kubb_core.ast.createSchema({
1580
1588
  type: "tuple",
1581
1589
  primitive: "array",
1582
- items: (schema.prefixItems ?? []).map((item) => convertSchema({ schema: item }, options)),
1583
- rest: schema.items ? convertSchema({ schema: schema.items }, options) : void 0,
1590
+ items: tupleItems,
1591
+ rest,
1584
1592
  min: schema.minItems,
1585
1593
  max: schema.maxItems,
1586
- ...buildSchemaBase(schema, name, nullable, defaultValue)
1594
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1587
1595
  });
1588
1596
  }
1589
1597
  /**
1590
1598
  * Converts a `type: 'array'` schema into an `ArraySchemaNode`.
1591
- *
1592
- * When the items schema is an inline enum, a name derived from the parent array's name and
1593
- * `enumSuffix` is forwarded so generators can emit a named enum declaration.
1594
1599
  */
1595
- function convertArray({ schema, name, nullable, defaultValue, options, mergedOptions }) {
1600
+ function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }) {
1596
1601
  const rawItems = schema.items;
1597
- const itemName = rawItems?.enum?.length && name ? pascalCase([name, mergedOptions.enumSuffix].join(" ")) : void 0;
1598
- return (0, _kubb_ast.createSchema)({
1602
+ const itemName = rawItems?.enum?.length && name ? _kubb_core.ast.enumPropName(void 0, name, options.enumSuffix) : void 0;
1603
+ const items = rawItems ? [parseSchema({
1604
+ schema: rawItems,
1605
+ name: itemName
1606
+ }, rawOptions)] : [];
1607
+ return _kubb_core.ast.createSchema({
1599
1608
  type: "array",
1600
1609
  primitive: "array",
1601
- items: rawItems ? [convertSchema({
1602
- schema: rawItems,
1603
- name: itemName
1604
- }, options)] : [],
1610
+ items,
1605
1611
  min: schema.minItems,
1606
1612
  max: schema.maxItems,
1607
1613
  unique: schema.uniqueItems ?? void 0,
1608
- ...buildSchemaBase(schema, name, nullable, defaultValue)
1614
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1609
1615
  });
1610
1616
  }
1611
1617
  /**
1612
- * Converts a `type: 'string'` schema (without a special format) into a `StringSchemaNode`.
1618
+ * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1613
1619
  */
1614
1620
  function convertString({ schema, name, nullable, defaultValue }) {
1615
- return (0, _kubb_ast.createSchema)({
1621
+ return _kubb_core.ast.createSchema({
1616
1622
  type: "string",
1617
1623
  primitive: "string",
1618
1624
  min: schema.minLength,
1619
1625
  max: schema.maxLength,
1620
1626
  pattern: schema.pattern,
1621
- ...buildSchemaBase(schema, name, nullable, defaultValue)
1627
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1622
1628
  });
1623
1629
  }
1624
1630
  /**
1625
- * Converts a `type: 'number'` or `type: 'integer'` schema into the corresponding `SchemaNode`.
1631
+ * Converts a `type: 'number'` or `type: 'integer'` schema.
1626
1632
  */
1627
1633
  function convertNumeric({ schema, name, nullable, defaultValue }, type) {
1628
- return (0, _kubb_ast.createSchema)({
1634
+ return _kubb_core.ast.createSchema({
1629
1635
  type,
1630
1636
  primitive: type,
1631
1637
  min: schema.minimum,
1632
1638
  max: schema.maximum,
1633
1639
  exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1634
1640
  exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
1635
- ...buildSchemaBase(schema, name, nullable, defaultValue)
1641
+ multipleOf: schema.multipleOf,
1642
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1636
1643
  });
1637
1644
  }
1638
1645
  /**
1639
- * Converts a `type: 'boolean'` schema into a `BooleanSchemaNode`.
1646
+ * Converts a `type: 'boolean'` schema.
1640
1647
  */
1641
1648
  function convertBoolean({ schema, name, nullable, defaultValue }) {
1642
- return (0, _kubb_ast.createSchema)({
1649
+ return _kubb_core.ast.createSchema({
1643
1650
  type: "boolean",
1644
1651
  primitive: "boolean",
1645
- ...buildSchemaBase(schema, name, nullable, defaultValue)
1652
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1646
1653
  });
1647
1654
  }
1648
1655
  /**
1649
- * Converts an explicit `type: 'null'` or `const: null` schema into a `NullSchemaNode`.
1656
+ * Converts an explicit `type: 'null'` schema.
1650
1657
  */
1651
1658
  function convertNull({ schema, name, nullable }) {
1652
- return (0, _kubb_ast.createSchema)({
1659
+ return _kubb_core.ast.createSchema({
1653
1660
  type: "null",
1654
1661
  primitive: "null",
1655
1662
  name,
@@ -1660,31 +1667,22 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1660
1667
  });
1661
1668
  }
1662
1669
  /**
1663
- * Central dispatcher: converts an OAS `SchemaObject` into a `SchemaNode`.
1670
+ * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
1664
1671
  *
1665
- * Dispatch order (first match wins):
1666
- * 1. `$ref` pointer
1667
- * 2. `allOf` composition
1668
- * 3. `oneOf` / `anyOf` union
1669
- * 4. `const` literal (OAS 3.1)
1670
- * 5. `format`-based special type (date/time, uuid, blob, …)
1671
- * 6. OAS 3.1 `contentMediaType: 'application/octet-stream'` blob
1672
- * 7. OAS 3.1 multi-type array → union or fallthrough
1673
- * 8. Constraint-inferred type (minLength/maxLength → string; minimum/maximum → number)
1674
- * 9. `enum` values
1675
- * 10. Object / array / tuple / scalar by `type`
1676
- * 11. Empty schema fallback (`emptySchemaType` option)
1672
+ * Dispatch order (first match wins): `$ref` → `allOf` → `oneOf`/`anyOf` → `const` → `format`
1673
+ * octet-stream blob → multi-type array → constraint-inferred type → `enum` → object/array/tuple/scalar
1674
+ * empty-schema fallback (`emptySchemaType` option).
1677
1675
  */
1678
- function convertSchema({ schema, name }, options) {
1679
- const mergedOptions = {
1680
- ...DEFAULT_OPTIONS,
1681
- ...options
1676
+ function parseSchema({ schema, name }, rawOptions) {
1677
+ const options = {
1678
+ ...DEFAULT_PARSER_OPTIONS,
1679
+ ...rawOptions
1682
1680
  };
1683
1681
  const flattenedSchema = flattenSchema(schema);
1684
- if (flattenedSchema && flattenedSchema !== schema) return convertSchema({
1682
+ if (flattenedSchema && flattenedSchema !== schema) return parseSchema({
1685
1683
  schema: flattenedSchema,
1686
1684
  name
1687
- }, options);
1685
+ }, rawOptions);
1688
1686
  const nullable = isNullable(schema) || void 0;
1689
1687
  const defaultValue = schema.default === null && nullable ? void 0 : schema.default;
1690
1688
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
@@ -1694,8 +1692,8 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1694
1692
  nullable,
1695
1693
  defaultValue,
1696
1694
  type,
1697
- options,
1698
- mergedOptions
1695
+ rawOptions,
1696
+ options
1699
1697
  };
1700
1698
  if (isReference(schema)) return convertRef(ctx);
1701
1699
  if (schema.allOf?.length) return convertAllOf(ctx);
@@ -1705,24 +1703,24 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1705
1703
  const formatResult = convertFormat(ctx);
1706
1704
  if (formatResult) return formatResult;
1707
1705
  }
1708
- if (schema.type === "string" && schema.contentMediaType === "application/octet-stream") return (0, _kubb_ast.createSchema)({
1706
+ if (schema.type === "string" && schema.contentMediaType === "application/octet-stream") return _kubb_core.ast.createSchema({
1709
1707
  type: "blob",
1710
1708
  primitive: "string",
1711
- ...buildSchemaBase(schema, name, nullable, defaultValue)
1709
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1712
1710
  });
1713
1711
  if (Array.isArray(schema.type) && schema.type.length > 1) {
1714
1712
  const nonNullTypes = schema.type.filter((t) => t !== "null");
1715
1713
  const arrayNullable = schema.type.includes("null") || nullable || void 0;
1716
- if (nonNullTypes.length > 1) return (0, _kubb_ast.createSchema)({
1714
+ if (nonNullTypes.length > 1) return _kubb_core.ast.createSchema({
1717
1715
  type: "union",
1718
- members: nonNullTypes.map((t) => convertSchema({
1716
+ members: nonNullTypes.map((t) => parseSchema({
1719
1717
  schema: {
1720
1718
  ...schema,
1721
1719
  type: t
1722
1720
  },
1723
1721
  name
1724
- }, options)),
1725
- ...buildSchemaBase(schema, name, arrayNullable, defaultValue)
1722
+ }, rawOptions)),
1723
+ ...buildSchemaNode(schema, name, arrayNullable, defaultValue)
1726
1724
  });
1727
1725
  }
1728
1726
  if (!type) {
@@ -1738,56 +1736,106 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1738
1736
  if (type === "integer") return convertNumeric(ctx, "integer");
1739
1737
  if (type === "boolean") return convertBoolean(ctx);
1740
1738
  if (type === "null") return convertNull(ctx);
1741
- return (0, _kubb_ast.createSchema)({
1742
- type: resolveTypeOption(mergedOptions.emptySchemaType),
1739
+ const emptyType = typeOptionMap.get(options.emptySchemaType);
1740
+ return _kubb_core.ast.createSchema({
1741
+ type: emptyType,
1743
1742
  name,
1744
1743
  title: schema.title,
1745
1744
  description: schema.description
1746
1745
  });
1747
1746
  }
1748
1747
  /**
1749
- * Converts a single dereferenced OAS parameter object into a `ParameterNode`.
1750
- * When the parameter has no `schema` or its schema is a `$ref`, falls back to `unknownType`.
1748
+ * Converts a dereferenced OAS parameter object into a `ParameterNode`.
1751
1749
  */
1752
1750
  function parseParameter(options, param) {
1753
1751
  const required = param["required"] ?? false;
1754
- const schema = param["schema"] && !isReference(param["schema"]) ? convertSchema({ schema: param["schema"] }, options) : (0, _kubb_ast.createSchema)({ type: resolveTypeOption(options.unknownType) });
1755
- return (0, _kubb_ast.createParameter)({
1752
+ const schema = param["schema"] ? parseSchema({ schema: param["schema"] }, options) : _kubb_core.ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1753
+ return _kubb_core.ast.createParameter({
1756
1754
  name: param["name"],
1757
1755
  in: param["in"],
1758
1756
  schema: {
1759
1757
  ...schema,
1760
- description: param["description"] ?? schema.description,
1761
- optional: !required || !!schema.optional ? true : void 0
1758
+ description: param["description"] ?? schema.description
1762
1759
  },
1763
1760
  required
1764
1761
  });
1765
1762
  }
1766
1763
  /**
1767
- * Converts an OAS `Operation` into an `OperationNode`, resolving parameters,
1768
- * request body, and all response codes into their AST node equivalents.
1764
+ * Reads the inline `requestBody` metadata (description / required) that OAS exposes
1765
+ * outside the schema itself. Returns an empty object when the request body is missing or a `$ref`.
1766
+ */
1767
+ function getRequestBodyMeta(operation) {
1768
+ const body = operation.schema.requestBody;
1769
+ if (!body) return { required: false };
1770
+ return {
1771
+ description: body.description,
1772
+ required: body.required === true
1773
+ };
1774
+ }
1775
+ /**
1776
+ * Reads the inline response object (not a `$ref`) and returns its description plus its `content` map.
1777
+ */
1778
+ function getResponseMeta(responseObj) {
1779
+ if (typeof responseObj !== "object" || responseObj === null || Array.isArray(responseObj)) return {};
1780
+ const inline = responseObj;
1781
+ return {
1782
+ description: inline.description,
1783
+ content: inline.content
1784
+ };
1785
+ }
1786
+ /**
1787
+ * Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
1788
+ * `$ref` entries are skipped since their flags live on the dereferenced target.
1769
1789
  */
1770
- function parseOperation(options, oas, operation) {
1771
- const parameters = oas.getParameters(operation).map((param) => parseParameter(options, param));
1772
- const requestBodySchema = oas.getRequestSchema(operation);
1773
- const requestBody = requestBodySchema ? convertSchema({ schema: requestBodySchema }, options) : void 0;
1790
+ function collectPropertyKeysByFlag(schema, flag) {
1791
+ if (!schema?.properties) return void 0;
1792
+ const keys = [];
1793
+ for (const key in schema.properties) {
1794
+ const prop = schema.properties[key];
1795
+ if (prop && !isReference(prop) && prop[flag]) keys.push(key);
1796
+ }
1797
+ return keys.length ? keys : void 0;
1798
+ }
1799
+ /**
1800
+ * Converts an OAS `Operation` into an `OperationNode`.
1801
+ */
1802
+ function parseOperation(options, operation) {
1803
+ const parameters = getParameters(document, operation).map((param) => parseParameter(options, param));
1804
+ const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
1805
+ const requestBodyMeta = getRequestBodyMeta(operation);
1806
+ const content = allContentTypes.flatMap((ct) => {
1807
+ const schema = getRequestSchema(document, operation, { contentType: ct });
1808
+ if (!schema) return [];
1809
+ return [{
1810
+ contentType: ct,
1811
+ schema: _kubb_core.ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
1812
+ keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
1813
+ }];
1814
+ });
1815
+ const requestBody = content.length > 0 || requestBodyMeta.description ? {
1816
+ description: requestBodyMeta.description,
1817
+ required: requestBodyMeta.required || void 0,
1818
+ content: content.length > 0 ? content : void 0
1819
+ } : void 0;
1774
1820
  const responses = operation.getResponseStatusCodes().map((statusCode) => {
1775
1821
  const responseObj = operation.getResponseByStatusCode(statusCode);
1776
- const responseSchema = oas.getResponseSchema(operation, statusCode);
1777
- const schema = responseSchema && Object.keys(responseSchema).length > 0 ? convertSchema({ schema: responseSchema }, options) : (0, _kubb_ast.createSchema)({ type: resolveTypeOption(options.emptySchemaType) });
1778
- const description = typeof responseObj === "object" && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : void 0;
1779
- const rawContent = typeof responseObj === "object" && responseObj !== null && !Array.isArray(responseObj) ? responseObj.content : void 0;
1780
- return (0, _kubb_ast.createResponse)({
1822
+ const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType });
1823
+ const schema = responseSchema && Object.keys(responseSchema).length > 0 ? parseSchema({ schema: responseSchema }, options) : _kubb_core.ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType) });
1824
+ const { description, content } = getResponseMeta(responseObj);
1825
+ const mediaType = content ? getMediaType(Object.keys(content)[0] ?? "") : getMediaType(operation.contentType ?? "");
1826
+ return _kubb_core.ast.createResponse({
1781
1827
  statusCode,
1782
1828
  description,
1783
1829
  schema,
1784
- mediaType: rawContent ? toMediaType(Object.keys(rawContent)[0] ?? "") : toMediaType(operation.contentType ?? "")
1830
+ mediaType,
1831
+ keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
1785
1832
  });
1786
1833
  });
1787
- return (0, _kubb_ast.createOperation)({
1834
+ const urlPath = new URLPath(operation.path);
1835
+ return _kubb_core.ast.createOperation({
1788
1836
  operationId: operation.getOperationId(),
1789
1837
  method: operation.method.toUpperCase(),
1790
- path: new URLPath(operation.path).URL,
1838
+ path: urlPath.path,
1791
1839
  tags: operation.getTags().map((tag) => tag.name),
1792
1840
  summary: operation.getSummary() || void 0,
1793
1841
  description: operation.getDescription() || void 0,
@@ -1797,160 +1845,169 @@ function createOasParser(oas, { contentType, collisionDetection } = {}) {
1797
1845
  responses
1798
1846
  });
1799
1847
  }
1800
- /**
1801
- * Converts an OpenAPI/Swagger spec (wrapped in a Kubb `Oas` instance) into
1802
- * a `RootNode` — the top-level node of the `@kubb/ast` tree.
1803
- */
1804
- function parse(options) {
1805
- const mergedOptions = {
1806
- ...DEFAULT_OPTIONS,
1807
- ...options
1808
- };
1809
- const schemas = Object.entries(schemaObjects).map(([name, schemaObject]) => convertSchema({
1810
- schema: schemaObject,
1811
- name
1812
- }, mergedOptions));
1813
- const paths = oas.getPaths();
1814
- return (0, _kubb_ast.createRoot)({
1815
- schemas,
1816
- operations: Object.entries(paths).flatMap(([_path, methods]) => Object.entries(methods).map(([, operation]) => operation ? parseOperation(mergedOptions, oas, operation) : null).filter((op) => op !== null))
1817
- });
1818
- }
1819
- /**
1820
- * Walks a `SchemaNode` tree and resolves all `ref` node names through the provided callbacks.
1821
- *
1822
- * `resolveName` handles all schema types; `resolveEnumName` (when provided) takes precedence
1823
- * for `enum` nodes, enabling a separate naming strategy for enums (e.g. different suffix).
1824
- *
1825
- * Collision-resolved names (from `nameMapping`) are applied before user-supplied resolvers.
1826
- */
1827
- function resolveRefs(node, resolveName, resolveEnumName) {
1828
- return (0, _kubb_ast.transform)(node, { schema(schemaNode) {
1829
- const schemaRef = (0, _kubb_ast.narrowSchema)(schemaNode, _kubb_ast.schemaTypes.ref);
1830
- if (schemaRef && (schemaRef.ref || schemaRef.name)) {
1831
- const rawRef = schemaRef.ref ?? schemaRef.name;
1832
- const resolved = resolveName(nameMapping.get(rawRef) ?? rawRef);
1833
- if (resolved) return {
1834
- ...schemaNode,
1835
- name: resolved
1836
- };
1837
- }
1838
- if (schemaNode.type === "enum" && schemaNode.name) {
1839
- const resolved = (resolveEnumName ?? resolveName)(schemaNode.name);
1840
- if (resolved) return {
1841
- ...schemaNode,
1842
- name: resolved
1843
- };
1844
- }
1845
- } });
1846
- }
1847
1848
  return {
1848
- parse,
1849
- convertSchema,
1850
- resolveRefs,
1849
+ parseSchema,
1850
+ parseOperation,
1851
+ parseParameter
1852
+ };
1853
+ }
1854
+ /**
1855
+ * Parses an OpenAPI specification into Kubb's universal `InputNode` AST.
1856
+ *
1857
+ * This is the main entry point for `@kubb/adapter-oas`. It converts OpenAPI/Swagger specs into a spec-agnostic tree
1858
+ * that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here —
1859
+ * the tree is a pure data structure representing all schemas and operations.
1860
+ *
1861
+ * Returns the AST root and a `nameMapping` for resolving schema references.
1862
+ *
1863
+ * @example
1864
+ * ```ts
1865
+ * import { parseOas } from '@kubb/adapter-oas'
1866
+ *
1867
+ * const document = await parseFromConfig(config)
1868
+ * const { root, nameMapping } = parseOas(document, { dateType: 'date', contentType: 'application/json' })
1869
+ * ```
1870
+ */
1871
+ function parseOas(document, options = {}) {
1872
+ const { contentType, ...parserOptions } = options;
1873
+ const mergedOptions = {
1874
+ ...DEFAULT_PARSER_OPTIONS,
1875
+ ...parserOptions
1876
+ };
1877
+ const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType });
1878
+ const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({
1879
+ document,
1880
+ contentType
1881
+ });
1882
+ const schemas = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({
1883
+ schema,
1884
+ name
1885
+ }, mergedOptions));
1886
+ const paths = new oas.default(document).getPaths();
1887
+ const operations = Object.entries(paths).flatMap(([_path, methods]) => Object.entries(methods).map(([, operation]) => operation ? _parseOperation(mergedOptions, operation) : null).filter((op) => op !== null));
1888
+ return {
1889
+ root: _kubb_core.ast.createInput({
1890
+ schemas,
1891
+ operations
1892
+ }),
1851
1893
  nameMapping
1852
1894
  };
1853
1895
  }
1854
1896
  //#endregion
1855
1897
  //#region src/adapter.ts
1898
+ /**
1899
+ * Stable string identifier for the OAS adapter used in Kubb's adapter registry.
1900
+ */
1856
1901
  const adapterOasName = "oas";
1857
1902
  /**
1858
- * Creates an OpenAPI / Swagger adapter for Kubb.
1903
+ * Creates the default OpenAPI / Swagger adapter for Kubb.
1859
1904
  *
1860
- * This is the default adapter you can omit it from your config when using
1861
- * an OpenAPI spec, but supplying it explicitly lets you pass options.
1905
+ * Parses the spec, optionally validates it, resolves the base URL, and converts
1906
+ * everything into an `InputNode` that downstream plugins consume.
1862
1907
  *
1863
1908
  * @example
1864
1909
  * ```ts
1865
- * import { defineConfig } from '@kubb/core'
1910
+ * import { defineConfig } from 'kubb'
1866
1911
  * import { adapterOas } from '@kubb/adapter-oas'
1912
+ * import { pluginTs } from '@kubb/plugin-ts'
1867
1913
  *
1868
1914
  * export default defineConfig({
1869
- * adapter: adapterOas({ validate: true, dateType: 'date' }),
1870
- * input: { path: './openapi.yaml' },
1871
- * plugins: [pluginTs(), pluginZod()],
1915
+ * adapter: adapterOas({ dateType: 'date', serverIndex: 0 }),
1916
+ * input: { path: './openapi.yaml' },
1917
+ * plugins: [pluginTs()],
1872
1918
  * })
1873
1919
  * ```
1874
1920
  */
1875
1921
  const adapterOas = (0, _kubb_core.createAdapter)((options) => {
1876
- const { validate = true, oasClass, contentType, serverIndex, serverVariables, discriminator = "strict", collisionDetection = false, dateType = "string", integerType = "number", unknownType = "any", emptySchemaType = unknownType } = options;
1877
- const nameMapping = /* @__PURE__ */ new Map();
1922
+ 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;
1923
+ let nameMapping = /* @__PURE__ */ new Map();
1924
+ let parsedDocument;
1925
+ let inputNode;
1878
1926
  return {
1879
1927
  name: "oas",
1880
- options: {
1881
- validate,
1882
- oasClass,
1883
- contentType,
1884
- serverIndex,
1885
- serverVariables,
1886
- discriminator,
1887
- collisionDetection,
1888
- dateType,
1889
- integerType,
1890
- unknownType,
1891
- emptySchemaType,
1892
- nameMapping
1928
+ get options() {
1929
+ return {
1930
+ validate,
1931
+ contentType,
1932
+ serverIndex,
1933
+ serverVariables,
1934
+ discriminator,
1935
+ dateType,
1936
+ integerType,
1937
+ unknownType,
1938
+ emptySchemaType,
1939
+ enumSuffix,
1940
+ nameMapping
1941
+ };
1942
+ },
1943
+ get document() {
1944
+ return parsedDocument;
1945
+ },
1946
+ get inputNode() {
1947
+ return inputNode;
1893
1948
  },
1894
1949
  getImports(node, resolve) {
1895
- return getImports({
1950
+ return _kubb_core.ast.collectImports({
1896
1951
  node,
1897
1952
  nameMapping,
1898
- resolve
1953
+ resolve: (schemaName) => {
1954
+ const result = resolve(schemaName);
1955
+ if (!result) return;
1956
+ return _kubb_core.ast.createImport({
1957
+ name: [result.name],
1958
+ path: result.path
1959
+ });
1960
+ }
1899
1961
  });
1900
1962
  },
1901
1963
  async parse(source) {
1902
- const oas = await parseFromConfig(sourceToFakeConfig(source), oasClass);
1903
- oas.setOptions({
1904
- contentType,
1905
- discriminator,
1906
- collisionDetection
1907
- });
1908
- if (validate) try {
1909
- await oas.validate();
1910
- } catch (_err) {}
1911
- const server = serverIndex !== void 0 ? oas.api.servers?.at(serverIndex) : void 0;
1964
+ const document = await parseFromConfig(source);
1965
+ if (validate) await validateDocument(document);
1966
+ const server = serverIndex !== void 0 ? document.servers?.at(serverIndex) : void 0;
1912
1967
  const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : void 0;
1913
- const parser = createOasParser(oas, {
1968
+ const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
1914
1969
  contentType,
1915
- collisionDetection
1970
+ dateType,
1971
+ integerType,
1972
+ unknownType,
1973
+ emptySchemaType,
1974
+ enumSuffix
1916
1975
  });
1917
- nameMapping.clear();
1918
- for (const [key, value] of parser.nameMapping) nameMapping.set(key, value);
1919
- return (0, _kubb_ast.createRoot)({
1920
- ...parser.parse({
1921
- dateType,
1922
- integerType,
1923
- unknownType,
1924
- emptySchemaType
1925
- }),
1976
+ const node = discriminator === "inherit" ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot;
1977
+ nameMapping = parsedNameMapping;
1978
+ parsedDocument = document;
1979
+ inputNode = _kubb_core.ast.createInput({
1980
+ ...node,
1926
1981
  meta: {
1927
- title: oas.api.info?.title,
1928
- description: oas.api.info?.description,
1929
- version: oas.api.info?.version,
1982
+ title: document.info?.title,
1983
+ description: document.info?.description,
1984
+ version: document.info?.version,
1930
1985
  baseURL
1931
1986
  }
1932
1987
  });
1988
+ return inputNode;
1933
1989
  }
1934
1990
  };
1935
1991
  });
1936
- function sourceToFakeConfig(source) {
1937
- switch (source.type) {
1938
- case "path": return {
1939
- root: node_path.default.dirname(source.path),
1940
- input: { path: source.path }
1941
- };
1942
- case "data": return {
1943
- root: process.cwd(),
1944
- input: { data: source.data }
1945
- };
1946
- case "paths": return {
1947
- root: source.paths[0] ? node_path.default.dirname(source.paths[0]) : process.cwd(),
1948
- input: source.paths.map((p) => ({ path: p }))
1949
- };
1950
- }
1951
- }
1952
1992
  //#endregion
1993
+ //#region src/types.ts
1994
+ /**
1995
+ * Maps uppercase HTTP method names to lowercase for backwards compatibility.
1996
+ *
1997
+ * @example
1998
+ * ```ts
1999
+ * HttpMethods['GET'] // 'get'
2000
+ * HttpMethods['POST'] // 'post'
2001
+ * ```
2002
+ */
2003
+ const HttpMethods = Object.fromEntries(Object.entries(_kubb_core.ast.httpMethods).map(([lower, upper]) => [upper, lower]));
2004
+ //#endregion
2005
+ exports.HttpMethods = HttpMethods;
1953
2006
  exports.adapterOas = adapterOas;
1954
2007
  exports.adapterOasName = adapterOasName;
2008
+ exports.mergeDocuments = mergeDocuments;
2009
+ exports.parseDocument = parseDocument;
2010
+ exports.parseFromConfig = parseFromConfig;
2011
+ exports.validateDocument = validateDocument;
1955
2012
 
1956
2013
  //# sourceMappingURL=index.cjs.map