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

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