@kubb/adapter-oas 5.0.0-alpha.7 → 5.0.0-alpha.70

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