@kubb/adapter-oas 5.0.0-beta.11 → 5.0.0-beta.110

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