@kubb/plugin-zod 5.0.0-alpha.3 → 5.0.0-alpha.30

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.
Files changed (46) hide show
  1. package/dist/index.cjs +1619 -100
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.ts +418 -4
  4. package/dist/index.js +1614 -100
  5. package/dist/index.js.map +1 -1
  6. package/package.json +6 -34
  7. package/src/components/Operations.tsx +22 -15
  8. package/src/components/Zod.tsx +20 -119
  9. package/src/constants.ts +5 -0
  10. package/src/generators/zodGenerator.tsx +129 -159
  11. package/src/generators/zodGeneratorLegacy.tsx +365 -0
  12. package/src/index.ts +12 -1
  13. package/src/plugin.ts +102 -148
  14. package/src/presets.ts +30 -0
  15. package/src/printers/printerZod.ts +298 -0
  16. package/src/printers/printerZodMini.ts +273 -0
  17. package/src/resolvers/resolverZod.ts +61 -0
  18. package/src/resolvers/resolverZodLegacy.ts +60 -0
  19. package/src/types.ts +172 -93
  20. package/src/utils.ts +248 -0
  21. package/dist/components-B7zUFnAm.cjs +0 -890
  22. package/dist/components-B7zUFnAm.cjs.map +0 -1
  23. package/dist/components-eECfXVou.js +0 -842
  24. package/dist/components-eECfXVou.js.map +0 -1
  25. package/dist/components.cjs +0 -4
  26. package/dist/components.d.ts +0 -56
  27. package/dist/components.js +0 -2
  28. package/dist/generators-CRKtFRi1.js +0 -290
  29. package/dist/generators-CRKtFRi1.js.map +0 -1
  30. package/dist/generators-CzSLRVqQ.cjs +0 -301
  31. package/dist/generators-CzSLRVqQ.cjs.map +0 -1
  32. package/dist/generators.cjs +0 -4
  33. package/dist/generators.d.ts +0 -503
  34. package/dist/generators.js +0 -2
  35. package/dist/templates/ToZod.source.cjs +0 -7
  36. package/dist/templates/ToZod.source.cjs.map +0 -1
  37. package/dist/templates/ToZod.source.d.ts +0 -7
  38. package/dist/templates/ToZod.source.js +0 -6
  39. package/dist/templates/ToZod.source.js.map +0 -1
  40. package/dist/types-D0wsPC6Y.d.ts +0 -172
  41. package/src/components/index.ts +0 -2
  42. package/src/generators/index.ts +0 -2
  43. package/src/generators/operationsGenerator.tsx +0 -50
  44. package/src/parser.ts +0 -909
  45. package/src/templates/ToZod.source.ts +0 -4
  46. package/templates/ToZod.ts +0 -61
