@kubb/adapter-oas 5.0.0-beta.1 → 5.0.0-beta.100

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,27 +1,20 @@
1
- import "./chunk--u3MIqq1.js";
2
- import { ast, createAdapter } from "@kubb/core";
1
+ import "./rolldown-runtime-C0LytTxp.js";
2
+ import { ast, findCircularSchemas, narrowSchema } from "@kubb/ast";
3
+ import { Diagnostics, createAdapter } from "@kubb/core";
3
4
  import path from "node:path";
4
- import { bundle, loadConfig } from "@redocly/openapi-core";
5
- import OASNormalize from "oas-normalize";
6
- import swagger2openapi from "swagger2openapi";
7
- import BaseOas from "oas";
8
- import { isRef } from "oas/types";
9
- import { matchesMimeType } from "oas/utils";
5
+ import { access, readFile } from "node:fs/promises";
6
+ import { compileErrors, validate } from "@readme/openapi-parser";
7
+ import { upgrade } from "@scalar/openapi-upgrader";
8
+ import { bundle } from "api-ref-bundler";
9
+ import { parse } from "yaml";
10
+ import { childName, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
10
11
  //#region src/constants.ts
11
12
  /**
12
13
  * Default parser options applied when no explicit options are provided.
13
- *
14
- * @example
15
- * ```ts
16
- * import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
17
- *
18
- * const parser = createOasParser(oas)
19
- * const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
20
- * ```
21
14
  */
22
15
  const DEFAULT_PARSER_OPTIONS = {
23
16
  dateType: "string",
24
- integerType: "number",
17
+ integerType: "bigint",
25
18
  unknownType: "any",
26
19
  emptySchemaType: "any",
27
20
  enumSuffix: "enum"
@@ -38,32 +31,26 @@ const DEFAULT_PARSER_OPTIONS = {
38
31
  */
39
32
  const SCHEMA_REF_PREFIX = "#/components/schemas/";
40
33
  /**
41
- * OpenAPI version string written into the stub document created during multi-spec merges.
42
- */
43
- const MERGE_OPENAPI_VERSION = "3.0.0";
44
- /**
45
- * Fallback `info.title` placed in the stub document when merging multiple API files.
46
- */
47
- const MERGE_DEFAULT_TITLE = "Merged API";
48
- /**
49
- * Fallback `info.version` placed in the stub document when merging multiple API files.
34
+ * HTTP methods that count as operations on an OpenAPI path item. Other keys
35
+ * (`parameters`, `summary`, `$ref`, vendor extensions) are skipped when iterating operations.
50
36
  */
51
- const MERGE_DEFAULT_VERSION = "1.0.0";
37
+ const SUPPORTED_METHODS = /* @__PURE__ */ new Set([
38
+ "get",
39
+ "put",
40
+ "post",
41
+ "delete",
42
+ "options",
43
+ "head",
44
+ "patch",
45
+ "trace"
46
+ ]);
52
47
  /**
53
48
  * Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
54
49
  *
55
50
  * A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
56
51
  * intersection member rather than being merged into the parent.
57
- *
58
- * @example
59
- * ```ts
60
- * import { structuralKeys } from '@kubb/adapter-oas'
61
- *
62
- * const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
63
- * // true when fragment has e.g. 'properties' or 'oneOf'
64
- * ```
65
52
  */
66
- const structuralKeys = new Set([
53
+ const structuralKeys = /* @__PURE__ */ new Set([
67
54
  "properties",
68
55
  "items",
69
56
  "additionalProperties",
@@ -73,21 +60,24 @@ const structuralKeys = new Set([
73
60
  "not"
74
61
  ]);
75
62
  /**
63
+ * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
64
+ * `int64` and the date/time family. Keep this in sync with the `convertFormat`
65
+ * special-cases in `parser.ts`. `isHandledFormat` reads it so the
66
+ * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
67
+ */
68
+ const specialCasedFormats = /* @__PURE__ */ new Set([
69
+ "int64",
70
+ "date-time",
71
+ "date",
72
+ "time"
73
+ ]);
74
+ /**
76
75
  * Static map from OAS `format` strings to Kubb `SchemaType` values.
77
76
  *
78
77
  * Only formats whose AST type differs from the OAS `type` field appear here.
79
- * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
80
- * in the parser. `ipv4` and `ipv6` map to their own dedicated schema types; `hostname` and
81
- * `idn-hostname` map to `'url'` as the closest generic string-format type.
82
- *
83
- * @example
84
- * ```ts
85
- * import { formatMap } from '@kubb/adapter-oas'
86
- *
87
- * formatMap['uuid'] // 'uuid'
88
- * formatMap['binary'] // 'blob'
89
- * formatMap['float'] // 'number'
90
- * ```
78
+ * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled
79
+ * separately in the parser. `ipv4` and `ipv6` map to their own dedicated schema types. `hostname`
80
+ * and `idn-hostname` map to `'url'` as the closest generic string-format type.
91
81
  */
92
82
  const formatMap = {
93
83
  uuid: "uuid",
@@ -108,44 +98,24 @@ const formatMap = {
108
98
  };
109
99
  /**
110
100
  * Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
111
- *
112
- * @example
113
- * ```ts
114
- * import { enumExtensionKeys } from '@kubb/adapter-oas'
115
- *
116
- * const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
117
- * ```
118
101
  */
119
102
  const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
120
103
  /**
121
- * Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
122
- * Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
104
+ * Vendor extension keys that attach human-readable descriptions to enum values, checked in priority order.
123
105
  */
124
- const typeOptionMap = new Map([
125
- ["any", ast.schemaTypes.any],
126
- ["unknown", ast.schemaTypes.unknown],
127
- ["void", ast.schemaTypes.void]
128
- ]);
106
+ const enumDescriptionKeys = ["x-enumDescriptions", "x-enum-descriptions"];
129
107
  //#endregion
130
108
  //#region src/discriminator.ts
131
109
  /**
132
- * Injects discriminator enum values into child schemas so they know which value identifies them.
133
- *
134
- * Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
135
- * enum value each union member is mapped to, then adds (or replaces) that property on the matching
136
- * child object schema.
137
- *
138
- * Returns a new `InputNode` — the original is never mutated.
110
+ * Maps each child schema name to its discriminator patch data by scanning the given
111
+ * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
139
112
  *
140
- * @example
141
- * ```ts
142
- * const { root } = parseOas(document, options)
143
- * const next = applyDiscriminatorInheritance(root)
144
- * ```
113
+ * Called on a small pre-parsed subset of schemas (only the discriminator parents)
114
+ * rather than on all schemas at once.
145
115
  */
146
- function applyDiscriminatorInheritance(root) {
116
+ function buildDiscriminatorChildMap(schemas) {
147
117
  const childMap = /* @__PURE__ */ new Map();
148
- for (const schema of root.schemas) {
118
+ for (const schema of schemas) {
149
119
  let unionNode = ast.narrowSchema(schema, "union");
150
120
  if (!unionNode) {
151
121
  const intersectionMembers = ast.narrowSchema(schema, "intersection")?.members;
@@ -162,528 +132,285 @@ function applyDiscriminatorInheritance(root) {
162
132
  for (const member of members) {
163
133
  const intersectionNode = ast.narrowSchema(member, "intersection");
164
134
  if (!intersectionNode?.members) continue;
165
- let refNode;
166
- let objNode;
135
+ let refNode = null;
136
+ let objNode = null;
167
137
  for (const m of intersectionNode.members) {
168
138
  refNode ??= ast.narrowSchema(m, "ref");
169
139
  objNode ??= ast.narrowSchema(m, "object");
170
140
  }
171
141
  if (!refNode?.name || !objNode) continue;
172
142
  const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName);
173
- const enumNode = prop ? ast.narrowSchema(prop.schema, "enum") : void 0;
143
+ const enumNode = prop ? ast.narrowSchema(prop.schema, "enum") : null;
174
144
  if (!enumNode?.enumValues?.length) continue;
175
145
  const enumValues = enumNode.enumValues.filter((v) => v !== null);
176
146
  if (!enumValues.length) continue;
177
147
  const existing = childMap.get(refNode.name);
178
- if (existing) existing.enumValues.push(...enumValues);
179
- else childMap.set(refNode.name, {
180
- propertyName: discriminatorPropertyName,
181
- enumValues: [...enumValues]
182
- });
148
+ if (!existing) {
149
+ childMap.set(refNode.name, {
150
+ propertyName: discriminatorPropertyName,
151
+ enumValues: [...enumValues]
152
+ });
153
+ continue;
154
+ }
155
+ existing.enumValues.push(...enumValues);
183
156
  }
184
157
  }
185
- if (childMap.size === 0) return root;
186
- return ast.transform(root, { schema(node, { parent }) {
187
- if (parent?.kind !== "Input" || !node.name) return;
188
- const entry = childMap.get(node.name);
189
- if (!entry) return;
190
- const objectNode = ast.narrowSchema(node, "object");
191
- if (!objectNode) return;
192
- const { propertyName, enumValues } = entry;
193
- const enumSchema = ast.createSchema({
194
- type: "enum",
195
- enumValues
196
- });
197
- const newProp = ast.createProperty({
198
- name: propertyName,
199
- required: true,
200
- schema: enumSchema
201
- });
202
- const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
203
- const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
204
- return {
205
- ...objectNode,
206
- properties: newProperties
207
- };
208
- } });
209
- }
210
- //#endregion
211
- //#region ../../internals/utils/src/casing.ts
212
- /**
213
- * Shared implementation for camelCase and PascalCase conversion.
214
- * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
215
- * and capitalizes each word according to `pascal`.
216
- *
217
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
218
- */
219
- function toCamelOrPascal(text, pascal) {
220
- return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
221
- if (word.length > 1 && word === word.toUpperCase()) return word;
222
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
223
- return word.charAt(0).toUpperCase() + word.slice(1);
224
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
158
+ return childMap;
225
159
  }
226
160
  /**
227
- * Splits `text` on `.` and applies `transformPart` to each segment.
228
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
229
- * Segments are joined with `/` to form a file path.
230
- *
231
- * Only splits on dots followed by a letter so that version numbers
232
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
233
- *
234
- * Empty segments are filtered before joining. They arise when the text starts with
235
- * a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`
236
- * and `'..'` transforms to an empty string). Without this filter the join would produce
237
- * a leading `/`, which `path.resolve` would interpret as an absolute path, allowing
238
- * generated files to escape the configured output directory.
161
+ * Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
162
+ * the discriminant property).
239
163
  */
240
- function applyToFileParts(text, transformPart) {
241
- const parts = text.split(/\.(?=[a-zA-Z])/);
242
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/");
164
+ function patchDiscriminatorNode(node, entry) {
165
+ const objectNode = ast.narrowSchema(node, "object");
166
+ if (!objectNode) return node;
167
+ const { propertyName, enumValues } = entry;
168
+ const enumSchema = ast.factory.createSchema({
169
+ type: "enum",
170
+ enumValues
171
+ });
172
+ const newProp = ast.factory.createProperty({
173
+ name: propertyName,
174
+ required: true,
175
+ schema: enumSchema
176
+ });
177
+ const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
178
+ const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
179
+ return {
180
+ ...objectNode,
181
+ properties: newProperties
182
+ };
243
183
  }
244
184
  /**
245
- * Converts `text` to camelCase.
246
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
185
+ * Creates a single-property object schema used as a discriminator literal.
247
186
  *
248
187
  * @example
249
- * camelCase('hello-world') // 'helloWorld'
250
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
188
+ * ```ts
189
+ * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
190
+ * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
191
+ * ```
251
192
  */
252
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
253
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
254
- prefix,
255
- suffix
256
- } : {}));
257
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
258
- }
259
- /**
260
- * Converts `text` to PascalCase.
261
- * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
262
- *
263
- * @example
264
- * pascalCase('hello-world') // 'HelloWorld'
265
- * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
266
- */
267
- function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
268
- if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
269
- prefix,
270
- suffix
271
- }) : camelCase(part));
272
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
193
+ function createDiscriminantNode({ propertyName, value }) {
194
+ return ast.factory.createSchema({
195
+ type: "object",
196
+ primitive: "object",
197
+ properties: [ast.factory.createProperty({
198
+ name: propertyName,
199
+ schema: ast.factory.createSchema({
200
+ type: "enum",
201
+ primitive: "string",
202
+ enumValues: [value]
203
+ }),
204
+ required: true
205
+ })]
206
+ });
273
207
  }
274
- //#endregion
275
- //#region ../../internals/utils/src/object.ts
276
208
  /**
277
- * Returns `true` when `value` is a plain (non-null, non-array) object.
209
+ * Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
278
210
  *
279
211
  * @example
280
212
  * ```ts
281
- * isPlainObject({}) // true
282
- * isPlainObject([]) // false
283
- * isPlainObject(null) // false
213
+ * findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
284
214
  * ```
285
215
  */
286
- function isPlainObject(value) {
287
- return typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
216
+ function findDiscriminator(mapping, ref) {
217
+ if (!mapping || !ref) return null;
218
+ return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
288
219
  }
220
+ //#endregion
221
+ //#region ../../internals/utils/src/casing.ts
289
222
  /**
290
- * Recursively merges `source` into `target`, combining nested plain objects.
291
- * Arrays and non-object values from `source` override the corresponding values in `target`.
223
+ * Shared implementation for camelCase and PascalCase conversion.
224
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
225
+ * and capitalizes each word according to `pascal`.
292
226
  *
293
- * @example
294
- * ```ts
295
- * mergeDeep({ a: { x: 1 } }, { a: { y: 2 } })
296
- * // { a: { x: 1, y: 2 } }
297
- * ```
227
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
298
228
  */
299
- function mergeDeep(target, source) {
300
- const result = { ...target };
301
- for (const key of Object.keys(source)) {
302
- const sv = source[key];
303
- const tv = result[key];
304
- result[key] = sv !== null && typeof sv === "object" && !Array.isArray(sv) && tv !== null && typeof tv === "object" && !Array.isArray(tv) ? mergeDeep(tv, sv) : sv;
305
- }
306
- return result;
229
+ function toCamelOrPascal(text, pascal) {
230
+ return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
231
+ if (word.length > 1 && word === word.toUpperCase()) return word;
232
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
233
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
307
234
  }
308
- //#endregion
309
- //#region ../../internals/utils/src/reserved.ts
310
- /**
311
- * JavaScript and Java reserved words.
312
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
313
- */
314
- const reservedWords = new Set([
315
- "abstract",
316
- "arguments",
317
- "boolean",
318
- "break",
319
- "byte",
320
- "case",
321
- "catch",
322
- "char",
323
- "class",
324
- "const",
325
- "continue",
326
- "debugger",
327
- "default",
328
- "delete",
329
- "do",
330
- "double",
331
- "else",
332
- "enum",
333
- "eval",
334
- "export",
335
- "extends",
336
- "false",
337
- "final",
338
- "finally",
339
- "float",
340
- "for",
341
- "function",
342
- "goto",
343
- "if",
344
- "implements",
345
- "import",
346
- "in",
347
- "instanceof",
348
- "int",
349
- "interface",
350
- "let",
351
- "long",
352
- "native",
353
- "new",
354
- "null",
355
- "package",
356
- "private",
357
- "protected",
358
- "public",
359
- "return",
360
- "short",
361
- "static",
362
- "super",
363
- "switch",
364
- "synchronized",
365
- "this",
366
- "throw",
367
- "throws",
368
- "transient",
369
- "true",
370
- "try",
371
- "typeof",
372
- "var",
373
- "void",
374
- "volatile",
375
- "while",
376
- "with",
377
- "yield",
378
- "Array",
379
- "Date",
380
- "hasOwnProperty",
381
- "Infinity",
382
- "isFinite",
383
- "isNaN",
384
- "isPrototypeOf",
385
- "length",
386
- "Math",
387
- "name",
388
- "NaN",
389
- "Number",
390
- "Object",
391
- "prototype",
392
- "String",
393
- "toString",
394
- "undefined",
395
- "valueOf"
396
- ]);
397
235
  /**
398
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
236
+ * Converts `text` to PascalCase.
399
237
  *
400
- * @example
401
- * ```ts
402
- * isValidVarName('status') // true
403
- * isValidVarName('class') // false (reserved word)
404
- * isValidVarName('42foo') // false (starts with digit)
405
- * ```
238
+ * @example Word boundaries
239
+ * `pascalCase('hello-world') // 'HelloWorld'`
240
+ *
241
+ * @example With a suffix
242
+ * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
406
243
  */
407
- function isValidVarName(name) {
408
- if (!name || reservedWords.has(name)) return false;
409
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
244
+ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
245
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
410
246
  }
411
247
  //#endregion
412
- //#region ../../internals/utils/src/urlPath.ts
248
+ //#region ../../internals/utils/src/runtime.ts
413
249
  /**
414
- * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
250
+ * Detects the JavaScript runtime executing the current process and exposes its name and version.
415
251
  *
416
- * @example
417
- * const p = new URLPath('/pet/{petId}')
418
- * p.URL // '/pet/:petId'
419
- * p.template // '`/pet/${petId}`'
252
+ * Prefer the shared {@link runtime} instance over constructing your own.
420
253
  */
421
- var URLPath = class {
422
- /**
423
- * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
424
- */
425
- path;
426
- #options;
427
- constructor(path, options = {}) {
428
- this.path = path;
429
- this.#options = options;
430
- }
431
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
432
- *
433
- * @example
434
- * ```ts
435
- * new URLPath('/pet/{petId}').URL // '/pet/:petId'
436
- * ```
437
- */
438
- get URL() {
439
- return this.toURLPath();
440
- }
441
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
442
- *
443
- * @example
444
- * ```ts
445
- * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
446
- * new URLPath('/pet/{petId}').isURL // false
447
- * ```
448
- */
449
- get isURL() {
450
- try {
451
- return !!new URL(this.path).href;
452
- } catch {
453
- return false;
454
- }
455
- }
254
+ var Runtime = class {
456
255
  /**
457
- * Converts the OpenAPI path to a TypeScript template literal string.
458
- *
459
- * @example
460
- * new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
461
- * new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
462
- */
463
- get template() {
464
- return this.toTemplateString();
465
- }
466
- /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
256
+ * `true` when the current process is running under Bun.
467
257
  *
468
- * @example
469
- * ```ts
470
- * new URLPath('/pet/{petId}').object
471
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
472
- * ```
473
- */
474
- get object() {
475
- return this.toObject();
476
- }
477
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
258
+ * Detection keys off the global `Bun` object rather than `process.versions`,
259
+ * because Bun polyfills `process.versions.node` for Node compatibility and would
260
+ * otherwise look like Node.
478
261
  *
479
262
  * @example
480
263
  * ```ts
481
- * new URLPath('/pet/{petId}').params // { petId: 'petId' }
482
- * new URLPath('/pet').params // undefined
264
+ * if (runtime.isBun) {
265
+ * await Bun.write(path, data)
266
+ * }
483
267
  * ```
484
268
  */
485
- get params() {
486
- return this.getParams();
487
- }
488
- #transformParam(raw) {
489
- const param = isValidVarName(raw) ? raw : camelCase(raw);
490
- return this.#options.casing === "camelcase" ? camelCase(param) : param;
269
+ get isBun() {
270
+ return typeof Bun !== "undefined";
491
271
  }
492
272
  /**
493
- * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
273
+ * `true` when the current process is running under Deno.
494
274
  */
495
- #eachParam(fn) {
496
- for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
497
- const raw = match[1];
498
- fn(raw, this.#transformParam(raw));
499
- }
500
- }
501
- toObject({ type = "path", replacer, stringify } = {}) {
502
- const object = {
503
- url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
504
- params: this.getParams()
505
- };
506
- if (stringify) {
507
- if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
508
- if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
509
- return `{ url: '${object.url}' }`;
510
- }
511
- return object;
275
+ get isDeno() {
276
+ return typeof globalThis.Deno !== "undefined";
512
277
  }
513
278
  /**
514
- * Converts the OpenAPI path to a TypeScript template literal string.
515
- * An optional `replacer` can transform each extracted parameter name before interpolation.
279
+ * `true` when the current process is running under Node.
516
280
  *
517
- * @example
518
- * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
281
+ * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
519
282
  */
520
- toTemplateString({ prefix = "", replacer } = {}) {
521
- return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
522
- if (i % 2 === 0) return part;
523
- const param = this.#transformParam(part);
524
- return `\${${replacer ? replacer(param) : param}}`;
525
- }).join("")}\``;
283
+ get isNode() {
284
+ return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
526
285
  }
527
286
  /**
528
- * Extracts all `{param}` segments from the path and returns them as a key-value map.
529
- * An optional `replacer` transforms each parameter name in both key and value positions.
530
- * Returns `undefined` when no path parameters are found.
287
+ * Name of the runtime executing the current process.
531
288
  *
532
289
  * @example
533
290
  * ```ts
534
- * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
535
- * // { petId: 'petId', tagId: 'tagId' }
291
+ * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
536
292
  * ```
537
293
  */
538
- getParams(replacer) {
539
- const params = {};
540
- this.#eachParam((_raw, param) => {
541
- const key = replacer ? replacer(param) : param;
542
- params[key] = key;
543
- });
544
- return Object.keys(params).length > 0 ? params : void 0;
294
+ get name() {
295
+ if (this.isBun) return "bun";
296
+ if (this.isDeno) return "deno";
297
+ return "node";
545
298
  }
546
- /** Converts the OpenAPI path to Express-style colon syntax.
299
+ /**
300
+ * Version of the active runtime, or an empty string when it cannot be read.
547
301
  *
548
302
  * @example
549
303
  * ```ts
550
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
304
+ * runtime.version // '1.3.11' under Bun, '22.22.2' under Node
551
305
  * ```
552
306
  */
553
- toURLPath() {
554
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
307
+ get version() {
308
+ if (this.isBun) return process.versions.bun ?? "";
309
+ if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
310
+ return process.versions?.node ?? "";
555
311
  }
556
312
  };
557
- //#endregion
558
- //#region src/guards.ts
559
- /**
560
- * Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
561
- *
562
- * @example
563
- * ```ts
564
- * if (isOpenApiV2Document(doc)) {
565
- * // doc is OpenAPIV2.Document
566
- * }
567
- * ```
568
- */
569
- function isOpenApiV2Document(doc) {
570
- return !!doc && isPlainObject(doc) && !("openapi" in doc);
571
- }
572
313
  /**
573
- * Returns `true` when a schema should be treated as nullable.
574
- *
575
- * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
576
- * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
577
- *
578
- * @example
579
- * ```ts
580
- * isNullable({ type: 'string', nullable: true }) // true
581
- * isNullable({ type: ['string', 'null'] }) // true
582
- * isNullable({ type: 'string' }) // false
583
- * ```
314
+ * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
584
315
  */
585
- function isNullable(schema) {
586
- if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
587
- const schemaType = schema?.type;
588
- if (schemaType === "null") return true;
589
- if (Array.isArray(schemaType)) return schemaType.includes("null");
590
- return false;
591
- }
316
+ const runtime = new Runtime();
317
+ //#endregion
318
+ //#region ../../internals/utils/src/fs.ts
592
319
  /**
593
- * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
320
+ * Resolves to `true` when the file or directory at `path` exists.
321
+ * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
594
322
  *
595
323
  * @example
596
324
  * ```ts
597
- * isReference({ $ref: '#/components/schemas/Pet' }) // true
598
- * isReference({ type: 'string' }) // false
325
+ * if (await exists('./kubb.config.ts')) {
326
+ * const content = await read('./kubb.config.ts')
327
+ * }
599
328
  * ```
600
329
  */
601
- function isReference(obj) {
602
- return !!obj && typeof obj === "object" && "$ref" in obj;
330
+ async function exists(path) {
331
+ if (runtime.isBun) return Bun.file(path).exists();
332
+ return access(path).then(() => true, () => false);
603
333
  }
604
334
  /**
605
- * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object.
335
+ * Reads the file at `path` as a UTF-8 string.
336
+ * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
606
337
  *
607
338
  * @example
608
339
  * ```ts
609
- * isDiscriminator({ discriminator: { propertyName: 'type', mapping: {} } }) // true
610
- * isDiscriminator({ discriminator: 'type' }) // false (Swagger 2 string form)
340
+ * const source = await read('./src/Pet.ts')
611
341
  * ```
612
342
  */
613
- function isDiscriminator(obj) {
614
- const record = obj;
615
- return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
343
+ async function read(path) {
344
+ if (runtime.isBun) return Bun.file(path).text();
345
+ return readFile(path, { encoding: "utf8" });
616
346
  }
617
347
  //#endregion
618
348
  //#region src/factory.ts
349
+ const urlRegExp = /^https?:\/+/i;
350
+ async function readSource(sourcePath) {
351
+ if (urlRegExp.test(sourcePath)) {
352
+ const url = new URL(sourcePath);
353
+ const response = await fetch(url);
354
+ if (!response.ok) throw new Error(`Cannot fetch the OAS document at ${url.href} (HTTP ${response.status})`);
355
+ return response.text();
356
+ }
357
+ return read(sourcePath);
358
+ }
359
+ async function resolveSource(sourcePath) {
360
+ const data = await readSource(sourcePath);
361
+ if (sourcePath.toLowerCase().endsWith(".md")) return data;
362
+ return parse(data);
363
+ }
619
364
  /**
620
- * Loads and dereferences an OpenAPI document, returning the raw `Document`.
365
+ * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
621
366
  *
622
- * Accepts a file path string or an already-parsed document object. File paths are bundled via
623
- * Redocly to resolve external `$ref`s. Swagger 2.0 documents are automatically up-converted
624
- * to OpenAPI 3.0 via `swagger2openapi`.
367
+ * External file schemas are hoisted into named `components.schemas` entries, so a property
368
+ * pointing at `./schemas/User.yaml` ends up referencing `#/components/schemas/User`. Generators
369
+ * can then emit a named type with an import instead of inlining the shape. Sources are read with
370
+ * the Bun-aware `read` util for local YAML and JSON files, and with `fetch` for HTTP(S) URLs.
625
371
  *
626
- * @example
627
- * ```ts
628
- * const document = await parseDocument('./openapi.yaml')
629
- * const document = await parse(rawDocumentObject, { canBundle: false })
630
- * ```
372
+ * @example Local file
373
+ * `const document = await bundleDocument('./openapi.yaml')`
374
+ *
375
+ * @example Remote URL
376
+ * `const document = await bundleDocument('https://example.com/openapi.yaml')`
631
377
  */
632
- async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true } = {}) {
633
- if (typeof pathOrApi === "string" && canBundle) return parseDocument((await bundle({
634
- ref: pathOrApi,
635
- config: await loadConfig(),
636
- base: pathOrApi
637
- })).bundle.parsed, {
638
- canBundle,
639
- enablePaths
640
- });
641
- const document = await new OASNormalize(pathOrApi, {
642
- enablePaths,
643
- colorizeErrors: true
644
- }).load();
645
- if (isOpenApiV2Document(document)) {
646
- const { openapi } = await swagger2openapi.convertObj(document, { anchors: true });
647
- return openapi;
648
- }
649
- return document;
378
+ async function bundleDocument(pathOrUrl) {
379
+ const cache = /* @__PURE__ */ new Map();
380
+ const resolver = (sourcePath) => {
381
+ const key = urlRegExp.test(sourcePath) ? new URL(sourcePath).href : sourcePath;
382
+ const cached = cache.get(key);
383
+ if (cached) return cached;
384
+ const result = resolveSource(sourcePath);
385
+ cache.set(key, result);
386
+ return result;
387
+ };
388
+ await resolver(pathOrUrl);
389
+ return await bundle(pathOrUrl, resolver);
650
390
  }
651
391
  /**
652
- * Deep-merges multiple OpenAPI documents into a single `Document`.
392
+ * Loads and bundles an OpenAPI document, returning the raw `Document`.
653
393
  *
654
- * Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
655
- * Throws when the input array is empty.
394
+ * A string is a file path or URL: it is bundled via `api-ref-bundler`, hoisting external file
395
+ * schemas into named `components.schemas` entries so generators can emit named types and imports.
396
+ * An object is treated as an already-parsed document. Swagger 2.0 and OpenAPI 3.0 documents are
397
+ * up-converted to OpenAPI 3.1 via `@scalar/openapi-upgrader`.
656
398
  *
657
399
  * @example
658
400
  * ```ts
659
- * const document = await mergeDocuments(['./pets.yaml', './orders.yaml'])
401
+ * const document = await parseDocument('./openapi.yaml')
402
+ * const document = await parseDocument(rawDocumentObject)
660
403
  * ```
661
404
  */
662
- async function mergeDocuments(pathOrApi) {
663
- const documents = [];
664
- for (const p of pathOrApi) documents.push(await parseDocument(p, {
665
- enablePaths: false,
666
- canBundle: false
667
- }));
668
- if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
669
- const seed = {
670
- openapi: MERGE_OPENAPI_VERSION,
671
- info: {
672
- title: MERGE_DEFAULT_TITLE,
673
- version: MERGE_DEFAULT_VERSION
674
- },
675
- paths: {},
676
- components: { schemas: {} }
677
- };
678
- return parseDocument(documents.reduce((acc, current) => mergeDeep(acc, current), seed));
405
+ async function parseDocument(pathOrApi) {
406
+ if (typeof pathOrApi === "string") return parseDocument(await bundleDocument(pathOrApi));
407
+ return upgrade(pathOrApi, "3.1");
679
408
  }
680
409
  /**
681
410
  * Creates a `Document` from an `AdapterSource`.
682
411
  *
683
- * Handles all three source types:
684
- * - `{ type: 'path' }` — resolves and bundles a local file path or remote URL.
685
- * - `{ type: 'paths' }` — merges multiple file paths into a single document.
686
- * - `{ type: 'data' }` — parses an inline string (YAML/JSON) or raw object.
412
+ * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
413
+ * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
687
414
  *
688
415
  * @example
689
416
  * ```ts
@@ -691,17 +418,30 @@ async function mergeDocuments(pathOrApi) {
691
418
  * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
692
419
  * ```
693
420
  */
694
- function parseFromConfig(source) {
695
- if (source.type === "data") {
696
- if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
697
- return parseDocument(source.data, { canBundle: false });
698
- }
699
- if (source.type === "paths") return mergeDocuments(source.paths);
700
- if (new URLPath(source.path).isURL) return parseDocument(source.path);
701
- return parseDocument(path.resolve(path.dirname(source.path), source.path));
421
+ async function parseFromConfig(source) {
422
+ if (source.type === "data") return parseDocument(typeof source.data === "string" ? parse(source.data) : structuredClone(source.data));
423
+ if (URL.canParse(source.path)) return parseDocument(source.path);
424
+ const resolved = path.resolve(path.dirname(source.path), source.path);
425
+ await assertInputExists(resolved);
426
+ return parseDocument(resolved);
427
+ }
428
+ /**
429
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
430
+ * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
431
+ * its parse error instead.
432
+ */
433
+ async function assertInputExists(input) {
434
+ if (URL.canParse(input)) return;
435
+ if (!await exists(input)) throw new Diagnostics.Error({
436
+ code: Diagnostics.code.inputNotFound,
437
+ severity: "error",
438
+ message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
439
+ help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
440
+ location: { kind: "config" }
441
+ });
702
442
  }
703
443
  /**
704
- * Validates an OpenAPI document using `oas-normalize` with colorized error output.
444
+ * Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
705
445
  *
706
446
  * @example
707
447
  * ```ts
@@ -710,35 +450,117 @@ function parseFromConfig(source) {
710
450
  */
711
451
  async function validateDocument(document, { throwOnError = false } = {}) {
712
452
  try {
713
- await new OASNormalize(document, {
714
- enablePaths: true,
715
- colorizeErrors: true
716
- }).validate({ parser: { validate: { errors: { colorize: true } } } });
453
+ const result = await validate(structuredClone(document), { validate: { errors: { colorize: true } } });
454
+ if (!result.valid) throw new Error(compileErrors(result));
717
455
  } catch (error) {
718
456
  if (throwOnError) throw error;
719
457
  }
720
458
  }
721
459
  //#endregion
460
+ //#region src/oas.ts
461
+ /**
462
+ * Returns `true` when a schema should be treated as nullable.
463
+ *
464
+ * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
465
+ * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
466
+ */
467
+ function isNullable(schema) {
468
+ if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
469
+ const schemaType = schema?.type;
470
+ if (schemaType === "null") return true;
471
+ if (Array.isArray(schemaType)) return schemaType.includes("null");
472
+ return false;
473
+ }
474
+ /**
475
+ * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
476
+ */
477
+ function isReference(obj) {
478
+ return !!obj && typeof obj === "object" && "$ref" in obj;
479
+ }
480
+ /**
481
+ * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
482
+ * excluding the Swagger 2 string form.
483
+ */
484
+ function isDiscriminator(obj) {
485
+ const record = obj;
486
+ return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
487
+ }
488
+ /**
489
+ * Returns `true` when a schema is a binary payload: an octet-stream string body.
490
+ */
491
+ function isBinary(schema) {
492
+ return schema.type === "string" && schema.contentMediaType === "application/octet-stream";
493
+ }
494
+ /**
495
+ * MIME type fragments that mark a media type as JSON-like.
496
+ *
497
+ * A content type is JSON when it contains any of these substrings. The `+json` entry catches
498
+ * structured-syntax suffixes such as `application/vnd.api+json`.
499
+ */
500
+ const jsonMimeFragments = [
501
+ "application/json",
502
+ "application/x-json",
503
+ "text/json",
504
+ "text/x-json",
505
+ "+json"
506
+ ];
507
+ /**
508
+ * Returns `true` when a media type string is JSON-like.
509
+ *
510
+ * @example
511
+ * ```ts
512
+ * isJsonMimeType('application/json') // true
513
+ * isJsonMimeType('application/vnd.api+json') // true
514
+ * isJsonMimeType('multipart/form-data') // false
515
+ * ```
516
+ */
517
+ function isJsonMimeType(mimeType) {
518
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
519
+ }
520
+ //#endregion
722
521
  //#region src/refs.ts
522
+ const _refCache = /* @__PURE__ */ new WeakMap();
723
523
  /**
724
524
  * Resolves a local JSON pointer reference from a document.
725
525
  *
726
- * Accepts `#/...` refs. Returns `null` for empty or non-local refs.
727
- * Throws when the pointer cannot be resolved.
526
+ * Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
527
+ * resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
528
+ * build there is no sink to collect it, so it throws instead.
728
529
  *
729
530
  * @example
730
531
  * ```ts
731
- * resolveRef<SchemaObject>(document, '#/components/schemas/Pet') // SchemaObject | null
532
+ * resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
732
533
  * ```
733
534
  */
734
535
  function resolveRef(document, $ref) {
735
536
  const origRef = $ref;
736
537
  $ref = $ref.trim();
737
538
  if ($ref === "") return null;
738
- if ($ref.startsWith("#")) $ref = globalThis.decodeURIComponent($ref.substring(1));
739
- else return null;
539
+ if (!$ref.startsWith("#")) return null;
540
+ $ref = globalThis.decodeURIComponent($ref.substring(1));
541
+ let docCache = _refCache.get(document);
542
+ if (!docCache) {
543
+ docCache = /* @__PURE__ */ new Map();
544
+ _refCache.set(document, docCache);
545
+ }
546
+ if (docCache.has($ref)) return docCache.get($ref);
740
547
  const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
741
- if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
548
+ if (!current) {
549
+ const diagnostic = {
550
+ code: Diagnostics.code.refNotFound,
551
+ severity: "error",
552
+ message: `Could not find a definition for ${origRef}.`,
553
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
554
+ location: {
555
+ kind: "schema",
556
+ pointer: origRef,
557
+ ref: origRef
558
+ }
559
+ };
560
+ if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
561
+ return null;
562
+ }
563
+ docCache.set($ref, current);
742
564
  return current;
743
565
  }
744
566
  /**
@@ -761,43 +583,210 @@ function dereferenceWithRef(document, schema) {
761
583
  };
762
584
  return schema;
763
585
  }
764
- //#endregion
765
- //#region src/resolvers.ts
766
586
  /**
767
- * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
768
- * Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
769
- * Throws if an override value is not in the variable's `enum` list.
587
+ * Resolves a `$ref` slot in place: when `container[key]` holds a `$ref`, replaces it with the
588
+ * resolved value and returns that value. Returns `null` when the slot is empty, cannot be resolved,
589
+ * or is still a `$ref` after resolving. A non-`$ref` value is returned untouched, without writing.
770
590
  *
771
591
  * @example
772
592
  * ```ts
773
- * resolveServerUrl(
774
- * { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
775
- * { env: 'prod' },
776
- * )
777
- * // 'https://prod.api.example.com'
593
+ * derefInPlace<ResponseObject>({ document, container: operation.schema.responses, key: '200' })
778
594
  * ```
779
595
  */
780
- function resolveServerUrl(server, overrides) {
781
- if (!server.variables) return server.url;
782
- let url = server.url;
783
- for (const [key, variable] of Object.entries(server.variables)) {
784
- const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
785
- if (value === void 0) continue;
786
- if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Error(`Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`);
787
- url = url.replaceAll(`{${key}}`, value);
788
- }
789
- return url;
596
+ function derefInPlace({ document, container, key }) {
597
+ const value = container[key];
598
+ if (!isReference(value)) return value ? value : null;
599
+ const resolved = resolveRef(document, value.$ref);
600
+ container[key] = resolved;
601
+ return resolved && !isReference(resolved) ? resolved : null;
790
602
  }
603
+ //#endregion
604
+ //#region src/operation.ts
791
605
  /**
792
- * Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
793
- * Formats not in `formatMap` (e.g., `int64`, `date-time`) are handled separately by parser options.
606
+ * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
607
+ * with no leading or trailing dash.
794
608
  */
795
- function getSchemaType(format) {
796
- return formatMap[format] ?? null;
609
+ function slugify(value) {
610
+ return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
797
611
  }
798
612
  /**
799
- * Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
800
- * Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
613
+ * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
614
+ */
615
+ function getOperationId({ path, method, schema }) {
616
+ const { operationId } = schema;
617
+ if (typeof operationId === "string" && operationId.length > 0) return operationId;
618
+ return `${method}_${slugify(path).toLowerCase()}`;
619
+ }
620
+ /**
621
+ * Returns the declared response status codes, skipping `x-` extensions and non-object entries.
622
+ */
623
+ function getResponseStatusCodes({ schema }) {
624
+ const responses = schema.responses;
625
+ if (!responses || isReference(responses)) return [];
626
+ return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
627
+ }
628
+ /**
629
+ * Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
630
+ */
631
+ function getResponseByStatusCode({ document, operation, statusCode }) {
632
+ const responses = operation.schema.responses;
633
+ if (!responses || isReference(responses)) return false;
634
+ return derefInPlace({
635
+ document,
636
+ container: responses,
637
+ key: statusCode
638
+ }) ?? false;
639
+ }
640
+ /**
641
+ * Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
642
+ * `undefined` when the operation has no request body.
643
+ */
644
+ function getRequestBodyContent({ document, operation }) {
645
+ return derefInPlace({
646
+ document,
647
+ container: operation.schema,
648
+ key: "requestBody"
649
+ })?.content;
650
+ }
651
+ /**
652
+ * Returns the request body media type. With `mediaType` set, returns that entry or `false`.
653
+ * Otherwise picks the first JSON-like media type, then the first declared one, as a
654
+ * `[mediaType, object]` tuple.
655
+ */
656
+ function getRequestContent({ document, operation, mediaType }) {
657
+ const content = getRequestBodyContent({
658
+ document,
659
+ operation
660
+ });
661
+ if (!content) return false;
662
+ if (mediaType) return mediaType in content ? content[mediaType] : false;
663
+ const mediaTypes = Object.keys(content);
664
+ const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
665
+ return available ? [available, content[available]] : false;
666
+ }
667
+ /**
668
+ * Returns the primary request content type. Prefers a JSON-like media type (the last one wins
669
+ * when several are declared), then the first declared one, defaulting to `'application/json'`.
670
+ */
671
+ function getRequestContentType({ document, operation }) {
672
+ const content = getRequestBodyContent({
673
+ document,
674
+ operation
675
+ });
676
+ const mediaTypes = content ? Object.keys(content) : [];
677
+ let result = mediaTypes[0] ?? "application/json";
678
+ for (const mt of mediaTypes) if (isJsonMimeType(mt)) result = mt;
679
+ return result;
680
+ }
681
+ /**
682
+ * Builds an `Operation` for every supported HTTP method on every path, in document order.
683
+ * `x-` path keys and unresolvable path-item `$ref`s are skipped.
684
+ *
685
+ * @example
686
+ * ```ts
687
+ * for (const operation of getOperations(document)) {
688
+ * parseOperation(options, operation)
689
+ * }
690
+ * ```
691
+ */
692
+ function getOperations(document) {
693
+ const operations = [];
694
+ const paths = document.paths;
695
+ if (!paths) return operations;
696
+ for (const path of Object.keys(paths)) {
697
+ if (path.startsWith("x-")) continue;
698
+ const pathItem = derefInPlace({
699
+ document,
700
+ container: paths,
701
+ key: path
702
+ });
703
+ if (!pathItem) continue;
704
+ const item = pathItem;
705
+ for (const method of Object.keys(item)) {
706
+ if (!SUPPORTED_METHODS.has(method)) continue;
707
+ const schema = item[method];
708
+ if (!schema || typeof schema !== "object") continue;
709
+ operations.push({
710
+ path,
711
+ method,
712
+ schema
713
+ });
714
+ }
715
+ }
716
+ return operations;
717
+ }
718
+ //#endregion
719
+ //#region src/resolvers.ts
720
+ /**
721
+ * Reads the server URL from the document's `servers` array at `server.index`,
722
+ * interpolating any `server.variables` into the URL template.
723
+ *
724
+ * Returns `null` when `server.index` is omitted or out of range.
725
+ *
726
+ * @example Resolve the first server
727
+ * `resolveBaseUrl({ document, server: { index: 0 } })`
728
+ *
729
+ * @example Override a path variable
730
+ * `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
731
+ */
732
+ function resolveBaseUrl({ document, server }) {
733
+ const index = server?.index;
734
+ const entry = index !== void 0 ? document.servers?.at(index) : void 0;
735
+ return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
736
+ }
737
+ /**
738
+ * Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
739
+ * Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
740
+ * Throws if an override value is not in the variable's `enum` list.
741
+ *
742
+ * @example
743
+ * ```ts
744
+ * resolveServerUrl(
745
+ * { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
746
+ * { env: 'prod' },
747
+ * )
748
+ * // 'https://prod.api.example.com'
749
+ * ```
750
+ */
751
+ function resolveServerUrl(server, overrides) {
752
+ if (!server.variables) return server.url;
753
+ let url = server.url;
754
+ for (const [key, variable] of Object.entries(server.variables)) {
755
+ const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
756
+ if (value === void 0) continue;
757
+ if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Diagnostics.Error({
758
+ code: Diagnostics.code.invalidServerVariable,
759
+ severity: "error",
760
+ message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
761
+ help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
762
+ location: {
763
+ kind: "document",
764
+ pointer: "#/servers"
765
+ }
766
+ });
767
+ url = url.replaceAll(`{${key}}`, value);
768
+ }
769
+ return url;
770
+ }
771
+ /**
772
+ * Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
773
+ * Formats not in `formatMap` (e.g., `int64`, `date-time`) are handled separately by parser options.
774
+ */
775
+ function getSchemaType(format) {
776
+ return formatMap[format] ?? null;
777
+ }
778
+ /**
779
+ * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
780
+ * `specialCasedFormats` that `convertFormat` handles directly. False means the format falls back to
781
+ * the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
782
+ * diagnostic in step with the parser as `formatMap` grows.
783
+ */
784
+ function isHandledFormat(format) {
785
+ return getSchemaType(format) !== null || specialCasedFormats.has(format);
786
+ }
787
+ /**
788
+ * Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
789
+ * Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
801
790
  */
802
791
  function getPrimitiveType(type) {
803
792
  if (type === "number" || type === "integer" || type === "bigint") return type;
@@ -805,15 +794,9 @@ function getPrimitiveType(type) {
805
794
  return "string";
806
795
  }
807
796
  /**
808
- * Narrows a content-type string to the `MediaType` union Kubb recognizes, or returns `null`.
809
- */
810
- function getMediaType(contentType) {
811
- return Object.values(ast.mediaTypes).includes(contentType) ? contentType : null;
812
- }
813
- /**
814
797
  * Returns all parameters for an operation, merging path-level and operation-level entries.
815
798
  * Operation-level parameters override path-level ones with the same `in:name` key.
816
- * `$ref` parameters resolve via `dereferenceWithRef` for backward compatibility.
799
+ * Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
817
800
  *
818
801
  * @example
819
802
  * ```ts
@@ -840,19 +823,19 @@ function getResponseBody(responseBody, contentType) {
840
823
  if (!(contentType in body.content)) return false;
841
824
  return body.content[contentType];
842
825
  }
843
- let availableContentType;
844
826
  const contentTypes = Object.keys(body.content);
845
- for (const mt of contentTypes) if (matchesMimeType.json(mt)) {
846
- availableContentType = mt;
847
- break;
848
- }
849
- if (!availableContentType) availableContentType = contentTypes[0];
850
- if (availableContentType) return [
851
- availableContentType,
852
- body.content[availableContentType],
853
- ...body.description ? [body.description] : []
854
- ];
855
- return false;
827
+ const availableContentType = contentTypes.find(isJsonMimeType) ?? contentTypes[0];
828
+ if (!availableContentType) return false;
829
+ return body.content[availableContentType];
830
+ }
831
+ function resolveResponseRefs(document, operation) {
832
+ const responses = operation.schema.responses;
833
+ if (!responses) return;
834
+ for (const key in responses) derefInPlace({
835
+ document,
836
+ container: responses,
837
+ key
838
+ });
856
839
  }
857
840
  /**
858
841
  * Returns the response schema for a given operation and HTTP status code.
@@ -866,16 +849,14 @@ function getResponseBody(responseBody, contentType) {
866
849
  * ```
867
850
  */
868
851
  function getResponseSchema(document, operation, statusCode, options = {}) {
869
- if (operation.schema.responses) {
870
- const responses = operation.schema.responses;
871
- for (const key in responses) {
872
- const schema = responses[key];
873
- if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
874
- }
875
- }
876
- const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType);
852
+ resolveResponseRefs(document, operation);
853
+ const responseBody = getResponseBody(getResponseByStatusCode({
854
+ document,
855
+ operation,
856
+ statusCode
857
+ }), options.contentType);
877
858
  if (responseBody === false) return {};
878
- const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
859
+ const schema = responseBody.schema;
879
860
  if (!schema) return {};
880
861
  return dereferenceWithRef(document, schema);
881
862
  }
@@ -889,16 +870,35 @@ function getResponseSchema(document, operation, statusCode, options = {}) {
889
870
  */
890
871
  function getRequestSchema(document, operation, options = {}) {
891
872
  if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
892
- const requestBody = operation.getRequestBody(options.contentType);
873
+ const requestBody = getRequestContent({
874
+ document,
875
+ operation,
876
+ mediaType: options.contentType
877
+ });
893
878
  if (requestBody === false) return null;
879
+ const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
894
880
  const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
881
+ if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
882
+ type: "string",
883
+ contentMediaType: "application/octet-stream"
884
+ };
895
885
  if (!schema) return null;
896
886
  return dereferenceWithRef(document, schema);
897
887
  }
898
888
  /**
889
+ * Returns `true` when `fragment` carries any JSON Schema keyword that makes it
890
+ * structurally significant on its own (see `structuralKeys`).
891
+ *
892
+ * A fragment with a structural keyword can't be safely merged into a parent schema.
893
+ */
894
+ function hasStructuralKeywords(fragment) {
895
+ for (const key in fragment) if (structuralKeys.has(key)) return true;
896
+ return false;
897
+ }
898
+ /**
899
899
  * Flattens a keyword-only `allOf` into its parent schema.
900
900
  *
901
- * Only flattens when every member is a plain fragment — no `$ref` and no structural keywords
901
+ * Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
902
902
  * (see `structuralKeys`). Outer schema values take precedence over fragment values.
903
903
  * Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
904
904
  *
@@ -906,35 +906,28 @@ function getRequestSchema(document, operation, options = {}) {
906
906
  * ```ts
907
907
  * flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
908
908
  * // { type: 'object', properties: {}, description: 'A pet' }
909
+ * ```
909
910
  *
911
+ * @example
912
+ * ```ts
910
913
  * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
911
- * // returned unchanged — contains a $ref
914
+ * // returned unchanged, contains a $ref
912
915
  * ```
913
916
  */
914
- /**
915
- * Returns `true` when `fragment` carries any JSON Schema keyword that makes it
916
- * structurally significant on its own (see `structuralKeys`).
917
- *
918
- * A fragment with a structural keyword can't be safely merged into a parent schema.
919
- */
920
- function hasStructuralKeywords(fragment) {
921
- for (const key in fragment) if (structuralKeys.has(key)) return true;
922
- return false;
923
- }
924
917
  function flattenSchema(schema) {
925
918
  if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
926
919
  const allOfFragments = schema.allOf;
927
- if (allOfFragments.some((item) => isRef(item))) return schema;
920
+ if (allOfFragments.some((item) => isReference(item))) return schema;
928
921
  if (allOfFragments.some(hasStructuralKeywords)) return schema;
929
- const merged = { ...schema };
930
- delete merged.allOf;
931
- for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) if (merged[key] === void 0) merged[key] = value;
922
+ const { allOf: _allOf, ...rest } = schema;
923
+ const merged = rest;
924
+ for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
932
925
  return merged;
933
926
  }
934
927
  /**
935
928
  * Extracts the inline schema from a media-type `content` map.
936
929
  *
937
- * Prefers `preferredContentType` when given; otherwise uses the first key in the map.
930
+ * Prefers `preferredContentType` when given, otherwise uses the first key in the map.
938
931
  * Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
939
932
  *
940
933
  * @example
@@ -953,21 +946,22 @@ function extractSchemaFromContent(content, preferredContentType) {
953
946
  /**
954
947
  * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
955
948
  */
956
- function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
949
+ function* collectRefs(schema) {
957
950
  if (Array.isArray(schema)) {
958
- for (const item of schema) collectRefs(item, refs);
959
- return refs;
951
+ for (const item of schema) yield* collectRefs(item);
952
+ return;
960
953
  }
961
954
  if (schema && typeof schema === "object") for (const key in schema) {
962
955
  const value = schema[key];
963
- if (key === "$ref" && typeof value === "string") {
964
- if (value.startsWith("#/components/schemas/")) {
965
- const name = value.slice(21);
966
- if (name) refs.add(name);
967
- }
968
- } else collectRefs(value, refs);
956
+ if (!(key === "$ref" && typeof value === "string")) {
957
+ yield* collectRefs(value);
958
+ continue;
959
+ }
960
+ if (value.startsWith("#/components/schemas/")) {
961
+ const name = value.slice(21);
962
+ if (name) yield name;
963
+ }
969
964
  }
970
- return refs;
971
965
  }
972
966
  /**
973
967
  * Returns a copy of `schemas` topologically sorted by `$ref` dependency.
@@ -983,7 +977,7 @@ function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
983
977
  */
984
978
  function sortSchemas(schemas) {
985
979
  const deps = /* @__PURE__ */ new Map();
986
- for (const [name, schema] of Object.entries(schemas)) deps.set(name, Array.from(collectRefs(schema)));
980
+ for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
987
981
  const sorted = [];
988
982
  const visited = /* @__PURE__ */ new Set();
989
983
  function visit(name, stack) {
@@ -1004,9 +998,6 @@ const semanticSuffixes = {
1004
998
  responses: "Response",
1005
999
  requestBodies: "Request"
1006
1000
  };
1007
- function getSemanticSuffix(source) {
1008
- return semanticSuffixes[source];
1009
- }
1010
1001
  function resolveSchemaRef(document, schema) {
1011
1002
  if (!isReference(schema)) return schema;
1012
1003
  const resolved = resolveRef(document, schema.$ref);
@@ -1024,7 +1015,7 @@ function resolveSchemaRef(document, schema) {
1024
1015
  *
1025
1016
  * @example
1026
1017
  * ```ts
1027
- * const { schemas, nameMapping } = getSchemas(document, { contentType: 'application/json' })
1018
+ * const { schemas, renames } = getSchemas(document, { contentType: 'application/json' })
1028
1019
  * ```
1029
1020
  */
1030
1021
  function getSchemas(document, { contentType }) {
@@ -1049,32 +1040,32 @@ function getSchemas(document, { contentType }) {
1049
1040
  normalizedNames.set(key, bucket);
1050
1041
  }
1051
1042
  const schemas = {};
1052
- const nameMapping = /* @__PURE__ */ new Map();
1043
+ const renames = /* @__PURE__ */ new Map();
1053
1044
  for (const [, items] of normalizedNames) {
1054
1045
  const isSingle = items.length === 1;
1055
1046
  let hasMultipleSources = false;
1056
1047
  if (!isSingle) {
1057
1048
  const firstSource = items[0].source;
1058
- for (let i = 1; i < items.length; i++) if (items[i].source !== firstSource) {
1049
+ for (const item of items) if (item.source !== firstSource) {
1059
1050
  hasMultipleSources = true;
1060
1051
  break;
1061
1052
  }
1062
1053
  }
1063
1054
  items.forEach((item, index) => {
1064
- const suffix = isSingle ? "" : hasMultipleSources ? getSemanticSuffix(item.source) : index === 0 ? "" : String(index + 1);
1055
+ const suffix = isSingle ? "" : hasMultipleSources ? semanticSuffixes[item.source] : index === 0 ? "" : String(index + 1);
1065
1056
  const uniqueName = item.originalName + suffix;
1066
1057
  schemas[uniqueName] = item.schema;
1067
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
1058
+ if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
1068
1059
  });
1069
1060
  }
1070
1061
  return {
1071
1062
  schemas: sortSchemas(schemas),
1072
- nameMapping
1063
+ renames
1073
1064
  };
1074
1065
  }
1075
1066
  /**
1076
1067
  * Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
1077
- * Returns `null` when `dateType: false`, signalling the format should fall through to `string`.
1068
+ * Returns `null` when `dateType: false`, so the format falls through to `string`.
1078
1069
  */
1079
1070
  function getDateType(options, format) {
1080
1071
  if (!options.dateType) return null;
@@ -1108,6 +1099,15 @@ function getDateType(options, format) {
1108
1099
  /**
1109
1100
  * Collects the shared metadata fields passed to every `createSchema` call.
1110
1101
  */
1102
+ /**
1103
+ * Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
1104
+ * labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
1105
+ * 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
1106
+ */
1107
+ function extractExamples(schema) {
1108
+ if (Array.isArray(schema.examples)) return schema.examples;
1109
+ return schema.example !== void 0 ? [schema.example] : void 0;
1110
+ }
1111
1111
  function buildSchemaNode(schema, name, nullable, defaultValue) {
1112
1112
  return {
1113
1113
  name,
@@ -1118,15 +1118,16 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
1118
1118
  readOnly: schema.readOnly,
1119
1119
  writeOnly: schema.writeOnly,
1120
1120
  default: defaultValue,
1121
- example: schema.example
1121
+ examples: extractExamples(schema),
1122
+ format: schema.format
1122
1123
  };
1123
1124
  }
1124
1125
  /**
1125
1126
  * Returns all request body content type keys for an operation.
1126
1127
  *
1127
- * The requestBody is dereferenced **in-place** when it is a `$ref` — the same mutation
1128
- * that `getRequestSchema` already performs — so that the returned list accurately reflects
1129
- * the available content types even for referenced bodies.
1128
+ * The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
1129
+ * `getRequestSchema` already performs), so the returned list accurately reflects the
1130
+ * available content types even for referenced bodies.
1130
1131
  *
1131
1132
  * @example
1132
1133
  * ```ts
@@ -1140,15 +1141,38 @@ function getRequestBodyContentTypes(document, operation) {
1140
1141
  if (!body) return [];
1141
1142
  return body.content ? Object.keys(body.content) : [];
1142
1143
  }
1144
+ /**
1145
+ * Returns all response content type keys for an operation at a given status code.
1146
+ *
1147
+ * Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
1148
+ * so the returned list reflects the available content types even for referenced responses.
1149
+ *
1150
+ * @example
1151
+ * ```ts
1152
+ * getResponseBodyContentTypes(document, operation, 200)
1153
+ * // ['application/json', 'application/xml']
1154
+ * ```
1155
+ */
1156
+ function getResponseBodyContentTypes(document, operation, statusCode) {
1157
+ resolveResponseRefs(document, operation);
1158
+ const responseObj = getResponseByStatusCode({
1159
+ document,
1160
+ operation,
1161
+ statusCode
1162
+ });
1163
+ if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
1164
+ const body = responseObj;
1165
+ return body.content ? Object.keys(body.content) : [];
1166
+ }
1143
1167
  //#endregion
1144
- //#region src/parser.ts
1168
+ //#region src/converters.ts
1145
1169
  /**
1146
1170
  * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
1147
1171
  *
1148
1172
  * This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
1149
- * from the array to its items sub-schema, making them valid for downstream processing.
1173
+ * from the array to its items sub-schema, so they are valid for downstream processing.
1150
1174
  *
1151
- * @note This is a defensive measure for robustness with non-compliant specs.
1175
+ * @note A defensive measure for non-compliant specs.
1152
1176
  */
1153
1177
  function normalizeArrayEnum(schema) {
1154
1178
  const normalizedItems = {
@@ -1162,490 +1186,677 @@ function normalizeArrayEnum(schema) {
1162
1186
  };
1163
1187
  }
1164
1188
  /**
1165
- * Factory function that creates schema and operation converters for a given OpenAPI context.
1166
- *
1167
- * Returns closures that share mutable state (`resolvingRefs` set for cycle detection).
1168
- * Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
1169
- * made possible by hoisting of function declarations.
1189
+ * Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
1190
+ * and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
1191
+ */
1192
+ function createNullNode(schema, name, nullable) {
1193
+ return ast.factory.createSchema({
1194
+ type: "null",
1195
+ primitive: "null",
1196
+ name,
1197
+ title: schema.title,
1198
+ description: schema.description,
1199
+ deprecated: schema.deprecated,
1200
+ nullable,
1201
+ format: schema.format
1202
+ });
1203
+ }
1204
+ /**
1205
+ * Names the inline enums on a property's schema, and on each item when the property is a tuple, from
1206
+ * the parent and property name. Wraps `macroEnumName` at the property construction site.
1207
+ */
1208
+ function nameEnums(node, options) {
1209
+ const macro = macroEnumName(options);
1210
+ const named = ast.applyMacros(node, [macro], { depth: "shallow" });
1211
+ const tupleNode = ast.narrowSchema(named, "tuple");
1212
+ if (tupleNode?.items) {
1213
+ const namedItems = tupleNode.items.map((item) => ast.applyMacros(item, [macro], { depth: "shallow" }));
1214
+ if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
1215
+ ...tupleNode,
1216
+ items: namedItems
1217
+ };
1218
+ }
1219
+ return named;
1220
+ }
1221
+ /**
1222
+ * Converts a `$ref` schema into a `RefSchemaNode`.
1170
1223
  *
1171
- * @note Not exported; called internally by `parseOas()` and `parseSchema()`.
1224
+ * The resolved schema is stored in `node.schema`. Usage-site sibling fields
1225
+ * (description, readOnly, nullable, etc.) are stored directly on the ref node.
1226
+ * Use `syncSchemaRef(node)` in printers to get a merged view of both.
1227
+ * Circular refs are detected in `resolveRefNode` and leave `schema` as `null`.
1172
1228
  */
1173
- function createSchemaParser(ctx) {
1174
- const document = ctx.document;
1175
- /**
1176
- * Tracks `$ref` paths that are currently being resolved to prevent infinite
1177
- * recursion when schemas contain circular references (e.g. `Pet → parent → Pet`).
1178
- */
1179
- const resolvingRefs = /* @__PURE__ */ new Set();
1180
- /**
1181
- * Converts a `$ref` schema into a `RefSchemaNode`.
1182
- *
1183
- * The resolved schema is stored in `node.schema`. Usage-site sibling fields
1184
- * (description, readOnly, nullable, etc.) are stored directly on the ref node.
1185
- * Use `syncSchemaRef(node)` in printers to get a merged view of both.
1186
- * Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
1187
- */
1188
- function convertRef({ schema, name, nullable, defaultValue, rawOptions }) {
1189
- let resolvedSchema;
1190
- const refPath = schema.$ref;
1191
- if (refPath && !resolvingRefs.has(refPath)) try {
1192
- const referenced = resolveRef(document, refPath);
1193
- if (referenced) {
1194
- resolvingRefs.add(refPath);
1195
- resolvedSchema = parseSchema({ schema: referenced }, rawOptions);
1196
- resolvingRefs.delete(refPath);
1197
- }
1198
- } catch {}
1199
- return ast.createSchema({
1200
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1201
- type: "ref",
1202
- name: ast.extractRefName(schema.$ref),
1203
- ref: schema.$ref,
1204
- schema: resolvedSchema
1229
+ function convertRef({ schema, name, nullable, defaultValue, rawOptions, document, resolveRefNode, refExists, renames }) {
1230
+ const refPath = schema.$ref;
1231
+ const resolvedSchema = refPath ? resolveRefNode(refPath, rawOptions) : null;
1232
+ if (refPath && document.components && !refExists(refPath)) return ast.factory.createSchema({
1233
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
1234
+ type: "unknown"
1235
+ });
1236
+ const targetName = renames?.get(schema.$ref);
1237
+ return ast.factory.createSchema({
1238
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
1239
+ type: "ref",
1240
+ name: extractRefName(schema.$ref),
1241
+ ref: schema.$ref,
1242
+ ...targetName ? { targetName } : {},
1243
+ schema: resolvedSchema
1244
+ });
1245
+ }
1246
+ /**
1247
+ * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
1248
+ */
1249
+ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse, document }) {
1250
+ if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
1251
+ const [memberSchema] = schema.allOf;
1252
+ const memberNode = parse({
1253
+ schema: memberSchema,
1254
+ name
1255
+ }, rawOptions);
1256
+ const { kind: _kind, ...memberNodeProps } = memberNode;
1257
+ const mergedNullable = nullable || memberNode.nullable || void 0;
1258
+ const mergedDefault = schema.default === null && mergedNullable ? void 0 : schema.default ?? memberNode.default;
1259
+ return ast.factory.createSchema({
1260
+ ...memberNodeProps,
1261
+ name,
1262
+ title: schema.title ?? memberNode.title,
1263
+ description: schema.description ?? memberNode.description,
1264
+ deprecated: schema.deprecated ?? memberNode.deprecated,
1265
+ nullable: mergedNullable,
1266
+ readOnly: schema.readOnly ?? memberNode.readOnly,
1267
+ writeOnly: schema.writeOnly ?? memberNode.writeOnly,
1268
+ default: mergedDefault,
1269
+ examples: extractExamples(schema) ?? memberNode.examples,
1270
+ pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0),
1271
+ format: schema.format ?? memberNode.format
1205
1272
  });
1206
1273
  }
1207
- /**
1208
- * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
1209
- */
1210
- function convertAllOf({ schema, name, nullable, defaultValue, rawOptions }) {
1211
- if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
1212
- const [memberSchema] = schema.allOf;
1213
- const memberNode = parseSchema({
1214
- schema: memberSchema,
1215
- name: null
1216
- }, rawOptions);
1217
- const { kind: _kind, ...memberNodeProps } = memberNode;
1218
- const mergedNullable = nullable || memberNode.nullable || void 0;
1219
- const mergedDefault = schema.default === null && mergedNullable ? void 0 : schema.default ?? memberNode.default;
1220
- return ast.createSchema({
1221
- ...memberNodeProps,
1222
- name,
1223
- title: schema.title ?? memberNode.title,
1224
- description: schema.description ?? memberNode.description,
1225
- deprecated: schema.deprecated ?? memberNode.deprecated,
1226
- nullable: mergedNullable,
1227
- readOnly: schema.readOnly ?? memberNode.readOnly,
1228
- writeOnly: schema.writeOnly ?? memberNode.writeOnly,
1229
- default: mergedDefault,
1230
- example: schema.example ?? memberNode.example,
1231
- pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0)
1274
+ const filteredDiscriminantValues = [];
1275
+ const allOfMembers = schema.allOf.filter((item) => {
1276
+ if (!isReference(item) || !name) return true;
1277
+ const deref = resolveRef(document, item.$ref);
1278
+ if (!deref || !isDiscriminator(deref)) return true;
1279
+ const parentUnion = deref.oneOf ?? deref.anyOf;
1280
+ if (!parentUnion) return true;
1281
+ const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1282
+ const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1283
+ const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1284
+ if (inOneOf || inMapping) {
1285
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
1286
+ if (discriminatorValue) filteredDiscriminantValues.push({
1287
+ propertyName: deref.discriminator.propertyName,
1288
+ value: discriminatorValue
1232
1289
  });
1290
+ return false;
1233
1291
  }
1234
- const filteredDiscriminantValues = [];
1235
- const allOfMembers = schema.allOf.filter((item) => {
1236
- if (!isReference(item) || !name) return true;
1237
- const deref = resolveRef(document, item.$ref);
1238
- if (!deref || !isDiscriminator(deref)) return true;
1239
- const parentUnion = deref.oneOf ?? deref.anyOf;
1240
- if (!parentUnion) return true;
1241
- const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1242
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1243
- const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1244
- if (inOneOf || inMapping) {
1245
- const discriminatorValue = ast.findDiscriminator(deref.discriminator.mapping, childRef);
1246
- if (discriminatorValue) filteredDiscriminantValues.push({
1247
- propertyName: deref.discriminator.propertyName,
1248
- value: discriminatorValue
1249
- });
1250
- return false;
1251
- }
1252
- return true;
1253
- }).map((s) => parseSchema({ schema: s }, rawOptions));
1254
- const syntheticStart = allOfMembers.length;
1255
- if (Array.isArray(schema.required) && schema.required.length) {
1256
- const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
1257
- const missingRequired = schema.required.filter((key) => !outerKeys.has(key));
1258
- if (missingRequired.length) {
1259
- const resolvedMembers = schema.allOf.flatMap((item) => {
1260
- if (!isReference(item)) return [item];
1261
- const deref = resolveRef(document, item.$ref);
1262
- return deref && !isReference(deref) ? [deref] : [];
1263
- });
1264
- for (const key of missingRequired) for (const resolved of resolvedMembers) if (resolved.properties?.[key]) {
1265
- allOfMembers.push(parseSchema({ schema: {
1266
- properties: { [key]: resolved.properties[key] },
1292
+ return true;
1293
+ }).map((s) => parse({
1294
+ schema: s,
1295
+ name
1296
+ }, rawOptions));
1297
+ const syntheticStart = allOfMembers.length;
1298
+ if (Array.isArray(schema.required) && schema.required.length) {
1299
+ const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
1300
+ const missingRequired = schema.required.filter((key) => !outerKeys.has(key));
1301
+ if (missingRequired.length) {
1302
+ const resolvedMembers = schema.allOf.flatMap((item) => {
1303
+ if (!isReference(item)) return [item];
1304
+ const deref = resolveRef(document, item.$ref);
1305
+ return deref && !isReference(deref) ? [deref] : [];
1306
+ });
1307
+ for (const key of missingRequired) for (const resolved of resolvedMembers) {
1308
+ const prop = resolved.properties?.[key];
1309
+ if (prop) {
1310
+ const memberSchema = {
1311
+ properties: { [key]: prop },
1267
1312
  required: [key]
1268
- } }, rawOptions));
1313
+ };
1314
+ allOfMembers.push(parse({
1315
+ schema: memberSchema,
1316
+ name
1317
+ }, rawOptions));
1269
1318
  break;
1270
1319
  }
1271
1320
  }
1272
1321
  }
1273
- if (schema.properties) {
1274
- const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
1275
- allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions));
1276
- }
1277
- for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(ast.createDiscriminantNode({
1278
- propertyName,
1279
- value
1280
- }));
1281
- return ast.createSchema({
1282
- type: "intersection",
1283
- members: [...ast.mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
1284
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1322
+ }
1323
+ if (schema.properties) {
1324
+ const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
1325
+ allOfMembers.push(parse({ schema: schemaWithoutAllOf }, rawOptions));
1326
+ }
1327
+ for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(createDiscriminantNode({
1328
+ propertyName,
1329
+ value
1330
+ }));
1331
+ return ast.factory.createSchema({
1332
+ type: "intersection",
1333
+ members: [...mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
1334
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1335
+ });
1336
+ }
1337
+ /**
1338
+ * Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
1339
+ */
1340
+ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse, document }) {
1341
+ function pickDiscriminatorPropertyNode(node, propertyName) {
1342
+ const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1343
+ if (!discriminatorProperty) return null;
1344
+ return ast.factory.createSchema({
1345
+ type: "object",
1346
+ primitive: "object",
1347
+ properties: [discriminatorProperty]
1285
1348
  });
1286
1349
  }
1287
- /**
1288
- * Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
1289
- */
1290
- function convertUnion({ schema, name, nullable, defaultValue, rawOptions }) {
1291
- function pickDiscriminatorPropertyNode(node, propertyName) {
1292
- const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1293
- if (!discriminatorProperty) return null;
1294
- return ast.createSchema({
1295
- type: "object",
1296
- primitive: "object",
1297
- properties: [discriminatorProperty]
1350
+ function resolveRefSilent($ref) {
1351
+ if (!$ref.startsWith("#")) return null;
1352
+ return decodeURIComponent($ref.substring(1)).split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document) ?? null;
1353
+ }
1354
+ function implicitDiscriminantValue(member) {
1355
+ if (!discriminator || discriminator.mapping || !isReference(member)) return null;
1356
+ const value = extractRefName(member.$ref);
1357
+ if (!value) return null;
1358
+ const variant = resolveRefSilent(member.$ref);
1359
+ if (!variant) return null;
1360
+ const propertyName = discriminator.propertyName;
1361
+ const seen = /* @__PURE__ */ new Set([member.$ref]);
1362
+ function constrains(v) {
1363
+ const prop = v.properties?.[propertyName];
1364
+ const resolved = prop && isReference(prop) ? resolveRefSilent(prop.$ref) : prop;
1365
+ if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
1366
+ const composition = v.allOf ?? v.oneOf ?? v.anyOf;
1367
+ if (!composition) return false;
1368
+ return composition.some((m) => {
1369
+ if (!isReference(m)) return constrains(m);
1370
+ if (seen.has(m.$ref)) return false;
1371
+ seen.add(m.$ref);
1372
+ const r = resolveRefSilent(m.$ref);
1373
+ return r ? constrains(r) : false;
1298
1374
  });
1299
1375
  }
1300
- const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
1301
- const strategy = schema.oneOf ? "one" : "any";
1302
- const unionBase = {
1303
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1304
- discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1305
- strategy
1306
- };
1307
- const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
1308
- const sharedPropertiesNode = schema.properties ? (() => {
1309
- const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema;
1310
- return parseSchema({
1311
- schema: discriminator ? Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== "discriminator")) : schemaWithoutUnion,
1376
+ return constrains(variant) ? null : value;
1377
+ }
1378
+ const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
1379
+ const strategy = schema.oneOf ? "one" : "any";
1380
+ const unionBase = {
1381
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
1382
+ discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1383
+ strategy
1384
+ };
1385
+ const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
1386
+ const { oneOf: _o, anyOf: _a, discriminator: _d, ...memberBaseSchema } = schema;
1387
+ const sharedPropertiesNode = schema.properties ? parse({
1388
+ schema: memberBaseSchema,
1389
+ name
1390
+ }, rawOptions) : void 0;
1391
+ if (sharedPropertiesNode || discriminator) {
1392
+ const members = unionMembers.map((s) => {
1393
+ const ref = isReference(s) ? s.$ref : void 0;
1394
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
1395
+ const memberNode = parse({
1396
+ schema: s,
1312
1397
  name
1313
1398
  }, rawOptions);
1314
- })() : void 0;
1315
- if (sharedPropertiesNode || discriminator?.mapping) {
1316
- const members = unionMembers.map((s) => {
1317
- const ref = isReference(s) ? s.$ref : void 0;
1318
- const discriminatorValue = ast.findDiscriminator(discriminator?.mapping, ref);
1319
- const memberNode = parseSchema({ schema: s }, rawOptions);
1320
- if (!discriminatorValue || !discriminator) return memberNode;
1321
- const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.setDiscriminatorEnum({
1322
- node: sharedPropertiesNode,
1323
- propertyName: discriminator.propertyName,
1324
- values: [discriminatorValue]
1325
- }), discriminator.propertyName) : void 0;
1326
- return ast.createSchema({
1327
- type: "intersection",
1328
- members: [memberNode, narrowedDiscriminatorNode ?? ast.createDiscriminantNode({
1329
- propertyName: discriminator.propertyName,
1330
- value: discriminatorValue
1331
- })]
1332
- });
1333
- });
1334
- const unionNode = ast.createSchema({
1335
- type: "union",
1336
- ...unionBase,
1337
- members
1338
- });
1339
- if (!sharedPropertiesNode) return unionNode;
1340
- return ast.createSchema({
1399
+ if (!discriminatorValue || !discriminator) return memberNode;
1400
+ const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.applyMacros(sharedPropertiesNode, [macroDiscriminatorEnum({
1401
+ propertyName: discriminator.propertyName,
1402
+ values: [discriminatorValue]
1403
+ })], { depth: "shallow" }), discriminator.propertyName) : void 0;
1404
+ return ast.factory.createSchema({
1341
1405
  type: "intersection",
1342
- ...buildSchemaNode(schema, name, nullable, defaultValue),
1343
- members: [unionNode, sharedPropertiesNode]
1406
+ members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
1407
+ propertyName: discriminator.propertyName,
1408
+ value: discriminatorValue
1409
+ })]
1344
1410
  });
1345
- }
1346
- return ast.createSchema({
1411
+ });
1412
+ const unionNode = ast.factory.createSchema({
1347
1413
  type: "union",
1348
1414
  ...unionBase,
1349
- members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s }, rawOptions)))
1415
+ members
1350
1416
  });
1351
- }
1352
- /**
1353
- * Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
1354
- */
1355
- function convertConst({ schema, name, nullable, defaultValue }) {
1356
- const constValue = schema.const;
1357
- if (constValue === null) return ast.createSchema({
1358
- type: "null",
1359
- primitive: "null",
1360
- name,
1361
- title: schema.title,
1362
- description: schema.description,
1363
- deprecated: schema.deprecated
1364
- });
1365
- const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
1366
- return ast.createSchema({
1367
- type: "enum",
1368
- primitive: constPrimitive,
1369
- enumValues: [constValue],
1370
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1417
+ if (!sharedPropertiesNode) return unionNode;
1418
+ return ast.factory.createSchema({
1419
+ type: "intersection",
1420
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
1421
+ members: [unionNode, sharedPropertiesNode]
1371
1422
  });
1372
1423
  }
1373
- /**
1374
- * Converts a format-annotated schema into a special-type `SchemaNode`.
1375
- * Returns `null` when the format should fall through to string handling (`dateType: false`).
1376
- */
1377
- function convertFormat({ schema, name, nullable, defaultValue, options }) {
1378
- const base = buildSchemaNode(schema, name, nullable, defaultValue);
1379
- if (schema.format === "int64") return ast.createSchema({
1380
- type: options.integerType === "bigint" ? "bigint" : "integer",
1381
- primitive: "integer",
1382
- ...base,
1383
- min: schema.minimum,
1384
- max: schema.maximum,
1385
- exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1386
- exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
1387
- });
1388
- if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
1389
- const dateType = getDateType(options, schema.format);
1390
- if (!dateType) return null;
1391
- if (dateType.type === "datetime") return ast.createSchema({
1392
- ...base,
1393
- primitive: "string",
1394
- type: "datetime",
1395
- offset: dateType.offset,
1396
- local: dateType.local
1397
- });
1398
- return ast.createSchema({
1399
- ...base,
1400
- primitive: "string",
1401
- type: dateType.type,
1402
- representation: dateType.representation
1403
- });
1404
- }
1405
- const specialType = getSchemaType(schema.format);
1406
- if (!specialType) return null;
1407
- const specialPrimitive = specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string";
1408
- if (specialType === "number" || specialType === "integer" || specialType === "bigint") return ast.createSchema({
1409
- ...base,
1410
- primitive: specialPrimitive,
1411
- type: specialType
1412
- });
1413
- if (specialType === "url") return ast.createSchema({
1414
- ...base,
1415
- primitive: "string",
1416
- type: "url",
1417
- min: schema.minLength,
1418
- max: schema.maxLength
1419
- });
1420
- if (specialType === "ipv4") return ast.createSchema({
1424
+ const unionNode = ast.factory.createSchema({
1425
+ type: "union",
1426
+ ...unionBase,
1427
+ members: unionMembers.map((s) => parse({
1428
+ schema: s,
1429
+ name
1430
+ }, rawOptions))
1431
+ });
1432
+ return ast.applyMacros(unionNode, [macroSimplifyUnion], { depth: "shallow" });
1433
+ }
1434
+ /**
1435
+ * Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
1436
+ */
1437
+ function convertConst({ schema, name, nullable, defaultValue }) {
1438
+ const constValue = schema.const;
1439
+ if (constValue === null) return createNullNode(schema, name);
1440
+ const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
1441
+ return ast.factory.createSchema({
1442
+ type: "enum",
1443
+ primitive: constPrimitive,
1444
+ enumValues: [constValue],
1445
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1446
+ });
1447
+ }
1448
+ /**
1449
+ * Converts a format-annotated schema into a special-type `SchemaNode`.
1450
+ * Returns `null` when the format should fall through to string handling (`dateType: false`).
1451
+ */
1452
+ function convertFormat({ schema, name, nullable, defaultValue, options }) {
1453
+ const base = buildSchemaNode(schema, name, nullable, defaultValue);
1454
+ if (schema.format === "int64") return ast.factory.createSchema({
1455
+ type: options.integerType === "bigint" ? "bigint" : "integer",
1456
+ primitive: "integer",
1457
+ ...base,
1458
+ min: schema.minimum,
1459
+ max: schema.maximum,
1460
+ exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1461
+ exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
1462
+ });
1463
+ if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
1464
+ const dateType = getDateType(options, schema.format);
1465
+ if (!dateType) return null;
1466
+ if (dateType.type === "datetime") return ast.factory.createSchema({
1421
1467
  ...base,
1422
1468
  primitive: "string",
1423
- type: "ipv4"
1469
+ type: "datetime",
1470
+ offset: dateType.offset,
1471
+ local: dateType.local
1424
1472
  });
1425
- if (specialType === "ipv6") return ast.createSchema({
1473
+ return ast.factory.createSchema({
1426
1474
  ...base,
1427
1475
  primitive: "string",
1428
- type: "ipv6"
1476
+ type: dateType.type,
1477
+ representation: dateType.representation
1429
1478
  });
1430
- if (specialType === "uuid" || specialType === "email") return ast.createSchema({
1431
- ...base,
1432
- primitive: "string",
1433
- type: specialType,
1479
+ }
1480
+ const specialType = getSchemaType(schema.format);
1481
+ if (!specialType) return null;
1482
+ const specialPrimitive = specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string";
1483
+ const hasLength = specialType === "url" || specialType === "uuid" || specialType === "email";
1484
+ return ast.factory.createSchema({
1485
+ ...base,
1486
+ primitive: specialPrimitive,
1487
+ type: specialType,
1488
+ ...hasLength ? {
1434
1489
  min: schema.minLength,
1435
1490
  max: schema.maxLength
1436
- });
1437
- return ast.createSchema({
1438
- ...base,
1439
- primitive: specialPrimitive,
1440
- type: specialType
1441
- });
1442
- }
1443
- /**
1444
- * Converts an `enum` schema into an `EnumSchemaNode`.
1445
- */
1446
- function convertEnum({ schema, name, nullable, type, rawOptions }) {
1447
- if (type === "array") return parseSchema({
1448
- schema: normalizeArrayEnum(schema),
1449
- name
1450
- }, rawOptions);
1451
- const nullInEnum = schema.enum.includes(null);
1452
- const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
1453
- const enumNullable = nullable || nullInEnum || void 0;
1454
- const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1455
- const enumPrimitive = getPrimitiveType(type);
1456
- const enumBase = {
1457
- type: "enum",
1458
- primitive: enumPrimitive,
1459
- name,
1460
- title: schema.title,
1461
- description: schema.description,
1462
- deprecated: schema.deprecated,
1463
- nullable: enumNullable,
1464
- readOnly: schema.readOnly,
1465
- writeOnly: schema.writeOnly,
1466
- default: enumDefault,
1467
- example: schema.example
1468
- };
1469
- const extensionKey = enumExtensionKeys.find((key) => key in schema);
1470
- if (extensionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
1471
- const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
1472
- const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
1473
- const uniqueValues = [...new Set(filteredValues)];
1474
- const seenNames = /* @__PURE__ */ new Set();
1475
- return ast.createSchema({
1476
- ...enumBase,
1477
- primitive: enumPrimitiveType,
1478
- namedEnumValues: uniqueValues.map((value, index) => ({
1479
- name: String(rawEnumNames?.[index] ?? value),
1480
- value,
1481
- primitive: enumPrimitiveType
1482
- })).filter((entry) => {
1483
- if (seenNames.has(entry.name)) return false;
1484
- seenNames.add(entry.name);
1485
- return true;
1486
- })
1487
- });
1488
- }
1489
- return ast.createSchema({
1491
+ } : {}
1492
+ });
1493
+ }
1494
+ /**
1495
+ * Converts an `enum` schema into an `EnumSchemaNode`.
1496
+ */
1497
+ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
1498
+ if (type === "array") return parse({
1499
+ schema: normalizeArrayEnum(schema),
1500
+ name
1501
+ }, rawOptions);
1502
+ const nullInEnum = schema.enum.includes(null);
1503
+ const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
1504
+ if (nullInEnum && filteredValues.length === 0) return createNullNode(schema, name);
1505
+ const enumNullable = nullable || nullInEnum || void 0;
1506
+ const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1507
+ const enumPrimitive = getPrimitiveType(type);
1508
+ const enumBase = {
1509
+ type: "enum",
1510
+ primitive: enumPrimitive,
1511
+ ...buildSchemaNode(schema, name, enumNullable, enumDefault)
1512
+ };
1513
+ const extensionKey = enumExtensionKeys.find((key) => key in schema);
1514
+ const descriptionKey = enumDescriptionKeys.find((key) => key in schema);
1515
+ if (extensionKey || descriptionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
1516
+ const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
1517
+ const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
1518
+ const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
1519
+ const uniqueValues = [...new Set(filteredValues)];
1520
+ const seenNames = /* @__PURE__ */ new Set();
1521
+ return ast.factory.createSchema({
1490
1522
  ...enumBase,
1491
- enumValues: [...new Set(filteredValues)]
1523
+ primitive: enumPrimitiveType,
1524
+ namedEnumValues: uniqueValues.map((value, index) => ({
1525
+ name: String(rawEnumNames?.[index] ?? value),
1526
+ value,
1527
+ primitive: enumPrimitiveType,
1528
+ description: rawEnumDescriptions?.[index]
1529
+ })).filter((entry) => {
1530
+ if (seenNames.has(entry.name)) return false;
1531
+ seenNames.add(entry.name);
1532
+ return true;
1533
+ })
1492
1534
  });
1493
1535
  }
1494
- /**
1495
- * Converts an object-like schema into an `ObjectSchemaNode`.
1496
- */
1497
- function convertObject({ schema, name, nullable, defaultValue, rawOptions, options }) {
1498
- const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
1499
- const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
1500
- const resolvedPropSchema = propSchema;
1501
- const propNullable = isNullable(resolvedPropSchema);
1502
- const propNode = parseSchema({
1503
- schema: resolvedPropSchema,
1504
- name: ast.childName(name, propName)
1505
- }, rawOptions);
1506
- let schemaNode = ast.setEnumName(propNode, name, propName, options.enumSuffix);
1507
- const tupleNode = ast.narrowSchema(schemaNode, "tuple");
1508
- if (tupleNode?.items) {
1509
- const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix));
1510
- if (namedItems.some((item, i) => item !== tupleNode.items[i])) schemaNode = {
1511
- ...tupleNode,
1512
- items: namedItems
1513
- };
1514
- }
1515
- return ast.createProperty({
1516
- name: propName,
1517
- schema: {
1518
- ...schemaNode,
1519
- nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
1520
- },
1521
- required
1522
- });
1523
- }) : [];
1524
- const additionalProperties = schema.additionalProperties;
1525
- let additionalPropertiesNode;
1526
- if (additionalProperties === true) additionalPropertiesNode = true;
1527
- else if (additionalProperties && Object.keys(additionalProperties).length > 0) additionalPropertiesNode = parseSchema({ schema: additionalProperties }, rawOptions);
1528
- else if (additionalProperties === false) additionalPropertiesNode = false;
1529
- else if (additionalProperties) additionalPropertiesNode = ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1530
- const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
1531
- const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? ast.createSchema({ type: typeOptionMap.get(options.unknownType) }) : parseSchema({ schema: patternSchema }, rawOptions)])) : void 0;
1532
- const objectNode = ast.createSchema({
1533
- type: "object",
1534
- primitive: "object",
1535
- properties,
1536
- additionalProperties: additionalPropertiesNode,
1537
- patternProperties,
1538
- minProperties: schema.minProperties,
1539
- maxProperties: schema.maxProperties,
1540
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1536
+ return ast.factory.createSchema({
1537
+ ...enumBase,
1538
+ enumValues: [...new Set(filteredValues)]
1539
+ });
1540
+ }
1541
+ /**
1542
+ * Converts an object-like schema into an `ObjectSchemaNode`.
1543
+ */
1544
+ function convertObject({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
1545
+ const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
1546
+ const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
1547
+ const resolvedPropSchema = propSchema;
1548
+ const propNullable = isNullable(resolvedPropSchema);
1549
+ const schemaNode = nameEnums(parse({
1550
+ schema: resolvedPropSchema,
1551
+ name: childName(name, propName)
1552
+ }, rawOptions), {
1553
+ parentName: name,
1554
+ propName,
1555
+ enumSuffix: options.enumSuffix
1541
1556
  });
1542
- if (isDiscriminator(schema) && schema.discriminator.mapping) {
1543
- const discPropName = schema.discriminator.propertyName;
1544
- const values = Object.keys(schema.discriminator.mapping);
1545
- const enumName = name ? ast.enumPropName(name, discPropName, options.enumSuffix) : void 0;
1546
- return ast.setDiscriminatorEnum({
1547
- node: objectNode,
1548
- propertyName: discPropName,
1549
- values,
1550
- enumName
1551
- });
1552
- }
1553
- return objectNode;
1554
- }
1555
- /**
1556
- * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
1557
- */
1558
- function convertTuple({ schema, name, nullable, defaultValue, rawOptions }) {
1559
- const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item }, rawOptions));
1560
- const rest = schema.items ? parseSchema({ schema: schema.items }, rawOptions) : ast.createSchema({ type: "any" });
1561
- return ast.createSchema({
1562
- type: "tuple",
1563
- primitive: "array",
1564
- items: tupleItems,
1565
- rest,
1566
- min: schema.minItems,
1567
- max: schema.maxItems,
1568
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1557
+ return ast.factory.createProperty({
1558
+ name: propName,
1559
+ schema: {
1560
+ ...schemaNode,
1561
+ nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
1562
+ },
1563
+ required
1569
1564
  });
1565
+ }) : [];
1566
+ const additionalProperties = schema.additionalProperties;
1567
+ const additionalPropertiesNode = (() => {
1568
+ if (additionalProperties === true) return true;
1569
+ if (additionalProperties === false) return false;
1570
+ if (additionalProperties && Object.keys(additionalProperties).length > 0) return parse({ schema: additionalProperties }, rawOptions);
1571
+ if (additionalProperties) return ast.factory.createSchema({ type: options.unknownType });
1572
+ })();
1573
+ const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
1574
+ const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? ast.factory.createSchema({ type: options.unknownType }) : parse({ schema: patternSchema }, rawOptions)])) : void 0;
1575
+ const objectNode = ast.factory.createSchema({
1576
+ type: "object",
1577
+ primitive: "object",
1578
+ properties,
1579
+ additionalProperties: additionalPropertiesNode,
1580
+ patternProperties,
1581
+ minProperties: schema.minProperties,
1582
+ maxProperties: schema.maxProperties,
1583
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1584
+ });
1585
+ if (isDiscriminator(schema) && schema.discriminator.mapping) {
1586
+ const discPropName = schema.discriminator.propertyName;
1587
+ const values = Object.keys(schema.discriminator.mapping);
1588
+ const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
1589
+ return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
1590
+ propertyName: discPropName,
1591
+ values,
1592
+ enumName
1593
+ })], { depth: "shallow" });
1570
1594
  }
1571
- /**
1572
- * Converts a `type: 'array'` schema into an `ArraySchemaNode`.
1573
- */
1574
- function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }) {
1575
- const rawItems = schema.items;
1576
- const itemName = rawItems?.enum?.length && name ? ast.enumPropName(void 0, name, options.enumSuffix) : void 0;
1577
- const items = rawItems ? [parseSchema({
1578
- schema: rawItems,
1579
- name: itemName
1580
- }, rawOptions)] : [];
1581
- return ast.createSchema({
1582
- type: "array",
1583
- primitive: "array",
1584
- items,
1585
- min: schema.minItems,
1586
- max: schema.maxItems,
1587
- unique: schema.uniqueItems ?? void 0,
1588
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1589
- });
1595
+ return objectNode;
1596
+ }
1597
+ /**
1598
+ * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
1599
+ */
1600
+ function convertTuple({ schema, name, nullable, defaultValue, rawOptions, parse }) {
1601
+ const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
1602
+ const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? ast.factory.createSchema({ type: "any" }) : parse({ schema: schema.items }, rawOptions);
1603
+ return ast.factory.createSchema({
1604
+ type: "tuple",
1605
+ primitive: "array",
1606
+ items: tupleItems,
1607
+ rest,
1608
+ min: schema.minItems,
1609
+ max: schema.maxItems,
1610
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1611
+ });
1612
+ }
1613
+ /**
1614
+ * Converts a `type: 'array'` schema into an `ArraySchemaNode`.
1615
+ */
1616
+ function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
1617
+ const rawItems = schema.items;
1618
+ const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name;
1619
+ const items = rawItems ? [parse({
1620
+ schema: rawItems,
1621
+ name: itemName
1622
+ }, rawOptions)] : [];
1623
+ return ast.factory.createSchema({
1624
+ type: "array",
1625
+ primitive: "array",
1626
+ items,
1627
+ min: schema.minItems,
1628
+ max: schema.maxItems,
1629
+ unique: schema.uniqueItems ?? void 0,
1630
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1631
+ });
1632
+ }
1633
+ /**
1634
+ * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1635
+ */
1636
+ function convertString({ schema, name, nullable, defaultValue }) {
1637
+ return ast.factory.createSchema({
1638
+ type: "string",
1639
+ primitive: "string",
1640
+ min: schema.minLength,
1641
+ max: schema.maxLength,
1642
+ pattern: schema.pattern,
1643
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1644
+ });
1645
+ }
1646
+ /**
1647
+ * Converts a `type: 'number'` or `type: 'integer'` schema.
1648
+ */
1649
+ function convertNumeric({ schema, name, nullable, defaultValue }, type) {
1650
+ return ast.factory.createSchema({
1651
+ type,
1652
+ primitive: type,
1653
+ min: schema.minimum,
1654
+ max: schema.maximum,
1655
+ exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1656
+ exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
1657
+ multipleOf: schema.multipleOf,
1658
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1659
+ });
1660
+ }
1661
+ /**
1662
+ * Converts a `type: 'boolean'` schema.
1663
+ */
1664
+ function convertBoolean({ schema, name, nullable, defaultValue }) {
1665
+ return ast.factory.createSchema({
1666
+ type: "boolean",
1667
+ primitive: "boolean",
1668
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1669
+ });
1670
+ }
1671
+ /**
1672
+ * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
1673
+ * into a `blob` node.
1674
+ */
1675
+ function convertBinary({ schema, name, nullable, defaultValue }) {
1676
+ return ast.factory.createSchema({
1677
+ type: "blob",
1678
+ primitive: "string",
1679
+ ...buildSchemaNode(schema, name, nullable, defaultValue)
1680
+ });
1681
+ }
1682
+ /**
1683
+ * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
1684
+ *
1685
+ * Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parse`
1686
+ * falls through and handles it as that single type with nullability already folded in.
1687
+ */
1688
+ function convertMultiType({ schema, name, nullable, defaultValue, rawOptions, parse }) {
1689
+ const types = schema.type;
1690
+ const nonNullTypes = types.filter((t) => t !== "null");
1691
+ if (nonNullTypes.length <= 1) return null;
1692
+ const arrayNullable = types.includes("null") || nullable || void 0;
1693
+ return ast.factory.createSchema({
1694
+ type: "union",
1695
+ members: nonNullTypes.map((t) => {
1696
+ return parse({
1697
+ schema: {
1698
+ ...schema,
1699
+ type: t
1700
+ },
1701
+ name
1702
+ }, rawOptions);
1703
+ }),
1704
+ ...buildSchemaNode(schema, name, arrayNullable, defaultValue)
1705
+ });
1706
+ }
1707
+ /**
1708
+ * Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
1709
+ * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
1710
+ * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
1711
+ * match/convert/fall-through contract.
1712
+ */
1713
+ const schemaRules = [
1714
+ {
1715
+ match: ({ schema }) => isReference(schema),
1716
+ convert: convertRef
1717
+ },
1718
+ {
1719
+ match: ({ schema }) => !!schema.allOf?.length,
1720
+ convert: convertAllOf
1721
+ },
1722
+ {
1723
+ match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length),
1724
+ convert: convertUnion
1725
+ },
1726
+ {
1727
+ match: ({ schema }) => "const" in schema && schema.const !== void 0,
1728
+ convert: convertConst
1729
+ },
1730
+ {
1731
+ match: ({ schema }) => !!schema.format,
1732
+ convert: convertFormat
1733
+ },
1734
+ {
1735
+ match: ({ schema }) => isBinary(schema),
1736
+ convert: convertBinary
1737
+ },
1738
+ {
1739
+ match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
1740
+ convert: convertMultiType
1741
+ },
1742
+ {
1743
+ match: ({ schema, type }) => !type && (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0),
1744
+ convert: convertString
1745
+ },
1746
+ {
1747
+ match: ({ schema, type }) => !type && (schema.minimum !== void 0 || schema.maximum !== void 0),
1748
+ convert: (ctx) => convertNumeric(ctx, "number")
1749
+ },
1750
+ {
1751
+ match: ({ schema }) => !!schema.enum?.length,
1752
+ convert: convertEnum
1753
+ },
1754
+ {
1755
+ match: ({ schema, type }) => type === "object" || !!schema.properties || !!schema.additionalProperties || "patternProperties" in schema,
1756
+ convert: convertObject
1757
+ },
1758
+ {
1759
+ match: ({ schema }) => "prefixItems" in schema,
1760
+ convert: convertTuple
1761
+ },
1762
+ {
1763
+ match: ({ schema, type }) => type === "array" || "items" in schema,
1764
+ convert: convertArray
1765
+ },
1766
+ {
1767
+ match: ({ type }) => type === "string",
1768
+ convert: convertString
1769
+ },
1770
+ {
1771
+ match: ({ type }) => type === "number",
1772
+ convert: (ctx) => convertNumeric(ctx, "number")
1773
+ },
1774
+ {
1775
+ match: ({ type }) => type === "integer",
1776
+ convert: (ctx) => convertNumeric(ctx, "integer")
1777
+ },
1778
+ {
1779
+ match: ({ type }) => type === "boolean",
1780
+ convert: convertBoolean
1781
+ },
1782
+ {
1783
+ match: ({ type }) => type === "null",
1784
+ convert: ({ schema, name, nullable }) => createNullNode(schema, name, nullable)
1590
1785
  }
1786
+ ];
1787
+ //#endregion
1788
+ //#region src/parser.ts
1789
+ /**
1790
+ * Creates the schema and operation converters bound to one OpenAPI document.
1791
+ *
1792
+ * Owns the per-instance `$ref` state (cycle detection, resolved-node cache, existence cache) and
1793
+ * the `parseSchema` recursion seam, then dispatches each schema through the ordered `schemaRules`
1794
+ * table from `converters.ts`. Every converter is a standalone function that recurses through the
1795
+ * `parse` function passed to it, so this file only wires state to the converters.
1796
+ *
1797
+ * @internal
1798
+ */
1799
+ function createSchemaParser(ctx) {
1800
+ const document = ctx.document;
1591
1801
  /**
1592
- * Converts a `type: 'string'` schema into a `StringSchemaNode`.
1802
+ * Tracks `$ref` paths that are currently being resolved to prevent infinite
1803
+ * recursion when schemas contain circular references (e.g. `Pet → parent → Pet`).
1593
1804
  */
1594
- function convertString({ schema, name, nullable, defaultValue }) {
1595
- return ast.createSchema({
1596
- type: "string",
1597
- primitive: "string",
1598
- min: schema.minLength,
1599
- max: schema.maxLength,
1600
- pattern: schema.pattern,
1601
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1602
- });
1603
- }
1805
+ const resolvingRefs = /* @__PURE__ */ new Set();
1604
1806
  /**
1605
- * Converts a `type: 'number'` or `type: 'integer'` schema.
1807
+ * Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
1808
+ *
1809
+ * Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
1810
+ * it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
1811
+ * since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
1812
+ * Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
1606
1813
  */
1607
- function convertNumeric({ schema, name, nullable, defaultValue }, type) {
1608
- return ast.createSchema({
1609
- type,
1610
- primitive: type,
1611
- min: schema.minimum,
1612
- max: schema.maximum,
1613
- exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
1614
- exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
1615
- multipleOf: schema.multipleOf,
1616
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1617
- });
1618
- }
1814
+ const resolvedRefCache = /* @__PURE__ */ new Map();
1619
1815
  /**
1620
- * Converts a `type: 'boolean'` schema.
1816
+ * Memoized record of whether a `$ref` path resolves to a node the document actually defines.
1817
+ * A circular ref still resolves to an existing target, so this stays `true` for cycles and only
1818
+ * goes `false` for a `$ref` that points at a component the spec never declares.
1621
1819
  */
1622
- function convertBoolean({ schema, name, nullable, defaultValue }) {
1623
- return ast.createSchema({
1624
- type: "boolean",
1625
- primitive: "boolean",
1626
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1627
- });
1820
+ const refExistence = /* @__PURE__ */ new Map();
1821
+ function refExists(refPath) {
1822
+ if (!refExistence.has(refPath)) {
1823
+ let exists = false;
1824
+ try {
1825
+ exists = !!resolveRef(document, refPath);
1826
+ } catch {
1827
+ exists = false;
1828
+ }
1829
+ refExistence.set(refPath, exists);
1830
+ }
1831
+ return refExistence.get(refPath) ?? false;
1628
1832
  }
1629
1833
  /**
1630
- * Converts an explicit `type: 'null'` schema.
1834
+ * Resolves a `$ref` to its parsed node, guarding against cycles and memoizing per instance.
1835
+ * Returns `null` when the ref is currently being resolved (a cycle) or cannot be resolved
1836
+ * (e.g. a minimal document in a unit test).
1631
1837
  */
1632
- function convertNull({ schema, name, nullable }) {
1633
- return ast.createSchema({
1634
- type: "null",
1635
- primitive: "null",
1636
- name,
1637
- title: schema.title,
1638
- description: schema.description,
1639
- deprecated: schema.deprecated,
1640
- nullable
1641
- });
1838
+ function resolveRefNode(refPath, rawOptions) {
1839
+ if (resolvingRefs.has(refPath)) return null;
1840
+ if (!resolvedRefCache.has(refPath)) {
1841
+ let resolved = null;
1842
+ try {
1843
+ const referenced = resolveRef(document, refPath);
1844
+ if (referenced) {
1845
+ resolvingRefs.add(refPath);
1846
+ resolved = parseSchema({ schema: referenced }, rawOptions);
1847
+ resolvingRefs.delete(refPath);
1848
+ }
1849
+ } catch {}
1850
+ resolvedRefCache.set(refPath, resolved);
1851
+ }
1852
+ return resolvedRefCache.get(refPath) ?? null;
1642
1853
  }
1643
1854
  /**
1644
- * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
1855
+ * Converts an OAS `SchemaObject` into a `SchemaNode`.
1645
1856
  *
1646
- * Dispatch order (first match wins): `$ref` → `allOf` → `oneOf`/`anyOf` → `const` → `format`
1647
- * → octet-stream blob → multi-type array → constraint-inferred type → `enum` → object/array/tuple/scalar
1648
- * → empty-schema fallback (`emptySchemaType` option).
1857
+ * Builds the per-schema context, then walks the ordered {@link schemaRules} table and returns
1858
+ * the first converter that produces a node. When none match, falls back to the configured
1859
+ * `emptySchemaType`.
1649
1860
  */
1650
1861
  function parseSchema({ schema, name }, rawOptions) {
1651
1862
  const options = {
@@ -1658,80 +1869,57 @@ function createSchemaParser(ctx) {
1658
1869
  name
1659
1870
  }, rawOptions);
1660
1871
  const nullable = isNullable(schema) || void 0;
1661
- const defaultValue = schema.default === null && nullable ? void 0 : schema.default;
1662
- const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
1663
- const ctx = {
1872
+ const context = {
1664
1873
  schema,
1665
1874
  name,
1666
1875
  nullable,
1667
- defaultValue,
1668
- type,
1876
+ defaultValue: schema.default === null && nullable ? void 0 : schema.default,
1877
+ type: Array.isArray(schema.type) ? schema.type[0] : schema.type,
1669
1878
  rawOptions,
1670
- options
1879
+ options,
1880
+ parse: parseSchema,
1881
+ document,
1882
+ resolveRefNode,
1883
+ refExists,
1884
+ renames: ctx.renames
1671
1885
  };
1672
- if (isReference(schema)) return convertRef(ctx);
1673
- if (schema.allOf?.length) return convertAllOf(ctx);
1674
- if ([...schema.oneOf ?? [], ...schema.anyOf ?? []].length) return convertUnion(ctx);
1675
- if ("const" in schema && schema.const !== void 0) return convertConst(ctx);
1676
- if (schema.format) {
1677
- const formatResult = convertFormat(ctx);
1678
- if (formatResult) return formatResult;
1886
+ for (const rule of schemaRules) {
1887
+ if (!rule.match(context)) continue;
1888
+ const node = rule.convert(context);
1889
+ if (node) return node;
1679
1890
  }
1680
- if (schema.type === "string" && schema.contentMediaType === "application/octet-stream") return ast.createSchema({
1681
- type: "blob",
1682
- primitive: "string",
1683
- ...buildSchemaNode(schema, name, nullable, defaultValue)
1684
- });
1685
- if (Array.isArray(schema.type) && schema.type.length > 1) {
1686
- const nonNullTypes = schema.type.filter((t) => t !== "null");
1687
- const arrayNullable = schema.type.includes("null") || nullable || void 0;
1688
- if (nonNullTypes.length > 1) return ast.createSchema({
1689
- type: "union",
1690
- members: nonNullTypes.map((t) => parseSchema({
1691
- schema: {
1692
- ...schema,
1693
- type: t
1694
- },
1695
- name
1696
- }, rawOptions)),
1697
- ...buildSchemaNode(schema, name, arrayNullable, defaultValue)
1698
- });
1699
- }
1700
- if (!type) {
1701
- if (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0) return convertString(ctx);
1702
- if (schema.minimum !== void 0 || schema.maximum !== void 0) return convertNumeric(ctx, "number");
1703
- }
1704
- if (schema.enum?.length) return convertEnum(ctx);
1705
- if (type === "object" || schema.properties || schema.additionalProperties || "patternProperties" in schema) return convertObject(ctx);
1706
- if ("prefixItems" in schema) return convertTuple(ctx);
1707
- if (type === "array" || "items" in schema) return convertArray(ctx);
1708
- if (type === "string") return convertString(ctx);
1709
- if (type === "number") return convertNumeric(ctx, "number");
1710
- if (type === "integer") return convertNumeric(ctx, "integer");
1711
- if (type === "boolean") return convertBoolean(ctx);
1712
- if (type === "null") return convertNull(ctx);
1713
- const emptyType = typeOptionMap.get(options.emptySchemaType);
1714
- return ast.createSchema({
1891
+ const emptyType = options.emptySchemaType;
1892
+ return ast.factory.createSchema({
1715
1893
  type: emptyType,
1716
1894
  name,
1717
1895
  title: schema.title,
1718
- description: schema.description
1896
+ description: schema.description,
1897
+ format: schema.format
1719
1898
  });
1720
1899
  }
1721
1900
  /**
1722
1901
  * Converts a dereferenced OAS parameter object into a `ParameterNode`.
1723
1902
  */
1724
- function parseParameter(options, param) {
1903
+ function parseParameter(options, param, parentName) {
1725
1904
  const required = param["required"] ?? false;
1726
- const schema = param["schema"] ? parseSchema({ schema: param["schema"] }, options) : ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
1727
- return ast.createParameter({
1728
- name: param["name"],
1905
+ const paramName = param["name"];
1906
+ const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : void 0;
1907
+ const schema = param["schema"] ? parseSchema({
1908
+ schema: param["schema"],
1909
+ name: schemaName
1910
+ }, options) : ast.factory.createSchema({ type: options.unknownType });
1911
+ const style = param["style"];
1912
+ const explode = param["explode"];
1913
+ return ast.factory.createParameter({
1914
+ name: paramName,
1729
1915
  in: param["in"],
1730
1916
  schema: {
1731
1917
  ...schema,
1732
1918
  description: param["description"] ?? schema.description
1733
1919
  },
1734
- required
1920
+ required,
1921
+ ...style !== void 0 ? { style } : {},
1922
+ ...explode !== void 0 ? { explode } : {}
1735
1923
  });
1736
1924
  }
1737
1925
  /**
@@ -1747,73 +1935,97 @@ function createSchemaParser(ctx) {
1747
1935
  };
1748
1936
  }
1749
1937
  /**
1750
- * Reads the inline response object (not a `$ref`) and returns its description plus its `content` map.
1751
- */
1752
- function getResponseMeta(responseObj) {
1753
- if (typeof responseObj !== "object" || responseObj === null || Array.isArray(responseObj)) return {};
1754
- const inline = responseObj;
1755
- return {
1756
- description: inline.description,
1757
- content: inline.content
1758
- };
1759
- }
1760
- /**
1761
1938
  * Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
1762
1939
  * `$ref` entries are skipped since their flags live on the dereferenced target.
1763
1940
  */
1764
1941
  function collectPropertyKeysByFlag(schema, flag) {
1765
- if (!schema?.properties) return void 0;
1942
+ if (!schema?.properties) return null;
1766
1943
  const keys = [];
1767
1944
  for (const key in schema.properties) {
1768
1945
  const prop = schema.properties[key];
1769
1946
  if (prop && !isReference(prop) && prop[flag]) keys.push(key);
1770
1947
  }
1771
- return keys.length ? keys : void 0;
1948
+ return keys.length ? keys : null;
1772
1949
  }
1773
1950
  /**
1774
1951
  * Converts an OAS `Operation` into an `OperationNode`.
1775
1952
  */
1776
1953
  function parseOperation(options, operation) {
1777
- const parameters = getParameters(document, operation).map((param) => parseParameter(options, param));
1954
+ const operationId = getOperationId(operation);
1955
+ const operationName = operationId ? pascalCase(operationId) : void 0;
1956
+ const parameters = getParameters(document, operation).map((param) => parseParameter(options, param, operationName));
1778
1957
  const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
1779
1958
  const requestBodyMeta = getRequestBodyMeta(operation);
1959
+ const requestBodyName = operationName ? `${operationName}Request` : void 0;
1780
1960
  const content = allContentTypes.flatMap((ct) => {
1781
1961
  const schema = getRequestSchema(document, operation, { contentType: ct });
1782
1962
  if (!schema) return [];
1783
- return [{
1963
+ return [ast.factory.createContent({
1784
1964
  contentType: ct,
1785
- schema: ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
1965
+ schema: ast.optionality(parseSchema({
1966
+ schema,
1967
+ name: requestBodyName
1968
+ }, options), requestBodyMeta.required),
1786
1969
  keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
1787
- }];
1970
+ })];
1788
1971
  });
1789
1972
  const requestBody = content.length > 0 || requestBodyMeta.description ? {
1790
1973
  description: requestBodyMeta.description,
1791
1974
  required: requestBodyMeta.required || void 0,
1792
1975
  content: content.length > 0 ? content : void 0
1793
1976
  } : void 0;
1794
- const responses = operation.getResponseStatusCodes().map((statusCode) => {
1795
- const responseObj = operation.getResponseByStatusCode(statusCode);
1796
- const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType });
1797
- const schema = responseSchema && Object.keys(responseSchema).length > 0 ? parseSchema({ schema: responseSchema }, options) : ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType) });
1798
- const { description, content } = getResponseMeta(responseObj);
1799
- const mediaType = content ? getMediaType(Object.keys(content)[0] ?? "") : getMediaType(operation.contentType ?? "");
1800
- return ast.createResponse({
1977
+ const responses = getResponseStatusCodes(operation).map((statusCode) => {
1978
+ const responseObj = getResponseByStatusCode({
1979
+ document,
1980
+ operation,
1981
+ statusCode
1982
+ });
1983
+ const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
1984
+ const description = typeof responseObj === "object" && responseObj !== null ? responseObj.description : void 0;
1985
+ const parseEntrySchema = (contentType) => {
1986
+ const raw = getResponseSchema(document, operation, statusCode, { contentType });
1987
+ return {
1988
+ schema: raw && Object.keys(raw).length > 0 ? parseSchema({
1989
+ schema: raw,
1990
+ name: responseName
1991
+ }, options) : ast.factory.createSchema({ type: options.emptySchemaType }),
1992
+ keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
1993
+ };
1994
+ };
1995
+ const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)).map((contentType) => ast.factory.createContent({
1996
+ contentType,
1997
+ ...parseEntrySchema(contentType)
1998
+ }));
1999
+ if (content.length === 0) content.push(ast.factory.createContent({
2000
+ contentType: getRequestContentType({
2001
+ document,
2002
+ operation
2003
+ }) || "application/json",
2004
+ ...parseEntrySchema(ctx.contentType)
2005
+ }));
2006
+ return ast.factory.createResponse({
1801
2007
  statusCode,
1802
2008
  description,
1803
- schema,
1804
- mediaType,
1805
- keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
2009
+ content
1806
2010
  });
1807
2011
  });
1808
- const urlPath = new URLPath(operation.path);
1809
- return ast.createOperation({
1810
- operationId: operation.getOperationId(),
2012
+ const pathItem = document.paths?.[operation.path];
2013
+ const pathItemDoc = pathItem && !isReference(pathItem) ? pathItem : void 0;
2014
+ const pickDoc = (key) => {
2015
+ const own = operation.schema[key];
2016
+ if (typeof own === "string") return own;
2017
+ const fallback = pathItemDoc?.[key];
2018
+ return typeof fallback === "string" ? fallback : void 0;
2019
+ };
2020
+ return ast.factory.createOperation({
2021
+ operationId,
2022
+ protocol: "http",
1811
2023
  method: operation.method.toUpperCase(),
1812
- path: urlPath.path,
1813
- tags: operation.getTags().map((tag) => tag.name),
1814
- summary: operation.getSummary() || void 0,
1815
- description: operation.getDescription() || void 0,
1816
- deprecated: operation.isDeprecated() || void 0,
2024
+ path: operation.path,
2025
+ tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
2026
+ summary: pickDoc("summary") || void 0,
2027
+ description: pickDoc("description") || void 0,
2028
+ deprecated: operation.schema.deprecated || void 0,
1817
2029
  parameters,
1818
2030
  requestBody,
1819
2031
  responses
@@ -1825,59 +2037,122 @@ function createSchemaParser(ctx) {
1825
2037
  parseParameter
1826
2038
  };
1827
2039
  }
2040
+ //#endregion
2041
+ //#region src/promoteEnums.ts
1828
2042
  /**
1829
- * Parses an OpenAPI specification into Kubb's universal `InputNode` AST.
1830
- *
1831
- * This is the main entry point for `@kubb/adapter-oas`. It converts OpenAPI/Swagger specs into a spec-agnostic tree
1832
- * that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here —
1833
- * the tree is a pure data structure representing all schemas and operations.
1834
- *
1835
- * Returns the AST root and a `nameMapping` for resolving schema references.
1836
- *
1837
- * @example
1838
- * ```ts
1839
- * import { parseOas } from '@kubb/adapter-oas'
1840
- *
1841
- * const document = await parseFromConfig(config)
1842
- * const { root, nameMapping } = parseOas(document, { dateType: 'date', contentType: 'application/json' })
1843
- * ```
2043
+ * Collects inline enums to lift to the top level, keyed by the name the parser derived for them
2044
+ * (e.g. `PetStatusEnum`). An enum already defined as a top-level component is left as-is, and a
2045
+ * name that recurs maps to the first definition so each name yields one shared type.
1844
2046
  */
1845
- function parseOas(document, options = {}) {
1846
- const { contentType, ...parserOptions } = options;
1847
- const mergedOptions = {
1848
- ...DEFAULT_PARSER_OPTIONS,
1849
- ...parserOptions
1850
- };
1851
- const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType });
1852
- const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({
1853
- document,
1854
- contentType
2047
+ function collectInlineEnums(roots, topLevelNames) {
2048
+ const promoted = /* @__PURE__ */ new Map();
2049
+ for (const root of roots) {
2050
+ const isSchemaRoot = root.kind === "Schema";
2051
+ for (const node of ast.collect(root, { schema: (schemaNode) => schemaNode })) {
2052
+ if (node.type !== "enum" || !node.name) continue;
2053
+ if (isSchemaRoot && node === root) continue;
2054
+ if (topLevelNames.has(node.name)) continue;
2055
+ if (!promoted.has(node.name)) promoted.set(node.name, {
2056
+ ...node,
2057
+ optional: void 0,
2058
+ nullish: void 0
2059
+ });
2060
+ }
2061
+ }
2062
+ return promoted;
2063
+ }
2064
+ /**
2065
+ * Replaces every promoted inline enum in `node` with a `ref` to its lifted definition, keeping the
2066
+ * occurrence's usage-slot and documentation fields.
2067
+ */
2068
+ function refPromotedEnums(node, promoted) {
2069
+ if (promoted.size === 0) return node;
2070
+ return ast.transform(node, { schema(schemaNode) {
2071
+ if (schemaNode.type !== "enum" || !schemaNode.name || !promoted.has(schemaNode.name)) return void 0;
2072
+ return ast.factory.createSchema({
2073
+ type: "ref",
2074
+ name: schemaNode.name,
2075
+ ref: `${SCHEMA_REF_PREFIX}${schemaNode.name}`,
2076
+ optional: schemaNode.optional,
2077
+ nullish: schemaNode.nullish,
2078
+ readOnly: schemaNode.readOnly,
2079
+ writeOnly: schemaNode.writeOnly,
2080
+ deprecated: schemaNode.deprecated,
2081
+ description: schemaNode.description,
2082
+ default: schemaNode.default,
2083
+ examples: schemaNode.examples
2084
+ });
2085
+ } });
2086
+ }
2087
+ //#endregion
2088
+ //#region src/schemaDiagnostics.ts
2089
+ /**
2090
+ * Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
2091
+ * top-level schema. Walks the node the parser produced, threading the RFC 6901
2092
+ * pointer as it descends so a nested field reports against its full path
2093
+ * (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
2094
+ * resolved schema is reported under its own walk. Reports land in the active build run, are a
2095
+ * no-op outside one, and repeats are deduped by the build.
2096
+ */
2097
+ function reportSchemaDiagnostics({ node, name }) {
2098
+ visit(node, `#/components/schemas/${escapePointerToken(name)}`);
2099
+ }
2100
+ /**
2101
+ * Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
2102
+ * property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
2103
+ */
2104
+ function escapePointerToken(token) {
2105
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
2106
+ }
2107
+ function visit(node, pointer) {
2108
+ if (node.deprecated) Diagnostics.report({
2109
+ code: Diagnostics.code.deprecated,
2110
+ severity: "info",
2111
+ message: "This schema is marked as deprecated.",
2112
+ location: {
2113
+ kind: "schema",
2114
+ pointer
2115
+ }
1855
2116
  });
1856
- const schemas = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({
1857
- schema,
1858
- name
1859
- }, mergedOptions));
1860
- const paths = new BaseOas(document).getPaths();
1861
- const operations = Object.entries(paths).flatMap(([_path, methods]) => Object.entries(methods).map(([, operation]) => operation ? _parseOperation(mergedOptions, operation) : null).filter((op) => op !== null));
1862
- return {
1863
- root: ast.createInput({
1864
- schemas,
1865
- operations
1866
- }),
1867
- nameMapping
1868
- };
2117
+ if (typeof node.format === "string" && !isHandledFormat(node.format)) Diagnostics.report({
2118
+ code: Diagnostics.code.unsupportedFormat,
2119
+ severity: "warning",
2120
+ message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
2121
+ help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
2122
+ location: {
2123
+ kind: "schema",
2124
+ pointer
2125
+ }
2126
+ });
2127
+ if (node.type === "object") {
2128
+ for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
2129
+ if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
2130
+ return;
2131
+ }
2132
+ if (node.type === "array") {
2133
+ for (const item of node.items ?? []) visit(item, `${pointer}/items`);
2134
+ return;
2135
+ }
2136
+ if (node.type === "tuple") {
2137
+ for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
2138
+ return;
2139
+ }
2140
+ if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
1869
2141
  }
1870
2142
  //#endregion
1871
2143
  //#region src/adapter.ts
1872
2144
  /**
1873
- * Stable string identifier for the OAS adapter used in Kubb's adapter registry.
2145
+ * The `name` of `@kubb/adapter-oas`, used to identify this adapter in a Kubb config.
1874
2146
  */
1875
2147
  const adapterOasName = "oas";
1876
2148
  /**
1877
- * Creates the default OpenAPI / Swagger adapter for Kubb.
2149
+ * Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.1 specifications. Reads the
2150
+ * spec from `input` (a file path, URL, inline content, or parsed object), validates
2151
+ * it, resolves the base URL, and converts every schema and operation into the
2152
+ * universal AST that every downstream plugin consumes.
1878
2153
  *
1879
- * Parses the spec, optionally validates it, resolves the base URL, and converts
1880
- * everything into an `InputNode` that downstream plugins consume.
2154
+ * Configure once on `defineConfig`. The adapter's choices (date representation,
2155
+ * integer width, server URL) apply to every plugin in the build.
1881
2156
  *
1882
2157
  * @example
1883
2158
  * ```ts
@@ -1886,96 +2161,163 @@ const adapterOasName = "oas";
1886
2161
  * import { pluginTs } from '@kubb/plugin-ts'
1887
2162
  *
1888
2163
  * export default defineConfig({
1889
- * adapter: adapterOas({ dateType: 'date', serverIndex: 0 }),
1890
- * input: { path: './openapi.yaml' },
2164
+ * input: './petStore.yaml',
2165
+ * output: { path: './src/gen' },
2166
+ * adapter: adapterOas({
2167
+ * server: { index: 0 },
2168
+ * discriminator: 'propagate',
2169
+ * dateType: 'date',
2170
+ * }),
1891
2171
  * plugins: [pluginTs()],
1892
2172
  * })
1893
2173
  * ```
1894
2174
  */
1895
2175
  const adapterOas = createAdapter((options) => {
1896
- const { validate = true, contentType, serverIndex, serverVariables, discriminator = "strict", dateType = DEFAULT_PARSER_OPTIONS.dateType, integerType = DEFAULT_PARSER_OPTIONS.integerType, unknownType = DEFAULT_PARSER_OPTIONS.unknownType, enumSuffix = DEFAULT_PARSER_OPTIONS.enumSuffix, emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType } = options;
1897
- let nameMapping = /* @__PURE__ */ new Map();
1898
- let parsedDocument;
1899
- let inputNode;
2176
+ const { validate = true, contentType, server, discriminator = "preserve", enums = "inline", 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;
2177
+ const parserOptions = {
2178
+ ...DEFAULT_PARSER_OPTIONS,
2179
+ dateType,
2180
+ integerType,
2181
+ unknownType,
2182
+ emptySchemaType,
2183
+ enumSuffix
2184
+ };
2185
+ let parsedDocument = null;
2186
+ const documentCache = /* @__PURE__ */ new WeakMap();
2187
+ const schemasCache = /* @__PURE__ */ new WeakMap();
2188
+ const schemaParserCache = /* @__PURE__ */ new WeakMap();
2189
+ function ensureDocument(source) {
2190
+ const cached = documentCache.get(source);
2191
+ if (cached) return cached;
2192
+ const promise = (async () => {
2193
+ const fresh = await parseFromConfig(source);
2194
+ if (validate) await validateDocument(fresh);
2195
+ parsedDocument = fresh;
2196
+ return fresh;
2197
+ })();
2198
+ documentCache.set(source, promise);
2199
+ return promise;
2200
+ }
2201
+ function ensureSchemas(document) {
2202
+ const cached = schemasCache.get(document);
2203
+ if (cached) return cached;
2204
+ const result = getSchemas(document, { contentType });
2205
+ schemasCache.set(document, result);
2206
+ return result;
2207
+ }
2208
+ function ensureSchemaParser({ document, renames }) {
2209
+ const cached = schemaParserCache.get(document);
2210
+ if (cached) return cached;
2211
+ const parser = createSchemaParser({
2212
+ document,
2213
+ contentType,
2214
+ renames
2215
+ });
2216
+ schemaParserCache.set(document, parser);
2217
+ return parser;
2218
+ }
2219
+ function parseInput({ document, schemas, parser }) {
2220
+ const { parseSchema, parseOperation } = parser;
2221
+ const parsedByName = /* @__PURE__ */ new Map();
2222
+ const refAliasMap = /* @__PURE__ */ new Map();
2223
+ const enumNames = [];
2224
+ const discriminatorParentNodes = [];
2225
+ for (const [name, schema] of Object.entries(schemas)) {
2226
+ const node = parseSchema({
2227
+ schema,
2228
+ name
2229
+ }, parserOptions);
2230
+ parsedByName.set(name, node);
2231
+ reportSchemaDiagnostics({
2232
+ node,
2233
+ name
2234
+ });
2235
+ if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2236
+ if (narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
2237
+ if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2238
+ }
2239
+ const circularNames = [...findCircularSchemas([...parsedByName.values()])];
2240
+ const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
2241
+ const operationNodes = [];
2242
+ for (const operation of getOperations(document)) {
2243
+ const operationNode = parseOperation(parserOptions, operation);
2244
+ if (operationNode) operationNodes.push(operationNode);
2245
+ }
2246
+ let promotedEnums = null;
2247
+ if (enums === "root") {
2248
+ promotedEnums = collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas)));
2249
+ for (const name of promotedEnums.keys()) enumNames.push(name);
2250
+ }
2251
+ const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
2252
+ for (const name of Object.keys(schemas)) {
2253
+ const alias = refAliasMap.get(name);
2254
+ let node;
2255
+ if (alias?.name && parsedByName.has(alias.name)) node = {
2256
+ ...parsedByName.get(alias.name),
2257
+ name
2258
+ };
2259
+ else {
2260
+ const parsed = parsedByName.get(name);
2261
+ const child = discriminatorChildMap?.get(name);
2262
+ node = child ? patchDiscriminatorNode(parsed, child) : parsed;
2263
+ }
2264
+ schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
2265
+ }
2266
+ const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;
2267
+ return ast.factory.createInput({
2268
+ schemas: schemaNodes,
2269
+ operations,
2270
+ meta: {
2271
+ title: document.info?.title,
2272
+ description: document.info?.description,
2273
+ version: document.info?.version,
2274
+ baseURL: resolveBaseUrl({
2275
+ document,
2276
+ server
2277
+ }),
2278
+ circularNames,
2279
+ enumNames
2280
+ }
2281
+ });
2282
+ }
1900
2283
  return {
1901
2284
  name: "oas",
1902
2285
  get options() {
1903
2286
  return {
1904
2287
  validate,
1905
2288
  contentType,
1906
- serverIndex,
1907
- serverVariables,
2289
+ server,
1908
2290
  discriminator,
2291
+ enums,
1909
2292
  dateType,
1910
2293
  integerType,
1911
2294
  unknownType,
1912
2295
  emptySchemaType,
1913
- enumSuffix,
1914
- nameMapping
2296
+ enumSuffix
1915
2297
  };
1916
2298
  },
1917
2299
  get document() {
1918
2300
  return parsedDocument;
1919
2301
  },
1920
- get inputNode() {
1921
- return inputNode;
1922
- },
1923
- getImports(node, resolve) {
1924
- return ast.collectImports({
1925
- node,
1926
- nameMapping,
1927
- resolve: (schemaName) => {
1928
- const result = resolve(schemaName);
1929
- if (!result) return;
1930
- return ast.createImport({
1931
- name: [result.name],
1932
- path: result.path
1933
- });
1934
- }
1935
- });
2302
+ async validate(input, options) {
2303
+ await assertInputExists(input);
2304
+ await validateDocument(await parseDocument(input), options);
1936
2305
  },
1937
2306
  async parse(source) {
1938
- const document = await parseFromConfig(source);
1939
- if (validate) await validateDocument(document);
1940
- const server = serverIndex !== void 0 ? document.servers?.at(serverIndex) : void 0;
1941
- const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : void 0;
1942
- const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
1943
- contentType,
1944
- dateType,
1945
- integerType,
1946
- unknownType,
1947
- emptySchemaType,
1948
- enumSuffix
1949
- });
1950
- const node = discriminator === "inherit" ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot;
1951
- nameMapping = parsedNameMapping;
1952
- parsedDocument = document;
1953
- inputNode = ast.createInput({
1954
- ...node,
1955
- meta: {
1956
- title: document.info?.title,
1957
- description: document.info?.description,
1958
- version: document.info?.version,
1959
- baseURL
1960
- }
2307
+ const document = await ensureDocument(source);
2308
+ const { schemas, renames } = ensureSchemas(document);
2309
+ return parseInput({
2310
+ document,
2311
+ schemas,
2312
+ parser: ensureSchemaParser({
2313
+ document,
2314
+ renames
2315
+ })
1961
2316
  });
1962
- return inputNode;
1963
2317
  }
1964
2318
  };
1965
2319
  });
1966
2320
  //#endregion
1967
- //#region src/types.ts
1968
- /**
1969
- * Maps uppercase HTTP method names to lowercase for backwards compatibility.
1970
- *
1971
- * @example
1972
- * ```ts
1973
- * HttpMethods['GET'] // 'get'
1974
- * HttpMethods['POST'] // 'post'
1975
- * ```
1976
- */
1977
- const HttpMethods = Object.fromEntries(Object.entries(ast.httpMethods).map(([lower, upper]) => [upper, lower]));
1978
- //#endregion
1979
- export { HttpMethods, adapterOas, adapterOasName, mergeDocuments, parseDocument, parseFromConfig, validateDocument };
2321
+ export { adapterOas, adapterOasName };
1980
2322
 
1981
2323
  //# sourceMappingURL=index.js.map