11import {
22 conventionName ,
33 type ContextSpec ,
4+ type GeneratorVerbOptions ,
5+ type NamingConvention ,
46 type NormalizedOutputOptions ,
7+ type OpenApiSchemaObject ,
8+ pascal ,
59 upath ,
610 type WriteSpecBuilder ,
711} from '@orval/core' ;
@@ -13,6 +17,58 @@ import {
1317} from '@orval/zod' ;
1418import fs from 'fs-extra' ;
1519
20+ function generateZodSchemaFileContent (
21+ header : string ,
22+ schemaName : string ,
23+ zodContent : string ,
24+ ) : string {
25+ return `${ header } import { z as zod } from 'zod';
26+
27+ export const ${ schemaName } = ${ zodContent }
28+
29+ export type ${ schemaName } = zod.infer<typeof ${ schemaName } >;
30+ ` ;
31+ }
32+
33+ async function writeZodSchemaIndex (
34+ schemasPath : string ,
35+ fileExtension : string ,
36+ header : string ,
37+ schemaNames : string [ ] ,
38+ namingConvention : NamingConvention ,
39+ shouldMergeExisting : boolean = false ,
40+ ) {
41+ const importFileExtension = fileExtension . replace ( / \. t s $ / , '' ) ;
42+ const indexPath = upath . join ( schemasPath , `index${ fileExtension } ` ) ;
43+
44+ let existingExports = '' ;
45+ if ( shouldMergeExisting && ( await fs . pathExists ( indexPath ) ) ) {
46+ const existingContent = await fs . readFile ( indexPath , 'utf-8' ) ;
47+ const headerMatch = existingContent . match ( / ^ ( \/ \* \* [ \s \S ] * ?\* \/ \n ) ? / ) ;
48+ const headerPart = headerMatch ? headerMatch [ 0 ] : '' ;
49+ existingExports = existingContent . substring ( headerPart . length ) . trim ( ) ;
50+ }
51+
52+ const newExports = schemaNames
53+ . map ( ( schemaName ) => {
54+ const fileName = conventionName ( schemaName , namingConvention ) ;
55+ return `export * from './${ fileName } ${ importFileExtension } ';` ;
56+ } )
57+ . sort ( )
58+ . join ( '\n' ) ;
59+
60+ const allExports = existingExports
61+ ? `${ existingExports } \n${ newExports } `
62+ : newExports ;
63+
64+ const uniqueExports = [ ...new Set ( allExports . split ( '\n' ) ) ]
65+ . filter ( ( line ) => line . trim ( ) )
66+ . sort ( )
67+ . join ( '\n' ) ;
68+
69+ await fs . outputFile ( indexPath , `${ header } \n${ uniqueExports } \n` ) ;
70+ }
71+
1672export async function writeZodSchemas (
1773 builder : WriteSpecBuilder ,
1874 schemasPath : string ,
@@ -21,7 +77,6 @@ export async function writeZodSchemas(
2177 output : NormalizedOutputOptions ,
2278) {
2379 const schemasWithOpenApiDef = builder . schemas . filter ( ( s ) => s . schema ) ;
24- const importFileExtension = fileExtension . replace ( / \. t s $ / , '' ) ;
2580
2681 await Promise . all (
2782 schemasWithOpenApiDef . map ( async ( generatorSchema ) => {
@@ -73,28 +128,178 @@ export async function writeZodSchemas(
73128 isZodV4 ,
74129 ) ;
75130
76- const fileContent = `${ header } import { z as zod } from 'zod';
77- ${ parsedZodDefinition . consts ? `\n${ parsedZodDefinition . consts } \n` : '' }
78- export const ${ name } Schema = ${ parsedZodDefinition . zod } ;
79- export type ${ name } = zod.infer<typeof ${ name } Schema>;
80- ` ;
131+ const zodContent = parsedZodDefinition . consts
132+ ? `${ parsedZodDefinition . consts } \n${ parsedZodDefinition . zod } `
133+ : parsedZodDefinition . zod ;
134+
135+ const fileContent = generateZodSchemaFileContent (
136+ header ,
137+ name ,
138+ zodContent ,
139+ ) ;
81140
82141 await fs . outputFile ( filePath , fileContent ) ;
83142 } ) ,
84143 ) ;
85144
86145 if ( output . indexFiles ) {
87- const schemaFilePath = upath . join ( schemasPath , `/index${ fileExtension } ` ) ;
88-
89- const exports = schemasWithOpenApiDef
90- . map ( ( schema ) => {
91- const fileName = conventionName ( schema . name , output . namingConvention ) ;
92- return `export * from './${ fileName } ${ importFileExtension } ';` ;
93- } )
94- . toSorted ( ( a , b ) => a . localeCompare ( b ) )
95- . join ( '\n' ) ;
96-
97- const fileContent = `${ header } \n${ exports } ` ;
98- await fs . outputFile ( schemaFilePath , fileContent ) ;
146+ const schemaNames = schemasWithOpenApiDef . map ( ( schema ) => schema . name ) ;
147+ await writeZodSchemaIndex (
148+ schemasPath ,
149+ fileExtension ,
150+ header ,
151+ schemaNames ,
152+ output . namingConvention ,
153+ false ,
154+ ) ;
155+ }
156+ }
157+
158+ export async function writeZodSchemasFromVerbs (
159+ verbOptions : Record < string , GeneratorVerbOptions > ,
160+ schemasPath : string ,
161+ fileExtension : string ,
162+ header : string ,
163+ output : NormalizedOutputOptions ,
164+ context : ContextSpec ,
165+ ) {
166+ const verbOptionsArray = Object . values ( verbOptions ) ;
167+
168+ if ( verbOptionsArray . length === 0 ) {
169+ return ;
170+ }
171+
172+ const isZodV4 = ! ! output . packageJson && isZodVersionV4 ( output . packageJson ) ;
173+ const strict =
174+ typeof output . override ?. zod ?. strict === 'object'
175+ ? ( output . override . zod . strict . body ?? false )
176+ : ( output . override ?. zod ?. strict ?? false ) ;
177+ const coerce =
178+ typeof output . override ?. zod ?. coerce === 'object'
179+ ? ( output . override . zod . coerce . body ?? false )
180+ : ( output . override ?. zod ?. coerce ?? false ) ;
181+
182+ const generateVerbsSchemas = verbOptionsArray . flatMap ( ( verbOption ) => {
183+ const operation = verbOption . originalOperation ;
184+
185+ const bodySchema =
186+ operation . requestBody && 'content' in operation . requestBody
187+ ? operation . requestBody . content [ 'application/json' ] ?. schema
188+ : undefined ;
189+
190+ const bodySchemas = bodySchema
191+ ? [
192+ {
193+ name : `${ pascal ( verbOption . operationName ) } Body` ,
194+ schema : dereference ( bodySchema as OpenApiSchemaObject , context ) ,
195+ } ,
196+ ]
197+ : [ ] ;
198+
199+ const queryParams = operation . parameters ?. filter (
200+ ( p ) => 'in' in p && p . in === 'query' ,
201+ ) ;
202+
203+ const queryParamsSchemas =
204+ queryParams && queryParams . length > 0
205+ ? [
206+ {
207+ name : `${ pascal ( verbOption . operationName ) } Params` ,
208+ schema : {
209+ type : 'object' as const ,
210+ properties : Object . fromEntries (
211+ queryParams
212+ . filter ( ( p ) => 'schema' in p && p . schema )
213+ . map ( ( p ) => [
214+ p . name ,
215+ dereference ( p . schema as OpenApiSchemaObject , context ) ,
216+ ] ) ,
217+ ) ,
218+ required : queryParams
219+ . filter ( ( p ) => p . required )
220+ . map ( ( p ) => p . name ) ,
221+ } ,
222+ } ,
223+ ]
224+ : [ ] ;
225+
226+ const headerParams = operation . parameters ?. filter (
227+ ( p ) => 'in' in p && p . in === 'header' ,
228+ ) ;
229+
230+ const headerParamsSchemas =
231+ headerParams && headerParams . length > 0
232+ ? [
233+ {
234+ name : `${ pascal ( verbOption . operationName ) } Headers` ,
235+ schema : {
236+ type : 'object' as const ,
237+ properties : Object . fromEntries (
238+ headerParams
239+ . filter ( ( p ) => 'schema' in p && p . schema )
240+ . map ( ( p ) => [
241+ p . name ,
242+ dereference ( p . schema as OpenApiSchemaObject , context ) ,
243+ ] ) ,
244+ ) ,
245+ required : headerParams
246+ . filter ( ( p ) => p . required )
247+ . map ( ( p ) => p . name ) ,
248+ } ,
249+ } ,
250+ ]
251+ : [ ] ;
252+
253+ return [ ...bodySchemas , ...queryParamsSchemas , ...headerParamsSchemas ] ;
254+ } ) ;
255+
256+ await Promise . all (
257+ generateVerbsSchemas . map ( async ( { name, schema } ) => {
258+ const fileName = conventionName ( name , output . namingConvention ) ;
259+ const filePath = upath . join ( schemasPath , `${ fileName } ${ fileExtension } ` ) ;
260+
261+ const zodDefinition = generateZodValidationSchemaDefinition (
262+ schema ,
263+ context ,
264+ name ,
265+ strict ,
266+ isZodV4 ,
267+ {
268+ required : true ,
269+ } ,
270+ ) ;
271+
272+ const parsedZodDefinition = parseZodValidationSchemaDefinition (
273+ zodDefinition ,
274+ context ,
275+ coerce ,
276+ strict ,
277+ isZodV4 ,
278+ ) ;
279+
280+ const zodContent = parsedZodDefinition . consts
281+ ? `${ parsedZodDefinition . consts } \n${ parsedZodDefinition . zod } `
282+ : parsedZodDefinition . zod ;
283+
284+ const fileContent = generateZodSchemaFileContent (
285+ header ,
286+ name ,
287+ zodContent ,
288+ ) ;
289+
290+ await fs . outputFile ( filePath , fileContent ) ;
291+ } ) ,
292+ ) ;
293+
294+ if ( output . indexFiles && generateVerbsSchemas . length > 0 ) {
295+ const schemaNames = generateVerbsSchemas . map ( ( s ) => s . name ) ;
296+ await writeZodSchemaIndex (
297+ schemasPath ,
298+ fileExtension ,
299+ header ,
300+ schemaNames ,
301+ output . namingConvention ,
302+ true ,
303+ ) ;
99304 }
100305}
0 commit comments