@@ -16,7 +16,11 @@ import {
16
16
GeneratorVerbOptions ,
17
17
isString ,
18
18
resolveRef ,
19
+ ContextSpecs ,
20
+ isObject ,
21
+ isBoolean ,
19
22
} from '@orval/core' ;
23
+ import SwaggerParser from '@apidevtools/swagger-parser' ;
20
24
21
25
const ZOD_DEPENDENCIES : GeneratorDependency [ ] = [
22
26
{
@@ -42,7 +46,7 @@ const resolveZodType = (schemaTypeValue: SchemaObject['type']) => {
42
46
case 'null' :
43
47
return 'mixed' ;
44
48
default :
45
- return schemaTypeValue ?? 'mixed ' ;
49
+ return schemaTypeValue ?? 'any ' ;
46
50
}
47
51
} ;
48
52
@@ -55,85 +59,124 @@ const generateZodValidationSchemaDefinition = (
55
59
56
60
const consts = [ ] ;
57
61
const functions : [ string , any ] [ ] = [ ] ;
58
- const type = resolveZodType ( schema ? .type ) ;
62
+ const type = resolveZodType ( schema . type ) ;
59
63
const required =
60
- schema ? .default !== undefined
64
+ schema . default !== undefined
61
65
? false
62
- : _required ?? ! schema ? .nullable ?? false ;
66
+ : _required ?? ! schema . nullable ?? false ;
63
67
const min =
64
- schema ?. minimum ??
65
- schema ?. exclusiveMinimum ??
66
- schema ?. minLength ??
67
- undefined ;
68
+ schema . minimum ?? schema . exclusiveMinimum ?? schema . minLength ?? undefined ;
68
69
const max =
69
- schema ?. maximum ??
70
- schema ?. exclusiveMaximum ??
71
- schema ?. maxLength ??
72
- undefined ;
73
- const matches = schema ?. pattern ?? undefined ;
70
+ schema . maximum ?? schema . exclusiveMaximum ?? schema . maxLength ?? undefined ;
71
+ const matches = schema . pattern ?? undefined ;
74
72
75
73
switch ( type ) {
76
- case 'object' :
77
- functions . push ( [
78
- 'object' ,
79
- Object . keys ( schema ?. properties ?? { } )
80
- . map ( ( key ) => ( {
81
- [ key ] : generateZodValidationSchemaDefinition (
82
- schema ?. properties ?. [ key ] as any ,
83
- schema ?. required ?. includes ( key ) ,
84
- camel ( `${ name } -${ key } ` ) ,
85
- ) ,
86
- } ) )
87
- . reduce ( ( acc , curr ) => ( { ...acc , ...curr } ) , { } ) ,
88
- ] ) ;
89
- break ;
90
74
case 'array' :
91
- const items = schema ? .items as SchemaObject | undefined ;
75
+ const items = schema . items as SchemaObject | undefined ;
92
76
functions . push ( [
93
77
'array' ,
94
78
generateZodValidationSchemaDefinition ( items , true , camel ( name ) ) ,
95
79
] ) ;
96
80
break ;
97
81
case 'string' : {
98
- if ( schema ? .enum && type === 'string' ) {
82
+ if ( schema . enum && type === 'string' ) {
99
83
break ;
100
84
}
101
85
102
86
functions . push ( [ type as string , undefined ] ) ;
103
87
104
- if ( schema ? .format === 'date-time' || schema ? .format === 'date' ) {
88
+ if ( schema . format === 'date-time' || schema . format === 'date' ) {
105
89
functions . push ( [ 'datetime' , undefined ] ) ;
106
90
break ;
107
91
}
108
92
109
- if ( schema ? .format === 'email' ) {
93
+ if ( schema . format === 'email' ) {
110
94
functions . push ( [ 'email' , undefined ] ) ;
111
95
break ;
112
96
}
113
97
114
- if ( schema ? .format === 'uri' || schema ? .format === 'hostname' ) {
98
+ if ( schema . format === 'uri' || schema . format === 'hostname' ) {
115
99
functions . push ( [ 'url' , undefined ] ) ;
116
100
break ;
117
101
}
118
102
119
- if ( schema ? .format === 'uuid' ) {
103
+ if ( schema . format === 'uuid' ) {
120
104
functions . push ( [ 'uuid' , undefined ] ) ;
121
105
break ;
122
106
}
123
107
124
108
break ;
125
109
}
126
- default :
110
+ case 'object' :
111
+ default : {
112
+ if ( schema . allOf || schema . oneOf || schema . anyOf ) {
113
+ const separator = schema . allOf
114
+ ? 'allOf'
115
+ : schema . oneOf
116
+ ? 'oneOf'
117
+ : 'anyOf' ;
118
+
119
+ const schemas = ( schema . allOf ?? schema . oneOf ?? schema . anyOf ) as (
120
+ | SchemaObject
121
+ | ReferenceObject
122
+ ) [ ] ;
123
+
124
+ functions . push ( [
125
+ separator ,
126
+ schemas . map ( ( schema ) =>
127
+ generateZodValidationSchemaDefinition (
128
+ schema as SchemaObject ,
129
+ true ,
130
+ camel ( name ) ,
131
+ ) ,
132
+ ) ,
133
+ ] ) ;
134
+ break ;
135
+ }
136
+
137
+ if ( schema . properties ) {
138
+ functions . push ( [
139
+ 'object' ,
140
+ Object . keys ( schema . properties )
141
+ . map ( ( key ) => ( {
142
+ [ key ] : generateZodValidationSchemaDefinition (
143
+ schema . properties ?. [ key ] as any ,
144
+ schema . required ?. includes ( key ) ,
145
+ camel ( `${ name } -${ key } ` ) ,
146
+ ) ,
147
+ } ) )
148
+ . reduce ( ( acc , curr ) => ( { ...acc , ...curr } ) , { } ) ,
149
+ ] ) ;
150
+
151
+ break ;
152
+ }
153
+
154
+ if ( schema . additionalProperties ) {
155
+ functions . push ( [
156
+ 'additionalProperties' ,
157
+ isBoolean ( schema . additionalProperties )
158
+ ? schema . additionalProperties
159
+ : generateZodValidationSchemaDefinition (
160
+ schema . additionalProperties as SchemaObject ,
161
+ true ,
162
+ name ,
163
+ ) ,
164
+ ] ) ;
165
+
166
+ break ;
167
+ }
168
+
127
169
functions . push ( [ type as string , undefined ] ) ;
128
170
break ;
171
+ }
129
172
}
130
173
131
174
if ( min !== undefined ) {
132
175
consts . push ( `export const ${ name } Min = ${ min } ;` ) ;
133
176
functions . push ( [ 'min' , `${ name } Min` ] ) ;
134
177
}
135
178
if ( max !== undefined ) {
136
- consts . push ( `export const ${ name } Max = ${ min } ;` ) ;
179
+ consts . push ( `export const ${ name } Max = ${ max } ;` ) ;
137
180
functions . push ( [ 'max' , `${ name } Max` ] ) ;
138
181
}
139
182
if ( matches ) {
@@ -147,11 +190,12 @@ const generateZodValidationSchemaDefinition = (
147
190
consts . push ( `export const ${ name } RegExp = ${ regexp } ;` ) ;
148
191
functions . push ( [ 'regex' , `${ name } RegExp` ] ) ;
149
192
}
150
- if ( schema ?. enum ) {
193
+
194
+ if ( schema . enum && type !== 'number' ) {
151
195
functions . push ( [
152
196
'enum' ,
153
197
[
154
- `[${ schema ? .enum
198
+ `[${ schema . enum
155
199
. map ( ( value ) => ( isString ( value ) ? `'${ escape ( value ) } '` : `${ value } ` ) )
156
200
. join ( ', ' ) } ]`,
157
201
] ,
@@ -168,40 +212,109 @@ const generateZodValidationSchemaDefinition = (
168
212
const parseZodValidationSchemaDefinition = (
169
213
input : Record < string , { functions : [ string , any ] [ ] ; consts : string [ ] } > ,
170
214
) : { zod : string ; consts : string } => {
171
- const parseProperty = ( [ fn , args = '' ] : [ string , any ] ) : string => {
172
- if ( fn === 'object' ) return ` ${ parseZodValidationSchemaDefinition ( args ) } ` ;
173
- if ( fn === 'array' )
174
- return `.array(${
175
- Array . isArray ( args )
176
- ? `zod${ args . map ( parseProperty ) . join ( '' ) } `
177
- : parseProperty ( args )
178
- } )`;
179
-
180
- return `.${ fn } (${ args } )` ;
181
- } ;
182
-
183
215
if ( ! Object . keys ( input ) . length ) {
184
216
return { zod : '' , consts : '' } ;
185
217
}
186
218
187
- const consts = Object . entries ( input ) . reduce ( ( acc , [ key , schema ] ) => {
219
+ let consts = '' ;
220
+
221
+ const parseProperty = ( property : [ string , any ] ) : string => {
222
+ const [ fn , args = '' ] = property ;
223
+ if ( fn === 'allOf' ) {
224
+ return args . reduce (
225
+ ( acc : string , { functions } : { functions : [ string , any ] [ ] } ) => {
226
+ const value = functions . map ( parseProperty ) . join ( '' ) ;
227
+ const valueWithZod = `${ value . startsWith ( '.' ) ? 'zod' : '' } ${ value } ` ;
228
+
229
+ if ( ! acc ) {
230
+ acc += valueWithZod ;
231
+ return acc ;
232
+ }
233
+
234
+ acc += `.and(${ valueWithZod } )` ;
235
+
236
+ return acc ;
237
+ } ,
238
+ '' ,
239
+ ) ;
240
+ }
241
+
242
+ if ( fn === 'oneOf' || fn === 'anyOf' ) {
243
+ return args . reduce (
244
+ ( acc : string , { functions } : { functions : [ string , any ] [ ] } ) => {
245
+ const value = functions . map ( parseProperty ) . join ( '' ) ;
246
+ const valueWithZod = `${ value . startsWith ( '.' ) ? 'zod' : '' } ${ value } ` ;
247
+
248
+ if ( ! acc ) {
249
+ acc += valueWithZod ;
250
+ return acc ;
251
+ }
252
+
253
+ acc += `.or(${ valueWithZod } )` ;
254
+
255
+ return acc ;
256
+ } ,
257
+ '' ,
258
+ ) ;
259
+ }
260
+
261
+ if ( fn === 'additionalProperties' ) {
262
+ const value = args . functions . map ( parseProperty ) . join ( '' ) ;
263
+ const valueWithZod = `${ value . startsWith ( '.' ) ? 'zod' : '' } ${ value } ` ;
264
+ return `zod.record(zod.string(), ${ valueWithZod } )` ;
265
+ }
266
+
267
+ if ( fn === 'object' ) {
268
+ const parsed = parseZodValidationSchemaDefinition ( args ) ;
269
+ consts += parsed . consts ;
270
+ return ` ${ parsed . zod } ` ;
271
+ }
272
+ if ( fn === 'array' ) {
273
+ const value = args . functions . map ( parseProperty ) . join ( '' ) ;
274
+ return `.array(${ value . startsWith ( '.' ) ? 'zod' : '' } ${ value } )` ;
275
+ }
276
+ return `.${ fn } (${ args } )` ;
277
+ } ;
278
+
279
+ consts += Object . entries ( input ) . reduce ( ( acc , [ key , schema ] ) => {
188
280
return acc + schema . consts . join ( '\n' ) ;
189
281
} , '' ) ;
190
282
191
283
const zod = `zod.object({
192
284
${ Object . entries ( input )
193
- . map (
194
- ( [ key , schema ] ) =>
195
- `"${ key } ": ${
196
- schema . functions [ 0 ] [ 0 ] !== 'object' ? 'zod' : ''
197
- } ${ schema . functions . map ( parseProperty ) . join ( '' ) } `,
198
- )
285
+ . map ( ( [ key , schema ] ) => {
286
+ const value = schema . functions . map ( parseProperty ) . join ( '' ) ;
287
+ return `"${ key } ": ${ value . startsWith ( '.' ) ? 'zod' : '' } ${ value } ` ;
288
+ } )
199
289
. join ( ',' ) }
200
290
})` ;
201
291
202
292
return { zod, consts } ;
203
293
} ;
204
294
295
+ const deferenceScalar = ( value : any , context : ContextSpecs ) : unknown => {
296
+ if ( isObject ( value ) ) {
297
+ return deference ( value , context ) ;
298
+ } else if ( Array . isArray ( value ) ) {
299
+ return value . map ( ( item ) => deferenceScalar ( item , context ) ) ;
300
+ } else {
301
+ return value ;
302
+ }
303
+ } ;
304
+
305
+ const deference = (
306
+ schema : SchemaObject | ReferenceObject ,
307
+ context : ContextSpecs ,
308
+ ) : SchemaObject => {
309
+ const { schema : resolvedSchema } = resolveRef < SchemaObject > ( schema , context ) ;
310
+
311
+ return Object . entries ( resolvedSchema ) . reduce ( ( acc , [ key , value ] ) => {
312
+ acc [ key ] = deferenceScalar ( value , context ) ;
313
+
314
+ return acc ;
315
+ } , { } as any ) ;
316
+ } ;
317
+
205
318
const generateZodRoute = (
206
319
{ operationName, body, verb } : GeneratorVerbOptions ,
207
320
{ pathRoute, context } : GeneratorOptions ,
@@ -231,13 +344,13 @@ const generateZodRoute = (
231
344
232
345
const zodDefinitionsResponseProperties =
233
346
resolvedResponseJsonSchema ?. properties ??
234
- ( [ ] as ( SchemaObject | ReferenceObject ) [ ] ) ;
347
+ ( { } as { [ p : string ] : SchemaObject | ReferenceObject } ) ;
235
348
236
349
const zodDefinitionsResponse = Object . entries (
237
350
zodDefinitionsResponseProperties ,
238
351
)
239
352
. map ( ( [ key , response ] ) => {
240
- const { schema } = resolveRef < SchemaObject > ( response , context ) ;
353
+ const schema = deference ( response , context ) ;
241
354
242
355
return {
243
356
[ key ] : generateZodValidationSchemaDefinition (
@@ -266,11 +379,11 @@ const generateZodRoute = (
266
379
267
380
const zodDefinitionsBodyProperties =
268
381
resolvedRequestBodyJsonSchema ?. properties ??
269
- ( [ ] as ( SchemaObject | ReferenceObject ) [ ] ) ;
382
+ ( { } as { [ p : string ] : SchemaObject | ReferenceObject } ) ;
270
383
271
384
const zodDefinitionsBody = Object . entries ( zodDefinitionsBodyProperties )
272
385
. map ( ( [ key , body ] ) => {
273
- const { schema } = resolveRef < SchemaObject > ( body , context ) ;
386
+ const schema = deference ( body , context ) ;
274
387
275
388
return {
276
389
[ key ] : generateZodValidationSchemaDefinition (
@@ -292,7 +405,7 @@ const generateZodRoute = (
292
405
return acc ;
293
406
}
294
407
295
- const { schema } = resolveRef < SchemaObject > ( parameter . schema , context ) ;
408
+ const schema = deference ( parameter . schema , context ) ;
296
409
297
410
const definition = generateZodValidationSchemaDefinition (
298
411
schema ,
1 commit comments
vercel[bot] commentedon Apr 4, 2023
Successfully deployed to the following URLs:
orval – ./
orval.vercel.app
www.orval.dev
orval-git-master-anymaniax.vercel.app
orval.dev
orval-anymaniax.vercel.app