@kubb/adapter-oas 5.0.0-alpha.2 → 5.0.0-alpha.20

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