formidable_derive/
lib.rs

1use core::panic;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{Attribute, Meta, MetaNameValue, Expr, Lit};
6
7#[proc_macro_derive(Form, attributes(form))]
8pub fn my_proc_macro(input: TokenStream) -> TokenStream {
9    let ast = syn::parse(input).unwrap();
10    impl_form_macro(&ast)
11}
12
13// Helper function to create String from expression (for top-level generation)
14fn create_string_from_expr(expr: &Expr) -> proc_macro2::TokenStream {
15    match expr {
16        Expr::Lit(expr_lit) => {
17            if let Lit::Str(_) = &expr_lit.lit {
18                quote! {
19                    String::from(#expr)
20                }
21            } else {
22                panic!("Only string literals are supported");
23            }
24        },
25        #[cfg(feature = "leptos_i18n")]
26        Expr::Path(_) => {
27            quote! {
28                {
29                    let i18n = crate::app::i18n::use_i18n();
30                    String::from(leptos_i18n::tu_string!(i18n, #expr))
31                }
32            }
33        },
34        _ => {
35            panic!("Only string literals and i18n paths are supported");
36        }
37    }
38}
39
40// Unified form attribute configuration parsing
41#[derive(Default)]
42struct FieldConfigurationParser {
43    label: Option<Expr>,
44    description: Option<Expr>,
45}
46
47impl FieldConfigurationParser {
48    fn parse_from_attributes(attrs: &[Attribute]) -> Self {
49        let mut config = Self::default();
50        
51        for attr in attrs {
52            if attr.path().is_ident("form") {
53                match &attr.meta {
54                    Meta::List(meta_list) => {
55                        let parsed = meta_list.parse_args_with(|input: syn::parse::ParseStream| {
56                            let mut pairs = Vec::new();
57                            
58                            while !input.is_empty() {
59                                let name: syn::Ident = input.parse()?;
60                                input.parse::<syn::Token![=]>()?;
61                                let value: syn::Expr = input.parse()?;
62                                pairs.push((name, value));
63                                
64                                // Handle optional comma
65                                if input.peek(syn::Token![,]) {
66                                    input.parse::<syn::Token![,]>()?;
67                                }
68                            }
69                            
70                            Ok(pairs)
71                        });
72                        
73                        if let Ok(pairs) = parsed {
74                            for (name, value) in pairs {
75                                match name.to_string().as_str() {
76                                    "label" => config.label = Some(value),
77                                    "description" => config.description = Some(value),
78                                    _ => {} // Ignore unknown attributes
79                                }
80                            }
81                        }
82                    },
83                    Meta::NameValue(MetaNameValue { path, value, .. }) => {
84                        // Handle single name-value pairs like #[form(label = "value")]
85                        if path.is_ident("label") {
86                            config.label = Some(value.clone());
87                        } else if path.is_ident("description") {
88                            config.description = Some(value.clone());
89                        }
90                    },
91                    _ => {} // Ignore other meta types
92                }
93            }
94        }
95        
96        config
97    }
98    
99
100    fn to_field_configuration(&self) -> proc_macro2::TokenStream {       
101        let label =  create_string_from_expr(self.label.as_ref().expect("Label is required"));
102        let label = quote! { leptos::prelude::TextProp::from(#label) };
103
104        let description = if let Some(desc_expr) = &self.description {
105            let description = create_string_from_expr(desc_expr);
106            quote! { Some(leptos::prelude::TextProp::from(#description)) }
107        } else {
108            quote! { None }
109        };
110
111        quote! {
112            formidable::FieldConfiguration {
113                label: Some(#label),
114                description: #description,
115            }
116        }
117    }
118
119    fn label_string(&self) -> proc_macro2::TokenStream {
120        let label = self.label.as_ref().expect("Label is required");
121        create_string_from_expr(label)
122    }
123}
124
125// Shared field processing logic to eliminate duplication
126struct FieldProcessor;
127
128impl FieldProcessor {
129    /// Generate field signals for tracking field state
130    fn generate_field_signals(
131        fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
132        enum_name: Option<&syn::Ident>,
133        variant_name: Option<&syn::Ident>,
134    ) -> Vec<proc_macro2::TokenStream> {
135        fields.iter().map(|field| {
136            let field_name = field.ident.as_ref().unwrap();
137            let signal_name = quote::format_ident!("{}_signal", field_name);
138            let field_type = &field.ty;
139            
140            let initial_value = if let (Some(enum_name), Some(variant_name)) = (enum_name, variant_name) {
141                quote! {
142                    match value.as_ref() {
143                        Some(#enum_name::#variant_name { #field_name, .. }) => Some(Ok(#field_name.clone())),
144                        _ => None,
145                    }
146                }
147            } else {
148                quote! { value.as_ref().map(|v| Ok(v.#field_name.clone())) }
149            };
150            
151            quote! {
152                let #signal_name: leptos::prelude::RwSignal<Option<Result<#field_type, formidable::FormError>>> = 
153                    leptos::prelude::RwSignal::new(#initial_value);
154            }
155        }).collect()
156    }
157    
158    /// Generate field signal names for validation logic
159    fn generate_field_signal_names(
160        fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
161    ) -> Vec<proc_macro2::Ident> {
162        fields.iter().map(|field| {
163            let field_name = field.ident.as_ref().unwrap();
164            quote::format_ident!("{}_signal", field_name)
165        }).collect()
166    }
167    
168    /// Generate field constructor expressions for building structs/enums
169    fn generate_field_constructor(
170        fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
171    ) -> Vec<proc_macro2::TokenStream> {
172        fields.iter().map(|field| {
173            let field_name = field.ident.as_ref().unwrap();
174            let signal_name = quote::format_ident!("{}_signal", field_name);
175            quote! {
176                #field_name: #signal_name.get_untracked().and_then(|r| r.ok()).expect("Field should be valid when all_ok is true")
177            }
178        }).collect()
179    }
180    
181    /// Generate field form UI elements
182    fn generate_field_forms(
183        fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
184    ) -> Vec<proc_macro2::TokenStream> {
185        fields.iter().map(|field| {
186            let field_name = field.ident.as_ref().unwrap();
187            let field_name_str = field_name.to_string();
188            let field_type = &field.ty;
189            let form_config = FieldConfigurationParser::parse_from_attributes(&field.attrs);
190            let signal_name = quote::format_ident!("{}_signal", field_name);
191            let field_configuration = form_config.to_field_configuration();
192            
193            quote! {
194                {
195                    let field_name_as_name = name.push_key(#field_name_str);
196                    let field_value = #signal_name.get_untracked().and_then(|r| r.ok());
197                    let field_callback = Some(leptos::prelude::Callback::new(move |result: Result<#field_type, formidable::FormError>| {
198                        #signal_name.set(Some(result));
199                    }));
200                    
201                    <#field_type as Form>::view(
202                        #field_configuration,
203                        field_name_as_name, 
204                        field_value, 
205                        field_callback
206                    )
207                }
208            }
209        }).collect()
210    }
211    
212    /// Generate unified callback effect for field validation and construction
213    fn generate_callback_effect(
214        field_signal_names: &[proc_macro2::Ident],
215        constructor_type: ConstructorType,
216    ) -> proc_macro2::TokenStream {
217        let constructor = match constructor_type {
218            ConstructorType::Struct { name, field_constructor } => {
219                quote! {
220                    let merged_struct = #name {
221                        #(#field_constructor),*
222                    };
223                    parent_callback.run(Ok(merged_struct));
224                }
225            }
226            ConstructorType::EnumVariant { enum_name, variant_name, field_constructor } => {
227                quote! {
228                    let new_enum_value = #enum_name::#variant_name {
229                        #(#field_constructor),*
230                    };
231                    parent_callback.run(Ok(new_enum_value));
232                }
233            }
234        };
235        
236        quote! {
237            if let Some(parent_callback) = callback {
238                leptos::prelude::Effect::new(move || {
239                    // Check if all fields have some value (either Ok or Err)
240                    let all_fields_have_values = #(#field_signal_names.get().is_some())&&*;
241                    
242                    if all_fields_have_values {
243                        // All fields have been touched, now check if all are valid
244                        let all_ok = #(#field_signal_names.get().map(|r| r.is_ok()).unwrap_or(false))&&*;
245                        
246                        if all_ok {
247                            // All fields are valid, construct the result
248                            #constructor
249                        } else {
250                            // Some fields have errors, collect and merge them
251                            let mut merged_error = formidable::FormError::from(vec![]);
252                            #(
253                                if let Some(Err(err)) = #field_signal_names.get() {
254                                    merged_error.extend(err);
255                                }
256                            )*
257                            parent_callback.run(Err(merged_error));
258                        }
259                    }
260                });
261            }
262        }
263    }
264}
265
266/// Constructor type for generating different construction logic
267enum ConstructorType<'a> {
268    Struct {
269        name: &'a syn::Ident,
270        field_constructor: &'a [proc_macro2::TokenStream],
271    },
272    EnumVariant {
273        enum_name: &'a syn::Ident,
274        variant_name: &'a syn::Ident,
275        field_constructor: &'a [proc_macro2::TokenStream],
276    },
277}
278
279fn impl_form_macro(ast: &syn::DeriveInput) -> TokenStream {
280    let name = &ast.ident;
281
282    match &ast.data {
283        syn::Data::Struct(data_struct) => impl_form_for_struct(name, data_struct),
284        syn::Data::Enum(data_enum) => impl_form_for_enum(name, data_enum),
285        _ => panic!("Form can only be derived for structs and enums"),
286    }
287}
288
289fn impl_form_for_enum(name: &syn::Ident, data_enum: &syn::DataEnum) -> TokenStream {
290    let variants = &data_enum.variants;
291    
292    // Create a discriminant enum for variant selection
293    let discriminant_name = quote::format_ident!("{}Discriminant", name);
294    
295    // Generate discriminant enum variants with strum attributes
296    let discriminant_variants: Vec<_> = variants.iter().map(|variant| {
297        let variant_name = &variant.ident;
298        
299        quote! {
300            #variant_name 
301        }
302    }).collect();
303    
304    // Generate match arms for discriminant detection
305    let discriminant_match_arms: Vec<_> = variants.iter().map(|variant| {
306        let variant_name = &variant.ident;
307        match &variant.fields {
308            syn::Fields::Unit => {
309                quote! { #name::#variant_name => #discriminant_name::#variant_name }
310            },
311            syn::Fields::Unnamed(_) => {
312                quote! { #name::#variant_name(..) => #discriminant_name::#variant_name }
313            },
314            syn::Fields::Named(_) => {
315                quote! { #name::#variant_name { .. } => #discriminant_name::#variant_name }
316            }
317        }
318    }).collect();
319    
320    // Generate variant forms - simplified version for now
321    let variant_forms: Vec<_> = variants.iter().map(|variant| {
322        let variant_name = &variant.ident;
323        let form_config = FieldConfigurationParser::parse_from_attributes(&variant.attrs);
324        let field_configuration = form_config.to_field_configuration();
325
326        
327        match &variant.fields {
328            syn::Fields::Unit => {
329                // No fields, just the variant selection - call callback immediately when this variant is selected
330                quote! {
331                    #discriminant_name::#variant_name => {
332                        // For unit variants, call the callback immediately with the variant
333                        if let Some(parent_callback) = callback {
334                            leptos::prelude::Effect::new(move || {
335                                let new_enum_value = #name::#variant_name;
336                                parent_callback.run(Ok(new_enum_value));
337                            });
338                        }
339                        
340                        ().into_any()
341                    }
342                }
343            },
344            syn::Fields::Unnamed(fields) => {
345                if fields.unnamed.len() == 1 {
346                    // Single unnamed field - use variant's form attributes for field configuration
347                    let field_type = &fields.unnamed.first().unwrap().ty;
348                    
349                    
350                    quote! {
351                        #discriminant_name::#variant_name => {
352                            let field_value = match value.as_ref() {
353                                Some(#name::#variant_name(inner)) => Some(inner.clone()),
354                                _ => None,
355                            };
356                            let field_callback = callback.map(|cb| leptos::prelude::Callback::new(move |result: Result<#field_type, formidable::FormError>| {
357                                match result {
358                                    Ok(inner_value) => {
359                                        let new_enum_value = #name::#variant_name(inner_value);
360                                        cb.run(Ok(new_enum_value));
361                                    },
362                                    Err(err) => cb.run(Err(err)),
363                                }
364                            }));
365                            
366                            <#field_type as Form>::view(
367                                #field_configuration,
368                                name, 
369                                field_value, 
370                                field_callback
371                            ).into_any()
372                        }
373                    }
374                } else {
375                    // Multiple unnamed fields - simplified for now
376                    panic!("Multiple unnamed fields in enum variants not supported");
377                }
378            },
379            syn::Fields::Named(fields) => {
380                // Named fields - use shared field processing logic
381                let field_signals = FieldProcessor::generate_field_signals(&fields.named, Some(name), Some(variant_name));
382                let field_signal_names = FieldProcessor::generate_field_signal_names(&fields.named);
383                let field_constructor = FieldProcessor::generate_field_constructor(&fields.named);
384                let field_forms = FieldProcessor::generate_field_forms(&fields.named);
385           
386                let callback_effect = FieldProcessor::generate_callback_effect(
387                    &field_signal_names,
388                    ConstructorType::EnumVariant {
389                        enum_name: name,
390                        variant_name,
391                        field_constructor: &field_constructor,
392                    },
393                );
394                
395                quote! {
396                    #discriminant_name::#variant_name => {                        
397                        #(#field_signals)*
398                        
399                        #callback_effect
400
401                        let field_configuration = #field_configuration;
402                        
403                        view! {
404                            <formidable::components::Section name=name heading={field_configuration.label.clone()}>
405                                #(#field_forms)*
406                            </formidable::components::Section>
407                        }.into_any()
408                    }
409                }
410            }
411        }
412    }).collect();
413    
414    // Generate match arms for discriminant value_label (used in variant selector)
415    let discriminant_value_label_match_arms: Vec<_> = variants.iter().map(|variant| {
416        let variant_name = &variant.ident;
417        let form_config = FieldConfigurationParser::parse_from_attributes(&variant.attrs);
418        let label_string = form_config.label_string();
419        
420        quote! { #discriminant_name::#variant_name => {
421            write!(f, "{}", #label_string)
422        } }
423    }).collect();
424    
425    let generated = quote! {
426        impl Form for #name {
427            fn view(
428                field: formidable::FieldConfiguration,
429                name: formidable::Name,
430                value: Option<Self>,
431                callback: Option<leptos::prelude::Callback<Result<Self, formidable::FormError>>>,
432            ) -> impl leptos::prelude::IntoView {
433                use leptos::prelude::*;
434                use formidable::components;
435                use strum::VariantArray;
436                
437                // For now, create a simple discriminant enum inline
438                #[derive(Clone, Copy, Debug, PartialEq, Eq, strum::IntoStaticStr, strum::VariantArray, Default)]
439                enum #discriminant_name {
440                    #[default]
441                    #(#discriminant_variants),*
442                }
443                
444                // Determine current discriminant from value
445                let current_discriminant = value.as_ref().map(|v| {
446                    match v {
447                        #(#discriminant_match_arms,)*
448                    }
449                }).unwrap_or_default();
450                
451                let selected_discriminant = RwSignal::new(current_discriminant);
452
453                impl std::fmt::Display for #discriminant_name {
454                    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455                        match self {
456                            #(#discriminant_value_label_match_arms)*
457                        }
458                    }
459                }
460                
461                view! {
462                    <div>
463                        // Variant selector
464                        <div class="variant-selector">
465                            {
466                                if #discriminant_name::VARIANTS.len() > 5 {
467                                    view! { <components::Select label=field.label.expect("No label provided") name=name.push_key("variant") value=selected_discriminant /> }.into_any()
468                                } else {
469                                    view! { <components::Radio label=field.label.expect("No label provided") name=name.push_key("variant") value=selected_discriminant /> }.into_any()
470                                }
471                            }
472                        </div>
473                        
474                        // Variant-specific form
475                        <div class="variant">
476                            {move || {
477                                match selected_discriminant.get() {
478                                    #(#variant_forms)*
479                                }
480                            }}
481                        </div>
482                    </div>
483                }.into_any()
484            }
485        }
486    };
487
488    generated.into()
489}
490
491fn impl_form_for_struct(name: &syn::Ident, data_struct: &syn::DataStruct) -> TokenStream {
492    // Parse the struct data
493    let fields = match &data_struct.fields {
494        syn::Fields::Named(fields_named) => &fields_named.named,
495        _ => panic!("Form can only be derived for structs with named fields"),
496    };
497
498    // Use shared field processing logic
499    let field_signals = FieldProcessor::generate_field_signals(fields, None, None);
500    let field_signal_names = FieldProcessor::generate_field_signal_names(fields);
501    let field_constructor = FieldProcessor::generate_field_constructor(fields);
502    let field_forms = FieldProcessor::generate_field_forms(fields);
503    
504    let callback_effect = FieldProcessor::generate_callback_effect(
505        &field_signal_names,
506        ConstructorType::Struct {
507            name,
508            field_constructor: &field_constructor,
509        },
510    );
511
512    let generated = quote! {
513        impl Form for #name {
514            fn view(
515                field: formidable::FieldConfiguration,
516                name: formidable::Name,
517                value: Option<Self>,
518                callback: Option<leptos::prelude::Callback<Result<Self, formidable::FormError>>>,
519            ) -> impl leptos::prelude::IntoView {
520                use leptos::prelude::*;
521                
522                // Create signals for each field to track their state
523                #(#field_signals)*
524
525                #callback_effect
526
527                view! {
528                    <formidable::components::Section name=name heading={field.label}>
529                        {
530                            field.description.clone().map(|desc| view! {
531                                <p class="description">{desc.get()}</p>
532                            })
533                        }
534                        #(#field_forms)*
535                    </formidable::components::Section>
536                }.into_any()
537            }
538        }
539    };
540
541    generated.into()
542}