@kubb/adapter-oas 5.0.0-alpha.3 → 5.0.0-alpha.31

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