@@ -0,0 +1,60 @@
1
+ import { defineResolver } from '@kubb/core'
2
+ import type { PluginZod } from '../types.ts'
3
+ import { resolverZod } from './resolverZod.ts'
4
+
5
+ /**
6
+ * Legacy resolver for `@kubb/plugin-zod` that reproduces the naming conventions
7
+ * used in Kubb v4. Enable via `compatibilityPreset: 'kubbV4'`
8
+ * (or by composing this resolver manually).
9
+ *
10
+ * Key differences from the default resolver:
11
+ * - Response status types: `<operationId><StatusCode>Schema` (e.g. `createPets201Schema`) instead of `<operationId>Status201Schema`
12
+ * - Default/error responses: `<operationId>ErrorSchema` instead of `<operationId>StatusDefaultSchema`
13
+ * - Request body: `<operationId>MutationRequestSchema` (non-GET) / `<operationId>QueryRequestSchema` (GET)
14
+ * - Combined responses type: `<operationId>MutationSchema` / `<operationId>QuerySchema`
15
+ * - Response union: `<operationId>MutationResponseSchema` / `<operationId>QueryResponseSchema`
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { resolverZodLegacy } from '@kubb/plugin-zod'
20
+ *
21
+ * resolverZodLegacy.resolveResponseStatusName(node, 201) // → 'createPets201Schema'
22
+ * resolverZodLegacy.resolveResponseStatusName(node, 'default') // → 'createPetsErrorSchema'
23
+ * resolverZodLegacy.resolveDataName(node) // → 'createPetsMutationRequestSchema' (POST)
24
+ * resolverZodLegacy.resolveResponsesName(node) // → 'createPetsMutationSchema' (POST)
25
+ * resolverZodLegacy.resolveResponseName(node) // → 'createPetsMutationResponseSchema' (POST)
26
+ * ```
27
+ */
28
+ export const resolverZodLegacy = defineResolver<PluginZod>(() => {
29
+ return {
30
+ ...resolverZod,
31
+ pluginName: 'plugin-zod',
32
+ resolveResponseStatusName(node, statusCode) {
33
+ if (statusCode === 'default') {
34
+ return this.resolveSchemaName(`${node.operationId} Error`)
35
+ }
36
+ return this.resolveSchemaName(`${node.operationId} ${statusCode}`)
37
+ },
38
+ resolveDataName(node) {
39
+ const suffix = node.method === 'GET' ? 'QueryRequest' : 'MutationRequest'
40
+ return this.resolveSchemaName(`${node.operationId} ${suffix}`)
41
+ },
42
+ resolveResponsesName(node) {
43
+ const suffix = node.method === 'GET' ? 'Query' : 'Mutation'
44
+ return this.resolveSchemaName(`${node.operationId} ${suffix}`)
45
+ },
46
+ resolveResponseName(node) {
47
+ const suffix = node.method === 'GET' ? 'QueryResponse' : 'MutationResponse'
48
+ return this.resolveSchemaName(`${node.operationId} ${suffix}`)
49
+ },
50
+ resolvePathParamsName(node, _param) {
51
+ return this.resolveSchemaName(`${node.operationId} PathParams`)
52
+ },
53
+ resolveQueryParamsName(node, _param) {
54
+ return this.resolveSchemaName(`${node.operationId} QueryParams`)
55
+ },
56
+ resolveHeaderParamsName(node, _param) {
57
+ return this.resolveSchemaName(`${node.operationId} HeaderParams`)
58
+ },
59
+ }
60
+ })
package/src/types.ts CHANGED
@@ -1,22 +1,108 @@
1
- import type { Group, Output, PluginFactoryOptions, ResolveNameParams } from '@kubb/core'
2
- import type { contentType, Oas, SchemaObject } from '@kubb/oas'
3
- import type { Exclude, Include, Override, ResolvePathOptions, Schema } from '@kubb/plugin-oas'
4
- import type { Generator } from '@kubb/plugin-oas/generators'
1
+ import type { OperationParamsResolver } from '@kubb/ast'
2
+ import type { OperationNode, ParameterNode, SchemaNode, StatusCode, Visitor } from '@kubb/ast/types'
3
+ import type {
4
+ CompatibilityPreset,
5
+ Exclude,
6
+ Generator,
7
+ Group,
8
+ Include,
9
+ Output,
10
+ Override,
11
+ PluginFactoryOptions,
12
+ ResolvePathOptions,
13
+ Resolver,
14
+ UserGroup,
15
+ } from '@kubb/core'
16
+ import type { PrinterZodNodes } from './printers/printerZod.ts'
17
+ import type { PrinterZodMiniNodes } from './printers/printerZodMini.ts'
18
+
19
+ /**
20
+ * The concrete resolver type for `@kubb/plugin-zod`.
21
+ * Extends the base `Resolver` with zod-specific naming helpers.
22
+ */
23
+ export type ResolverZod = Resolver &
24
+ OperationParamsResolver & {
25
+ /**
26
+ * Resolves a camelCase schema function name with a `Schema` suffix.
27
+ */
28
+ resolveSchemaName(this: ResolverZod, name: string): string
29
+ /**
30
+ * Resolves the name for a `z.infer<typeof ...>` type export.
31
+ * Strips the trailing `Schema` suffix (added by `resolveSchemaName`) before PascalCasing.
32
+ *
33
+ * @example
34
+ * resolver.resolveSchemaTypeName('pet) // → 'PetSchema'
35
+ * resolver.resolveSchemaTypeName('addPet200') // → 'AddPet200Schema'
36
+ * resolver.resolveSchemaTypeName('PetName') // → 'PetNameSchema'
37
+ */
38
+ resolveSchemaTypeName(this: ResolverZod, name: string): string
39
+ /**
40
+ * Resolves the name for a `z.infer<typeof ...>` type export.
41
+ * Strips the trailing `Schema` suffix (added by `resolveSchemaName`) before PascalCasing.
42
+ *
43
+ * @example
44
+ * resolver.resolveTypeName('pet') // → 'Pet'
45
+ * resolver.resolveTypeName('addPet200') // → 'AddPet200'
46
+ * resolver.resolveTypeName('PetName') // → 'PetName'
47
+ */
48
+ resolveTypeName(this: ResolverZod, name: string): string
49
+ /**
50
+ * Resolves a PascalCase path/file name for the generated output.
51
+ */
52
+ resolvePathName(this: ResolverZod, name: string, type?: 'file' | 'function' | 'type' | 'const'): string
53
+ /**
54
+ * Resolves the name for an operation response by status code.
55
+ *
56
+ * @example
57
+ * resolver.resolveResponseStatusName(node, 200) // → 'listPetsStatus200Schema'
58
+ */
59
+ resolveResponseStatusName(this: ResolverZod, node: OperationNode, statusCode: StatusCode): string
60
+ /**
61
+ * Resolves the name for the collection of all operation responses.
62
+ *
63
+ * @example
64
+ * resolver.resolveResponsesName(node) // → 'listPetsResponsesSchema'
65
+ */
66
+ resolveResponsesName(this: ResolverZod, node: OperationNode): string
67
+ /**
68
+ * Resolves the name for the union of all operation responses.
69
+ *
70
+ * @example
71
+ * resolver.resolveResponseName(node) // → 'listPetsResponseSchema'
72
+ */
73
+ resolveResponseName(this: ResolverZod, node: OperationNode): string
74
+ /**
75
+ * Resolves the name for an operation's grouped path parameters type.
76
+ *
77
+ * @example
78
+ * resolver.resolvePathParamsName(node, param) // → 'deletePetPathPetIdSchema'
79
+ */
80
+ resolvePathParamsName(this: ResolverZod, node: OperationNode, param: ParameterNode): string
81
+ /**
82
+ * Resolves the name for an operation's grouped query parameters type.
83
+ *
84
+ * @example
85
+ * resolver.resolveQueryParamsName(node, param) // → 'findPetsByStatusQueryStatusSchema'
86
+ */
87
+ resolveQueryParamsName(this: ResolverZod, node: OperationNode, param: ParameterNode): string
88
+ /**
89
+ * Resolves the name for an operation's grouped header parameters type.
90
+ *
91
+ * @example
92
+ * resolver.resolveHeaderParamsName(node, param) // → 'deletePetHeaderApiKeySchema'
93
+ */
94
+ resolveHeaderParamsName(this: ResolverZod, node: OperationNode, param: ParameterNode): string
95
+ }
5
96
 
