@kubb/adapter-oas 5.0.0-alpha.6 → 5.0.0-alpha.60

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