@@ -26,88 +26,159 @@ public function __construct(
2626
2727 protected function process (): void
2828 {
29+ // https://laravel.com/docs/12.x/validation#available-validation-rules
30+ $ typeMap = [
31+ 'string ' => 'string ' ,
32+ 'integer ' => 'number ' ,
33+ 'int ' => 'number ' ,
34+ 'numeric ' => 'number ' ,
35+ 'float ' => 'number ' ,
36+ 'double ' => 'number ' ,
37+ 'decimal ' => 'number ' ,
38+ 'boolean ' => 'boolean ' ,
39+ 'bool ' => 'boolean ' ,
40+ 'accepted ' => 'boolean ' ,
41+ 'array ' => 'any[] ' ,
42+ 'object ' => 'Record<string, any> ' ,
43+ 'date ' => 'string ' ,
44+ 'email ' => 'string ' ,
45+ 'file ' => 'File ' ,
46+ 'image ' => 'File ' ,
47+ 'json ' => 'any ' ,
48+ 'url ' => 'string ' ,
49+ 'uuid ' => 'string ' ,
50+ 'ip ' => 'string ' ,
51+ 'active_url ' => 'string ' ,
52+ 'timezone ' => 'string ' ,
53+ 'digits ' => 'string ' ,
54+ 'digits_between ' => 'number ' ,
55+ 'ends_with ' => 'string ' ,
56+ 'starts_with ' => 'string ' ,
57+ 'alpha ' => 'string ' ,
58+ 'alpha_dash ' => 'string ' ,
59+ 'alpha_num ' => 'string ' ,
60+ 'regex ' => 'string ' ,
61+ 'present ' => '' ,
62+ 'distinct ' => '' ,
63+ 'nullable ' => '' ,
64+ 'required ' => '' ,
65+ 'sometimes ' => '' ,
66+ 'confirmed ' => '' ,
67+ 'between ' => '' ,
68+ 'in ' => '' ,
69+ 'not_in ' => '' ,
70+ 'size ' => '' ,
71+ 'min ' => '' ,
72+ 'max ' => '' ,
73+ ];
74+
2975 $ path = $ this ->option ('output ' );
76+ $ tsContent = '' ;
3077
31- $ formRequests = $ this ->readFormRequests ($ this ->base ());
78+ $ formRequests = $ this ->readFormRequests ($ this ->base ());
3279
33- // Group form requests by namespace
34- $ grouped = collect ($ formRequests )
80+ $ formRequests
3581 ->groupBy (function ($ formRequest ) {
36- return Str::of ($ formRequest ['class ' ])
37- ->beforeLast ('\\' )
38- ->replace ('\\' , '. ' )
39- ->toString ();
40- });
82+ return Str::of ($ formRequest ['class ' ])
83+ ->beforeLast ('\\' )
84+ ->replace ('\\' , '. ' )
85+ ->toString ();
86+ })
87+ ->each (function ($ requests , $ namespace ) use (&$ tsContent , $ typeMap ) {
88+ $ tsContent .= "declare namespace {$ namespace } { \n" ;
89+ foreach ($ requests as $ request ) {
90+ $ entity = $ request ['entity ' ];
91+ $ fields = $ request ['rules ' ];
92+
93+
94+ $ tsFields = [];
95+ foreach ($ fields as $ field => $ rules ) {
96+ $ tsType = 'string ' ; // default
97+ $ isNullable = false ;
98+ $ isRequired = false ;
99+
100+ foreach ($ rules as $ rule ) {
101+ $ rule = strtolower ($ rule );
102+ if (isset ($ typeMap [$ rule ]) && $ typeMap [$ rule ] !== '' ) {
103+ $ tsType = $ typeMap [$ rule ];
104+ }
105+ if ($ rule === 'nullable ' ) {
106+ $ isNullable = true ;
107+ }
108+ if ($ rule === 'required ' ) {
109+ $ isRequired = true ;
110+ }
111+ }
112+
113+ if ($ isNullable ) {
114+ $ tsType .= ' | null ' ;
115+ }
116+
117+ $ optional = $ isRequired ? '' : '? ' ;
118+ $ tsFields [] = " {$ field }{$ optional }: {$ tsType }; " ;
119+ }
41120
42- $ tsContent = $ grouped ->map (function ($ requests , $ namespace ) {
43- $ interfaces = collect ($ requests )->map (function ($ formRequest ) {
44- $ rulesString = collect ($ formRequest ['rules ' ])
45- ->map (fn ($ type , $ field ) => "{$ field }{$ type }; " )
46- ->join ("\n" );
47- return <<<TS
48- /**
49- * @see {$ formRequest ['class ' ]}
50- */
51- export interface {$ formRequest ['entity ' ]} {
52- {$ rulesString }
53- }
54- TS ;
55- })->join ("\n\n" );
56-
57- return <<<JAVASCRIPT
58- export namespace {$ namespace } {
59- {$ interfaces }
60- }
61- JAVASCRIPT ;
62- })->join ("\n\n" );
121+ $ tsContent .= " export type {$ entity } = { \n" ;
122+ $ tsContent .= implode ("\n" , $ tsFields );
123+ $ tsContent .= "\n }; \n" ;
124+ }
125+ $ tsContent .= "} \n" ;
126+ });
63127
64- $ tsContent .= "\n\nexport {}; \n" ;
65128 $ this ->files ->put ($ this ->tsFilePath ($ path ), $ tsContent );
66-
67129 }
68130
69- protected function parseRules (FormRequest $ formRequest ) {
70- $ mappings = [
71- // TS -> PHP
72- 'string ' => ['string ' , 'email ' ],
73- 'boolean ' => ['boolean ' ],
74- 'number ' => ['number ' , 'integer ' ],
75- 'any[] ' => ['array ' ],
76- ];
77-
78- $ rules = $ formRequest ->rules ();
79-
80- // Preparse the rules into a consistent format for easier processing
81- $ r = collect ($ rules )
82- ->map (function ($ rules , $ field ) use ($ mappings , $ formRequest ) {
83- $ adjustedRules = is_array ($ rules ) ? $ rules : explode ('| ' , $ rules );
131+ protected function parseRules (FormRequest $ formRequest ) {
132+ $ arrayOfRulesOrString = function ($ rules ) {
133+ if (is_string ($ rules )) {
134+ return explode ('| ' , $ rules );
135+ }
136+ // If it's an array, flatten and explode any pipe-separated strings
137+ return collect ($ rules )
138+ ->flatten ()
139+ ->filter (fn ($ rule ) => is_string ($ rule ))
140+ ->flatMap (function ($ rule ) {
141+ return str_contains ($ rule , '| ' ) ? explode ('| ' , $ rule ) : [$ rule ];
142+ })
143+ ->map (fn ($ rule ) => trim ($ rule ))
144+ ->values ()
145+ ->toArray ();
146+ };
147+
148+ $ fields = collect ($ formRequest ->rules ());
149+
150+ // Find all child keys (e.g., field.something) so we can remove them from the top level
151+ $ childKeys = collect ($ fields )
152+ ->keys ()
153+ ->filter (fn ($ k ) => str_contains ($ k , '. ' ) && $ fields ->has (Str::before ($ k , '. ' )))
154+ ->values ();
84155
85- return [
86- 'rules ' => $ adjustedRules
87- ];
88- })
89- // Strip any keys that contain a dot
90- ->reject (fn ($ v , $ k ) => Str::contains ($ k , '. ' ))
91- // Strip any rules that are closures or invokable objects
92- ->reject (fn ($ v ) => collect ($ v ['rules ' ])->contains (fn ($ rule ) => is_object ($ rule )))
93- ->map (function ($ item , $ field ) use ($ mappings ) {
94- $ isNullable = in_array ('nullable ' , $ item ['rules ' ]);
95- $ isSometimes = in_array ('sometimes ' , $ item ['rules ' ]);
96- $ isArray = in_array ('array ' , $ item ['rules ' ]);
97- $ type = 'any ' ;
98-
99- foreach ($ mappings as $ tsType => $ phpTypes ) {
100- if (count (array_intersect ($ phpTypes , $ item ['rules ' ])) > 0 ) {
101- $ type = $ tsType ;
102- break ;
156+ return $ fields
157+ // Normalise all rules to arrays of strings for each field
158+ ->map ($ arrayOfRulesOrString )
159+ // TODO: Support array/object inference
160+ /*
161+ ->map(function ($adjusted, $field) use ($fields, $arrayOfRulesOrString) {
162+ if (in_array('array', $adjusted)) {
163+ // Find all rules for keys like 'field.*'
164+ $children = collect($fields)
165+ ->filter(fn ($v, $k) => str_starts_with($k, $field . '.') && preg_match('/^' . preg_quote($field, '/') . '\.\*$/', $k))
166+ ->map($arrayOfRulesOrString);
167+
168+ if ($children->isNotEmpty()) {
169+ return [
170+ 'rules' => $adjusted,
171+ 'children' => $children->toArray(),
172+ ];
103173 }
104174 }
105175
106- $ prefix = ($ isNullable || $ isSometimes ) ? '?: ' : ': ' ;
107- return "{$ prefix } {$ type }" ;
108- });
109-
110- return $ r ->toArray ();
176+ return $adjusted;
177+ })
178+ */
179+ // Remove child keys from the top level
180+ ->reject (fn ($ v , $ k ) => $ childKeys ->contains ($ k ))
181+ ->toArray ();
111182 }
112183
113184 protected function readFormRequests (string $ path )
@@ -131,7 +202,6 @@ protected function readFormRequests(string $path)
131202 'rules ' => $ this ->parseRules (new $ class ()),
132203 'class ' => $ class
133204 ];
134- })
135- ->toArray ();
205+ }) ;
136206 }
137207}
0 commit comments