6
97
  export type Options = {
7
98
  /**
8
99
  * @default 'zod'
9
100
  */
10
- output?: Output<Oas>
11
- /**
12
- * Define which contentType should be used.
13
- * By default, the first JSON valid mediaType is used
14
- */
15
- contentType?: contentType
101
+ output?: Output
16
102
  /**
17
103
  * Group the Zod schemas based on the provided name.
18
104
  */
19
- group?: Group
105
+ group?: UserGroup
20
106
  /**
21
107
  * Array containing exclude parameters to exclude/skip tags/operations/methods/paths.
22
108
  */
@@ -36,8 +122,7 @@ export type Options = {
36
122
  * Path is used as-is; relative paths are based on the generated file location.
37
123
  * @default 'zod'
38
124
  */
39
- importPath?: string
40
-
125
+ importPath?: 'zod' | 'zod/mini' | (string & {})
41
126
  /**
42
127
  * Choose to use date or datetime as JavaScript Date instead of string.
43
128
  * - false falls back to a simple z.string() format.
@@ -46,33 +131,8 @@ export type Options = {
46
131
  * - 'stringLocal' uses z.string().datetime({ local: true }) for local datetime validation.
47
132
  * - 'date' uses z.date() for JavaScript Date objects.
48
133
  * @default 'string'
49
- * @note 'stringOffset' will become the default in Kubb v3.
50
134
  */
51
135
  dateType?: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
52
- /**
53
- * Choose to use `number` or `bigint` for integer fields with `int64` format.
54
- * - 'number' uses the JavaScript `number` type (matches JSON.parse() runtime behavior).
55
- * - 'bigint' uses the JavaScript `bigint` type (accurate for values exceeding Number.MAX_SAFE_INTEGER).
56
- * @note in v5 of Kubb 'bigint' will become the default to better align with OpenAPI's int64 specification.
57
- * @default 'number'
58
- */
59
- integerType?: 'number' | 'bigint'
60
- /**
61
- * Which type to use when the Swagger/OpenAPI file is not providing more information.
62
- * - 'any' allows any value.
63
- * - 'unknown' requires type narrowing before use.
64
- * - 'void' represents no value.
65
- * @default 'any'
66
- */
67
- unknownType?: 'any' | 'unknown' | 'void'
68
- /**
69
- * Which type to use for empty schema values.
70
- * - 'any' allows any value.
71
- * - 'unknown' requires type narrowing before use.
72
- * - 'void' represents no value.
73
- * @default `unknownType`
74
- */
75
- emptySchemaType?: 'any' | 'unknown' | 'void'
76
136
  /**
77
137
  * Use TypeScript(`@kubb/plugin-ts`) to add type annotation.
78
138
  */
@@ -82,91 +142,110 @@ export type Options = {
82
142
  */
83
143
  inferred?: boolean
84
144
  /**
85
- * Use of z.coerce.string() instead of z.string()
86
- * can also be an object to enable coercion for dates, strings, and numbers
145
+ * Use of z.coerce.string() instead of z.string().
146
+ * Can also be an object to enable coercion for dates, strings, and numbers.
87
147
  */
88
- coercion?:
89
- | boolean
90
- | {
91
- dates?: boolean
92
- strings?: boolean
93
- numbers?: boolean
94
- }
95
- operations?: boolean
96
- mapper?: Record<string, string>
97
- transformers?: {
98
- /**
99
- * Customize the names based on the type that is provided by the plugin.
100
- */
101
- name?: (name: ResolveNameParams['name'], type?: ResolveNameParams['type']) => string
102
- /**
103
- * Receive schema and baseName(propertyName) and return FakerMeta array
104
- * TODO TODO add docs
105
- * @beta
106
- */
107
- schema?: (
108
- props: {
109
- schema: SchemaObject | null
110
- name: string | null
111
- parentName: string | null
112
- },
113
- defaultSchemas: Schema[],
114
- ) => Schema[] | undefined
115
- }
148
+ coercion?: boolean | { dates?: boolean; strings?: boolean; numbers?: boolean }
116
149
  /**
117
- * Which version of Zod should be used.
118
- * - '3' uses Zod v3.x syntax and features.
119
- * - '4' uses Zod v4.x syntax and features.
120
- * @default '3'
150
+ * Generate operation-level schemas (grouped by operationId).
121
151
  */
122
- version?: '3' | '4'
152
+ operations?: boolean
123
153
  /**
124
154
  * Which Zod GUID validator to use for OpenAPI `format: uuid`.
125
155
  * - 'uuid' uses UUID validation.
126
- * - 'guid' uses GUID validation (Zod v4 only).
156
+ * - 'guid' uses GUID validation.
127
157
  * @default 'uuid'
128
158
  */
129
159
  guidType?: 'uuid' | 'guid'
130
160
  /**
131
161
  * Use Zod Mini's functional API for better tree-shaking support.
132
- * When enabled, generates functional syntax (e.g., `z.optional(z.string())`) instead of chainable methods (e.g., `z.string().optional()`).
133
- * Requires Zod v4 or later. When `mini: true`, `version` is set to '4' and `importPath` will default to 'zod/mini'.
162
+ * When enabled, generates functional syntax (e.g., `z.optional(z.string())`)
163
+ * instead of chainable methods (e.g., `z.string().optional()`).
164
+ * When `mini: true`, `importPath` will default to 'zod/mini'.
134
165
  * @default false
135
166
  */
136
167
  mini?: boolean
137
168
  /**
138
- * Callback function to wrap the output of the generated zod schema
169
+ * Callback function to wrap the output of the generated zod schema.
139
170
  *
140
- * This is useful for edge case scenarios where you might leverage something like `z.object({ ... }).openapi({ example: { some: "complex-example" }})`
141
- * or `extendApi(z.object({ ... }), { example: { some: "complex-example", ...otherOpenApiProperties }})`
142
- * while going from openapi -> zod -> openapi
171
+ * Useful for edge cases like adding `.openapi()` metadata or wrapping
172
+ * schemas with extension helpers (openapi -> zod -> openapi round-trips).
143
173
  */
144
- wrapOutput?: (arg: { output: string; schema: SchemaObject }) => string | undefined
174
+ wrapOutput?: (arg: { output: string; schema: SchemaNode }) => string | undefined
145
175
  /**
146
- * Define some generators next to the zod generators
176
+ * How to style your params, by default no casing is applied
177
+ * - 'camelcase' uses camelCase for pathParams, queryParams and headerParams property names
178
+ * @default undefined
179
+ */
180
+ paramsCasing?: 'camelcase'
181
+ /**
182
+ * Define additional generators next to the zod generators.
147
183
  */
148
184
  generators?: Array<Generator<PluginZod>>
185
+ /**
186
+ * Compatibility preset to ease migration from previous Kubb versions.
187
+ */
188
+ compatibilityPreset?: CompatibilityPreset
189
+ /**
190
+ * A single resolver whose methods override the default resolver's naming conventions.
191
+ * When a method returns `null` or `undefined`, the default resolver's result is used instead.
192
+ */
193
+ resolver?: Partial<ResolverZod> & ThisType<ResolverZod>
194
+ /**
195
+ * Override individual printer node handlers to customize rendering of specific schema types.
196
+ *
197
+ * Each key is a `SchemaType` (e.g. `'date'`, `'string'`). The function replaces the
198
+ * built-in handler for that type. Use `this.transform` to recurse into nested schema nodes.
199
+ * When `mini: true`, the overrides apply to the Zod Mini printer.
200
+ *
201
+ * @example Override the `date` node to use `z.string().date()`
202
+ * ```ts
203
+ * pluginZod({
204
+ * printer: {
205
+ * nodes: {
206
+ * date(node) {
207
+ * return 'z.string().date()'
208
+ * },
209
+ * },
210
+ * },
211
+ * })
212
+ * ```
213
+ */
214
+ printer?: {
215
+ nodes?: PrinterZodNodes | PrinterZodMiniNodes
216
+ }
217
+ /**
218
+ * A single AST visitor applied to each SchemaNode/OperationNode before printing.
219
+ * When a visitor method returns `null` or `undefined`, the preset transformer's result is used instead.
220
+ */
221
+ transformer?: Visitor
149
222
  }
150
223
 
151
224
  type ResolvedOptions = {
152
- output: Output<Oas>
153
- group: Options['group']
154
- override: NonNullable<Options['override']>
155
- transformers: NonNullable<Options['transformers']>
225
+ output: Output
226
+ exclude: Array<Exclude>
227
+ include: Array<Include> | undefined
228
+ override: Array<Override<ResolvedOptions>>
229
+ group: Group | undefined
156
230
  dateType: NonNullable<Options['dateType']>
157
- integerType: NonNullable<Options['integerType']>
158
- unknownType: NonNullable<Options['unknownType']>
159
- emptySchemaType: NonNullable<Options['emptySchemaType']>
160
231
  typed: NonNullable<Options['typed']>
161
232
  inferred: NonNullable<Options['inferred']>
162
- mapper: NonNullable<Options['mapper']>
163
233
  importPath: NonNullable<Options['importPath']>
164
234
  coercion: NonNullable<Options['coercion']>
165
235
  operations: NonNullable<Options['operations']>
166
- wrapOutput: Options['wrapOutput']
167
- version: NonNullable<Options['version']>
168
236
  guidType: NonNullable<Options['guidType']>
169
237
  mini: NonNullable<Options['mini']>
238
+ wrapOutput: Options['wrapOutput']
239
+ paramsCasing: Options['paramsCasing']
240
+ printer: Options['printer']
170
241
  }
171
242
 
172
- export type PluginZod = PluginFactoryOptions<'plugin-zod', Options, ResolvedOptions, never, ResolvePathOptions>
243
+ export type PluginZod = PluginFactoryOptions<'plugin-zod', Options, ResolvedOptions, never, ResolvePathOptions, ResolverZod>
244
+
245
+ declare global {
246
+ namespace Kubb {
247
+ interface PluginRegistry {
248
+ 'plugin-zod': PluginZod
249
+ }
250
+ }
251
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,248 @@
1
+ import { stringify, toRegExpString } from '@internals/utils'
2
+ import { createProperty, createSchema, extractRefName } from '@kubb/ast'
3
+ import type { OperationNode, ParameterNode, SchemaNode } from '@kubb/ast/types'
4
+ import type { PluginZod, ResolverZod } from './types.ts'
5
+
6
+ /**
7
+ * Returns `true` when the given coercion option enables coercion for the specified type.
8
+ */
9
+ export function shouldCoerce(coercion: PluginZod['resolvedOptions']['coercion'] | undefined, type: 'dates' | 'strings' | 'numbers'): boolean {
10
+ if (coercion === undefined || coercion === false) return false
11
+ if (coercion === true) return true
12
+
13
+ return !!coercion[type]
14
+ }
15
+
16
+ /**
17
+ * Collects all resolved schema names for an operation's parameters and responses
18
+ * into a single lookup object, useful for building imports and type references.
19
+ */
20
+ export function buildSchemaNames(node: OperationNode, { params, resolver }: { params: Array<ParameterNode>; resolver: ResolverZod }) {
21
+ const pathParam = params.find((p) => p.in === 'path')
22
+ const queryParam = params.find((p) => p.in === 'query')
23
+ const headerParam = params.find((p) => p.in === 'header')
24
+
25
+ const responses: Record<number | string, string> = {}
26
+ const errors: Record<number | string, string> = {}
27
+
28
+ for (const res of node.responses) {
29
+ const name = resolver.resolveResponseStatusName(node, res.statusCode)
30
+ const statusNum = Number(res.statusCode)
31
+
32
+ if (!Number.isNaN(statusNum)) {
33
+ responses[statusNum] = name
34
+ if (statusNum >= 400) {
35
+ errors[statusNum] = name
36
+ }
37
+ }
38
+ }
39
+
40
+ responses['default'] = resolver.resolveResponseName(node)
41
+
42
+ return {
43
+ request: node.requestBody?.schema ? resolver.resolveDataName(node) : undefined,
44
+ parameters: {
45
+ path: pathParam ? resolver.resolvePathParamsName(node, pathParam) : undefined,
46
+ query: queryParam ? resolver.resolveQueryParamsName(node, queryParam) : undefined,
47
+ header: headerParam ? resolver.resolveHeaderParamsName(node, headerParam) : undefined,
48
+ },
49
+ responses,
50
+ errors,
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Format a default value as a code-level literal.
56
+ * Objects become `{}`, primitives become their string representation, strings are quoted.
57
+ */
58
+ export function formatDefault(value: unknown): string {
59
+ if (typeof value === 'string') return stringify(value)
60
+ if (typeof value === 'object' && value !== null) return '{}'
61
+
62
+ return String(value ?? '')
63
+ }
64
+
65
+ /**
66
+ * Format a primitive enum/literal value.
67
+ * Strings are quoted; numbers and booleans are emitted raw.
68
+ */
69
+ export function formatLiteral(v: string | number | boolean): string {
70
+ if (typeof v === 'string') return stringify(v)
71
+
72
+ return String(v)
73
+ }
74
+
75
+ export type NumericConstraints = {
76
+ min?: number
77
+ max?: number
78
+ exclusiveMinimum?: number
79
+ exclusiveMaximum?: number
80
+ multipleOf?: number
81
+ }
82
+
83
+ export type LengthConstraints = {
84
+ min?: number
85
+ max?: number
86
+ pattern?: string
87
+ }
88
+
89
+ export type ModifierOptions = {
90
+ value: string
91
+ nullable?: boolean
92
+ optional?: boolean
93
+ nullish?: boolean
94
+ defaultValue?: unknown
95
+ description?: string
96
+ }
97
+
98
+ /**
99
+ * Build `.min()` / `.max()` / `.gt()` / `.lt()` constraint chains for numbers
100
+ * using the standard chainable Zod v4 API.
101
+ */
102
+ export function numberConstraints({ min, max, exclusiveMinimum, exclusiveMaximum, multipleOf }: NumericConstraints): string {
103
+ return [
104
+ min !== undefined ? `.min(${min})` : '',
105
+ max !== undefined ? `.max(${max})` : '',
106
+ exclusiveMinimum !== undefined ? `.gt(${exclusiveMinimum})` : '',
107
+ exclusiveMaximum !== undefined ? `.lt(${exclusiveMaximum})` : '',
108
+ multipleOf !== undefined ? `.multipleOf(${multipleOf})` : '',
109
+ ].join('')
110
+ }
111
+
112
+ /**
113
+ * Build `.min()` / `.max()` / `.regex()` chains for strings/arrays
114
+ * using the standard chainable Zod v4 API.
115
+ */
116
+ export function lengthConstraints({ min, max, pattern }: LengthConstraints): string {
117
+ return [
118
+ min !== undefined ? `.min(${min})` : '',
119
+ max !== undefined ? `.max(${max})` : '',
120
+ pattern !== undefined ? `.regex(${toRegExpString(pattern, null)})` : '',
121
+ ].join('')
122
+ }
123
+
124
+ /**
125
+ * Build `.check(z.minimum(), z.maximum())` for `zod/mini` numeric constraints.
126
+ */
127
+ export function numberChecksMini({ min, max, exclusiveMinimum, exclusiveMaximum, multipleOf }: NumericConstraints): string {
128
+ const checks: string[] = []
129
+ if (min !== undefined) checks.push(`z.minimum(${min})`)
130
+ if (max !== undefined) checks.push(`z.maximum(${max})`)
131
+ if (exclusiveMinimum !== undefined) checks.push(`z.minimum(${exclusiveMinimum}, { exclusive: true })`)
132
+ if (exclusiveMaximum !== undefined) checks.push(`z.maximum(${exclusiveMaximum}, { exclusive: true })`)
133
+ if (multipleOf !== undefined) checks.push(`z.multipleOf(${multipleOf})`)
134
+ return checks.length ? `.check(${checks.join(', ')})` : ''
135
+ }
136
+
137
+ /**
138
+ * Build `.check(z.minLength(), z.maxLength(), z.regex())` for `zod/mini` length constraints.
139
+ */
140
+ export function lengthChecksMini({ min, max, pattern }: LengthConstraints): string {
141
+ const checks: string[] = []
142
+ if (min !== undefined) checks.push(`z.minLength(${min})`)
143
+ if (max !== undefined) checks.push(`z.maxLength(${max})`)
144
+ if (pattern !== undefined) checks.push(`z.regex(${toRegExpString(pattern, null)})`)
145
+ return checks.length ? `.check(${checks.join(', ')})` : ''
146
+ }
147
+
148
+ /**
149
+ * Apply nullable / optional / nullish modifiers and an optional `.describe()` call
150
+ * to a schema value string using the chainable Zod v4 API.
151
+ */
152
+ export function applyModifiers({ value, nullable, optional, nullish, defaultValue, description }: ModifierOptions): string {
153
+ let result = value
154
+ if (nullish || (nullable && optional)) {
155
+ result = `${result}.nullish()`
156
+ } else if (optional) {
157
+ result = `${result}.optional()`
158
+ } else if (nullable) {
159
+ result = `${result}.nullable()`
160
+ }
161
+ if (defaultValue !== undefined) {
162
+ result = `${result}.default(${formatDefault(defaultValue)})`
163
+ }
164
+ if (description) {
165
+ result = `${result}.describe(${stringify(description)})`
166
+ }
167
+ return result
168
+ }
169
+
170
+ /**
171
+ * Apply nullable / optional / nullish modifiers using the functional `zod/mini` API
172
+ * (`z.nullable()`, `z.optional()`, `z.nullish()`).
173
+ */
174
+ export function applyMiniModifiers({ value, nullable, optional, nullish, defaultValue }: Omit<ModifierOptions, 'description'>): string {
175
+ let result = value
176
+ if (nullish) {
177
+ result = `z.nullish(${result})`
178
+ } else {
179
+ if (nullable) {
180
+ result = `z.nullable(${result})`
181
+ }
182
+ if (optional) {
183
+ result = `z.optional(${result})`
184
+ }
185
+ }
186
+ if (defaultValue !== undefined) {
187
+ result = `z._default(${result}, ${formatDefault(defaultValue)})`
188
+ }
189
+ return result
190
+ }
191
+
192
+ /**
193
+ * Returns true when the schema tree contains a self-referential `$ref`
194
+ * whose resolved name matches `schemaName`.
195
+ *
196
+ * A `visited` set prevents infinite recursion on circular schema graphs.
197
+ */
198
+ export function containsSelfRef(
199
+ node: SchemaNode,
200
+ { schemaName, resolver, visited = new Set() }: { schemaName: string; resolver: ResolverZod | undefined; visited?: Set<SchemaNode> },
201
+ ): boolean {
202
+ if (visited.has(node)) return false
203
+ visited.add(node)
204
+
205
+ if (node.type === 'ref' && node.ref) {
206
+ const rawName = extractRefName(node.ref) ?? node.name
207
+ const resolved = rawName ? (resolver?.default(rawName, 'function') ?? rawName) : node.name
208
+ return resolved === schemaName
209
+ }
210
+ if (node.type === 'object') {
211
+ if (node.properties?.some((p) => containsSelfRef(p.schema, { schemaName, resolver, visited }))) return true
212
+ if (node.additionalProperties && node.additionalProperties !== true) {
213
+ return containsSelfRef(node.additionalProperties, { schemaName, resolver, visited })
214
+ }
215
+ return false
216
+ }
217
+ if (node.type === 'array' || node.type === 'tuple') {
218
+ return node.items?.some((item) => containsSelfRef(item, { schemaName, resolver, visited })) ?? false
219
+ }
220
+ if (node.type === 'union' || node.type === 'intersection') {
221
+ return node.members?.some((m) => containsSelfRef(m, { schemaName, resolver, visited })) ?? false
222
+ }
223
+ return false
224
+ }
225
+
226
+ type BuildGroupedParamsSchemaOptions = {
227
+ params: Array<ParameterNode>
228
+ optional?: boolean
229
+ }
230
+
231
+ /**
232
+ * Builds an `object` schema node grouping the given parameter nodes.
233
+ * The `primitive: 'object'` marker ensures the Zod printer emits `z.object(…)` rather than a record.
234
+ */
235
+ export function buildGroupedParamsSchema({ params, optional }: BuildGroupedParamsSchemaOptions): SchemaNode {
236
+ return createSchema({
237
+ type: 'object',
238
+ optional,
239
+ primitive: 'object',
240
+ properties: params.map((param) =>
241
+ createProperty({
242
+ name: param.name,
243
+ required: param.required,
244
+ schema: param.schema,
245
+ }),
246
+ ),
247
+ })
248
+ }