@kubb/plugin-zod 3.0.0-alpha.0

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.
@@ -0,0 +1,171 @@
1
+ import { Oas } from '@kubb/plugin-oas/components'
2
+ import { Const, File, Type, useApp, useFile } from '@kubb/react'
3
+ import { pluginTsName } from '@kubb/plugin-ts'
4
+
5
+ import transformers from '@kubb/core/transformers'
6
+ import { isKeyword, schemaKeywords } from '@kubb/plugin-oas'
7
+ import { useSchema } from '@kubb/plugin-oas/hooks'
8
+ import type { ReactNode } from 'react'
9
+ import * as parserZod from '../parser/index.ts'
10
+ import { pluginZodName } from '../plugin.ts'
11
+ import type { PluginZod } from '../types.ts'
12
+
13
+ type Props = {
14
+ description?: string
15
+ withTypeAnnotation?: boolean
16
+ keysToOmit?: string[]
17
+ }
18
+
19
+ export function Schema(props: Props): ReactNode {
20
+ const { keysToOmit, withTypeAnnotation, description } = props
21
+ const { tree, name } = useSchema()
22
+ const {
23
+ pluginManager,
24
+ plugin: {
25
+ options: { mapper, typedSchema, coercion },
26
+ },
27
+ } = useApp<PluginZod>()
28
+
29
+ // all checks are also inside this.schema(React)
30
+ const resolvedName = pluginManager.resolveName({
31
+ name,
32
+ pluginKey: [pluginZodName],
33
+ type: 'function',
34
+ })
35
+ const resolvedTypeName = pluginManager.resolveName({
36
+ name,
37
+ pluginKey: [pluginZodName],
38
+ type: 'type',
39
+ })
40
+
41
+ const typeName = pluginManager.resolveName({
42
+ name,
43
+ pluginKey: [pluginTsName],
44
+ type: 'type',
45
+ })
46
+
47
+ if (!tree.length) {
48
+ return (
49
+ <Const
50
+ name={resolvedName}
51
+ export
52
+ JSDoc={{
53
+ comments: [description ? `@description ${transformers.jsStringEscape(description)}` : undefined].filter(Boolean),
54
+ }}
55
+ >
56
+ undefined
57
+ </Const>
58
+ )
59
+ }
60
+
61
+ const hasTuple = tree.some((item) => isKeyword(item, schemaKeywords.tuple))
62
+
63
+ const output = parserZod
64
+ .sort(tree)
65
+ .filter((item) => {
66
+ if (hasTuple && (isKeyword(item, schemaKeywords.min) || isKeyword(item, schemaKeywords.max))) {
67
+ return false
68
+ }
69
+
70
+ return true
71
+ })
72
+ .map((item) => parserZod.parse(undefined, item, { name, typeName, description, mapper, coercion, keysToOmit }))
73
+ .filter(Boolean)
74
+ .join('')
75
+
76
+ const suffix = output.endsWith('.nullable()') ? '.unwrap().and' : '.and'
77
+
78
+ return (
79
+ <>
80
+ <Const
81
+ export
82
+ name={resolvedName}
83
+ JSDoc={{
84
+ comments: [description ? `@description ${transformers.jsStringEscape(description)}` : undefined].filter(Boolean),
85
+ }}
86
+ >
87
+ {[
88
+ output,
89
+ keysToOmit?.length ? `${suffix}(z.object({ ${keysToOmit.map((key) => `${key}: z.never()`).join(',')} }))` : undefined,
90
+ withTypeAnnotation && typeName ? ` as z.ZodType<${typeName}>` : '',
91
+ ]
92
+ .filter(Boolean)
93
+ .join('') || ''}
94
+ </Const>
95
+ {typedSchema && (
96
+ <Type export name={resolvedTypeName}>
97
+ {`z.infer<typeof ${resolvedName}>`}
98
+ </Type>
99
+ )}
100
+ </>
101
+ )
102
+ }
103
+
104
+ type FileProps = {}
105
+
106
+ Schema.File = function ({}: FileProps): ReactNode {
107
+ const {
108
+ pluginManager,
109
+ plugin: {
110
+ options: { typed },
111
+ },
112
+ } = useApp<PluginZod>()
113
+ const { tree, schema } = useSchema()
114
+
115
+ const withData = tree.some(
116
+ (schema) =>
117
+ schema.keyword === schemaKeywords.array ||
118
+ schema.keyword === schemaKeywords.and ||
119
+ schema.keyword === schemaKeywords.object ||
120
+ schema.keyword === schemaKeywords.union ||
121
+ schema.keyword === schemaKeywords.tuple,
122
+ )
123
+
124
+ const withTypeAnnotation = !!typed
125
+
126
+ return (
127
+ <Oas.Schema.File output={pluginManager.config.output.path}>
128
+ <Schema.Imports />
129
+ <File.Source>
130
+ <Schema withTypeAnnotation={withTypeAnnotation} description={schema?.description} />
131
+ </File.Source>
132
+ </Oas.Schema.File>
133
+ )
134
+ }
135
+ Schema.Imports = (): ReactNode => {
136
+ const {
137
+ pluginManager,
138
+ plugin: {
139
+ options: { typed, importPath },
140
+ },
141
+ } = useApp<PluginZod>()
142
+ const { path: root } = useFile()
143
+ const { name, tree, schema } = useSchema()
144
+
145
+ // used for this.options.typed
146
+ const typeName = pluginManager.resolveName({
147
+ name,
148
+ pluginKey: [pluginTsName],
149
+ type: 'type',
150
+ })
151
+
152
+ const typeFileName = pluginManager.resolveName({
153
+ name: name,
154
+ pluginKey: [pluginTsName],
155
+ type: 'file',
156
+ })
157
+
158
+ const typePath = pluginManager.resolvePath({
159
+ baseName: typeFileName,
160
+ pluginKey: [pluginTsName],
161
+ })
162
+
163
+ const withTypeAnnotation = !!typed
164
+
165
+ return (
166
+ <>
167
+ <File.Import name={['z']} path={importPath} />
168
+ {withTypeAnnotation && typeName && typePath && <File.Import isTypeOnly root={root} path={typePath} name={[typeName]} />}
169
+ </>
170
+ )
171
+ }
@@ -0,0 +1,50 @@
1
+ export const operations = {
2
+ 'get_pets-pet-id': {
3
+ request: undefined,
4
+ parameters: {
5
+ path: undefined,
6
+ query: undefined,
7
+ header: undefined,
8
+ },
9
+ responses: {
10
+ 200: GetPetsPetIdQueryResponse,
11
+ default: GetPetsPetIdQueryResponse,
12
+ },
13
+ errors: {},
14
+ },
15
+ listPets: {
16
+ request: undefined,
17
+ parameters: {
18
+ path: undefined,
19
+ query: ListPetsQueryParams,
20
+ header: undefined,
21
+ },
22
+ responses: {
23
+ 200: ListPetsQueryResponse,
24
+ default: ListPetsQueryResponse,
25
+ },
26
+ errors: {},
27
+ },
28
+ createPets: {
29
+ request: CreatePetsMutationRequest,
30
+ parameters: {
31
+ path: undefined,
32
+ query: undefined,
33
+ header: undefined,
34
+ },
35
+ responses: {
36
+ 201: CreatePetsMutationResponse,
37
+ default: CreatePetsMutationResponse,
38
+ },
39
+ errors: {},
40
+ },
41
+ } as const
42
+ export const paths = {
43
+ '/pets/{pet_id}': {
44
+ get: operations['get_pets-pet-id'],
45
+ },
46
+ '/pets': {
47
+ get: operations['listPets'],
48
+ post: operations['createPets'],
49
+ },
50
+ } as const
@@ -0,0 +1,3 @@
1
+ export { OperationSchema } from './OperationSchema.tsx'
2
+ export { Operations } from './Operations.tsx'
3
+ export { Schema } from './Schema.tsx'
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { pluginZod } from './plugin.ts'
2
+
3
+ export { pluginZod, pluginZodName } from './plugin.ts'
4
+ export type { PluginZod } from './types.ts'
5
+
6
+ /**
7
+ * @deprecated Use `import { pluginZod } from '@kubb/plugin-zod'` instead
8
+ */
9
+ const definePluginDefault = pluginZod
10
+ /**
11
+ * @deprecated Use `import { pluginZod } from '@kubb/plugin-zod'` instead
12
+ */
13
+ export const definePlugin = pluginZod
14
+
15
+ export default definePluginDefault
@@ -0,0 +1,345 @@
1
+ import transformers, { createJSDocBlockText } from '@kubb/core/transformers'
2
+ import { type SchemaKeywordMapper, isKeyword, schemaKeywords } from '@kubb/plugin-oas'
3
+
4
+ import type { Schema, SchemaKeywordBase, SchemaMapper } from '@kubb/plugin-oas'
5
+
6
+ export const zodKeywordMapper = {
7
+ any: () => 'z.any()',
8
+ unknown: () => 'z.unknown()',
9
+ number: (coercion?: boolean, min?: number, max?: number) => {
10
+ return [coercion ? 'z.coerce.number()' : 'z.number()', min !== undefined ? `.min(${min})` : undefined, max !== undefined ? `.max(${max})` : undefined]
11
+ .filter(Boolean)
12
+ .join('')
13
+ },
14
+ integer: (coercion?: boolean, min?: number, max?: number) => {
15
+ return [coercion ? 'z.coerce.number()' : 'z.number()', min !== undefined ? `.min(${min})` : undefined, max !== undefined ? `.max(${max})` : undefined]
16
+ .filter(Boolean)
17
+ .join('')
18
+ },
19
+ object: (value?: string) => `z.object({${value}})`,
20
+ string: (coercion?: boolean, min?: number, max?: number) => {
21
+ return [coercion ? 'z.coerce.string()' : 'z.string()', min !== undefined ? `.min(${min})` : undefined, max !== undefined ? `.max(${max})` : undefined]
22
+ .filter(Boolean)
23
+ .join('')
24
+ },
25
+ boolean: () => 'z.boolean()',
26
+ undefined: () => 'z.undefined()',
27
+ nullable: () => '.nullable()',
28
+ null: () => 'z.null()',
29
+ nullish: () => '.nullish()',
30
+ array: (items: string[] = [], min?: number, max?: number) => {
31
+ return [`z.array(${items?.join('')})`, min !== undefined ? `.min(${min})` : undefined, max !== undefined ? `.max(${max})` : undefined]
32
+ .filter(Boolean)
33
+ .join('')
34
+ },
35
+ tuple: (items: string[] = []) => `z.tuple([${items?.join(', ')}])`,
36
+ enum: (items: string[] = []) => `z.enum([${items?.join(', ')}])`,
37
+ union: (items: string[] = []) => `z.union([${items?.join(', ')}])`,
38
+ const: (value?: string | number) => `z.literal(${value ?? ''})`,
39
+ /**
40
+ * ISO 8601
41
+ */
42
+ datetime: (offset = false, local = false) => {
43
+ if (offset) {
44
+ return `z.string().datetime({ offset: ${offset} })`
45
+ }
46
+
47
+ if (local) {
48
+ return `z.string().datetime({ local: ${local} })`
49
+ }
50
+
51
+ return 'z.string().datetime()'
52
+ },
53
+ /**
54
+ * Type `'date'` Date
55
+ * Type `'string'` ISO date format (YYYY-MM-DD)
56
+ * @default ISO date format (YYYY-MM-DD)
57
+ */
58
+ date: (type: 'date' | 'string' = 'string', coercion?: boolean) => {
59
+ if (type === 'string') {
60
+ return 'z.string().date()'
61
+ }
62
+
63
+ if (coercion) {
64
+ return 'z.coerce.date()'
65
+ }
66
+
67
+ return 'z.date()'
68
+ },
69
+ /**
70
+ * Type `'date'` Date
71
+ * Type `'string'` ISO time format (HH:mm:ss[.SSSSSS])
72
+ * @default ISO time format (HH:mm:ss[.SSSSSS])
73
+ */
74
+ time: (type: 'date' | 'string' = 'string', coercion?: boolean) => {
75
+ if (type === 'string') {
76
+ return 'z.string().time()'
77
+ }
78
+
79
+ if (coercion) {
80
+ return 'z.coerce.date()'
81
+ }
82
+
83
+ return 'z.date()'
84
+ },
85
+ uuid: () => '.uuid()',
86
+ url: () => '.url()',
87
+ strict: () => '.strict()',
88
+ default: (value?: string | number | true) => `.default(${value ?? ''})`,
89
+ and: (items: string[] = []) => items?.map((item) => `.and(${item})`).join(''),
90
+ describe: (value = '') => `.describe(${value})`,
91
+ min: (value?: number) => `.min(${value ?? ''})`,
92
+ max: (value?: number) => `.max(${value ?? ''})`,
93
+ optional: () => '.optional()',
94
+ matches: (value = '') => `.regex(${value})`,
95
+ email: () => '.email()',
96
+ firstName: undefined,
97
+ lastName: undefined,
98
+ password: undefined,
99
+ phone: undefined,
100
+ readOnly: undefined,
101
+ ref: (value?: string) => (value ? `z.lazy(() => ${value})` : undefined),
102
+ blob: () => 'z.string()',
103
+ deprecated: undefined,
104
+ example: undefined,
105
+ schema: undefined,
106
+ catchall: (value?: string) => (value ? `.catchall(${value})` : undefined),
107
+ name: undefined,
108
+ } satisfies SchemaMapper<string | null | undefined>
109
+
110
+ /**
111
+ * @link based on https://github.com/cellular/oazapfts/blob/7ba226ebb15374e8483cc53e7532f1663179a22c/src/codegen/generate.ts#L398
112
+ */
113
+
114
+ export function sort(items?: Schema[]): Schema[] {
115
+ const order: string[] = [
116
+ schemaKeywords.string,
117
+ schemaKeywords.datetime,
118
+ schemaKeywords.date,
119
+ schemaKeywords.time,
120
+ schemaKeywords.tuple,
121
+ schemaKeywords.number,
122
+ schemaKeywords.object,
123
+ schemaKeywords.enum,
124
+ schemaKeywords.url,
125
+ schemaKeywords.email,
126
+ schemaKeywords.firstName,
127
+ schemaKeywords.lastName,
128
+ schemaKeywords.password,
129
+ schemaKeywords.matches,
130
+ schemaKeywords.uuid,
131
+ schemaKeywords.min,
132
+ schemaKeywords.max,
133
+ schemaKeywords.default,
134
+ schemaKeywords.describe,
135
+ schemaKeywords.optional,
136
+ schemaKeywords.nullable,
137
+ schemaKeywords.nullish,
138
+ schemaKeywords.null,
139
+ ]
140
+
141
+ if (!items) {
142
+ return []
143
+ }
144
+
145
+ return transformers.orderBy(items, [(v) => order.indexOf(v.keyword)], ['asc'])
146
+ }
147
+
148
+ type ParserOptions = {
149
+ name: string
150
+ typeName?: string
151
+ description?: string
152
+
153
+ keysToOmit?: string[]
154
+ mapper?: Record<string, string>
155
+ coercion?: boolean
156
+ }
157
+
158
+ export function parse(parent: Schema | undefined, current: Schema, options: ParserOptions): string | undefined {
159
+ const value = zodKeywordMapper[current.keyword as keyof typeof zodKeywordMapper]
160
+
161
+ if (!value) {
162
+ return undefined
163
+ }
164
+
165
+ if (isKeyword(current, schemaKeywords.union)) {
166
+ // zod union type needs at least 2 items
167
+ if (Array.isArray(current.args) && current.args.length === 1) {
168
+ return parse(parent, current.args[0] as Schema, options)
169
+ }
170
+ if (Array.isArray(current.args) && !current.args.length) {
171
+ return ''
172
+ }
173
+
174
+ return zodKeywordMapper.union(
175
+ sort(current.args)
176
+ .map((schema) => parse(current, schema, options))
177
+ .filter(Boolean),
178
+ )
179
+ }
180
+
181
+ if (isKeyword(current, schemaKeywords.and)) {
182
+ const items = sort(current.args)
183
+ .filter((schema: Schema) => {
184
+ return ![schemaKeywords.optional, schemaKeywords.describe].includes(schema.keyword as typeof schemaKeywords.describe)
185
+ })
186
+ .map((schema: Schema) => parse(current, schema, options))
187
+ .filter(Boolean)
188
+
189
+ return `${items.slice(0, 1)}${zodKeywordMapper.and(items.slice(1))}`
190
+ }
191
+
192
+ if (isKeyword(current, schemaKeywords.array)) {
193
+ return zodKeywordMapper.array(
194
+ sort(current.args.items)
195
+ .map((schemas) => parse(current, schemas, options))
196
+ .filter(Boolean),
197
+ current.args.min,
198
+ current.args.max,
199
+ )
200
+ }
201
+
202
+ if (isKeyword(current, schemaKeywords.enum)) {
203
+ if (current.args.asConst) {
204
+ return zodKeywordMapper.union(
205
+ current.args.items
206
+ .map((schema) => {
207
+ return parse(
208
+ current,
209
+ {
210
+ keyword: schemaKeywords.const,
211
+ args: schema,
212
+ },
213
+ options,
214
+ )
215
+ })
216
+ .filter(Boolean),
217
+ )
218
+ }
219
+
220
+ return zodKeywordMapper.enum(
221
+ current.args.items.map((schema) => {
222
+ if (schema.format === 'number') {
223
+ return transformers.stringify(schema.value)
224
+ }
225
+ return transformers.stringify(schema.value)
226
+ }),
227
+ )
228
+ }
229
+
230
+ if (isKeyword(current, schemaKeywords.ref)) {
231
+ return zodKeywordMapper.ref(current.args?.name)
232
+ }
233
+
234
+ if (isKeyword(current, schemaKeywords.object)) {
235
+ const properties = Object.entries(current.args?.properties || {})
236
+ .filter((item) => {
237
+ const schema = item[1]
238
+ return schema && typeof schema.map === 'function'
239
+ })
240
+ .map(([name, schemas]) => {
241
+ const nameSchema = schemas.find((schema) => schema.keyword === schemaKeywords.name) as SchemaKeywordMapper['name']
242
+ const mappedName = nameSchema?.args || name
243
+
244
+ // custom mapper(pluginOptions)
245
+ if (options.mapper?.[mappedName]) {
246
+ return `"${name}": ${options.mapper?.[mappedName]}`
247
+ }
248
+
249
+ return `"${name}": ${sort(schemas)
250
+ .map((schema, array) => {
251
+ return parse(current, schema, options)
252
+ })
253
+ .filter(Boolean)
254
+ .join('')}`
255
+ })
256
+ .join(',')
257
+
258
+ const additionalProperties = current.args?.additionalProperties?.length
259
+ ? current.args.additionalProperties
260
+ .map((schema) => parse(current, schema, options))
261
+ .filter(Boolean)
262
+ .at(0)
263
+ : undefined
264
+
265
+ const text = [
266
+ zodKeywordMapper.object(properties),
267
+ current.args?.strict ? zodKeywordMapper.strict() : undefined,
268
+ additionalProperties ? zodKeywordMapper.catchall(additionalProperties) : undefined,
269
+ ].filter(Boolean)
270
+
271
+ return text.join('')
272
+ }
273
+
274
+ if (isKeyword(current, schemaKeywords.tuple)) {
275
+ return zodKeywordMapper.tuple(
276
+ sort(current.args.items)
277
+ .map((schema) => parse(current, schema, options))
278
+ .filter(Boolean),
279
+ )
280
+ }
281
+
282
+ if (isKeyword(current, schemaKeywords.const)) {
283
+ if (current.args.format === 'number' && current.args.value !== undefined) {
284
+ return zodKeywordMapper.const(Number.parseInt(current.args.value?.toString()))
285
+ }
286
+ return zodKeywordMapper.const(transformers.stringify(current.args.value))
287
+ }
288
+
289
+ if (isKeyword(current, schemaKeywords.matches)) {
290
+ if (current.args) {
291
+ return zodKeywordMapper.matches(transformers.toRegExpString(current.args))
292
+ }
293
+ }
294
+
295
+ if (isKeyword(current, schemaKeywords.default)) {
296
+ if (current.args) {
297
+ return zodKeywordMapper.default(current.args)
298
+ }
299
+ }
300
+
301
+ if (isKeyword(current, schemaKeywords.describe)) {
302
+ if (current.args) {
303
+ return zodKeywordMapper.describe(transformers.stringify(current.args.toString()))
304
+ }
305
+ }
306
+
307
+ if (isKeyword(current, schemaKeywords.string)) {
308
+ return zodKeywordMapper.string(options.coercion)
309
+ }
310
+
311
+ if (isKeyword(current, schemaKeywords.number) || isKeyword(current, schemaKeywords.integer)) {
312
+ return zodKeywordMapper.number(options.coercion)
313
+ }
314
+
315
+ if (isKeyword(current, schemaKeywords.min)) {
316
+ return zodKeywordMapper.min(current.args)
317
+ }
318
+ if (isKeyword(current, schemaKeywords.max)) {
319
+ return zodKeywordMapper.max(current.args)
320
+ }
321
+
322
+ if (isKeyword(current, schemaKeywords.datetime)) {
323
+ return zodKeywordMapper.datetime(current.args.offset, current.args.local)
324
+ }
325
+
326
+ if (isKeyword(current, schemaKeywords.date)) {
327
+ return zodKeywordMapper.date(current.args.type, options.coercion)
328
+ }
329
+
330
+ if (isKeyword(current, schemaKeywords.time)) {
331
+ return zodKeywordMapper.time(current.args.type, options.coercion)
332
+ }
333
+
334
+ if (current.keyword in zodKeywordMapper && 'args' in current) {
335
+ const value = zodKeywordMapper[current.keyword as keyof typeof zodKeywordMapper] as (typeof zodKeywordMapper)['const']
336
+
337
+ return value((current as SchemaKeywordBase<unknown>).args as any)
338
+ }
339
+
340
+ if (current.keyword in zodKeywordMapper) {
341
+ return value()
342
+ }
343
+
344
+ return undefined
345
+ }