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
13fn 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#[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 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 _ => {} }
80 }
81 }
82 },
83 Meta::NameValue(MetaNameValue { path, value, .. }) => {
84 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 _ => {} }
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
125struct FieldProcessor;
127
128impl FieldProcessor {
129 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 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 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 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 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 let all_fields_have_values = #(#field_signal_names.get().is_some())&&*;
241
242 if all_fields_have_values {
243 let all_ok = #(#field_signal_names.get().map(|r| r.is_ok()).unwrap_or(false))&&*;
245
246 if all_ok {
247 #constructor
249 } else {
250 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
266enum 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 let discriminant_name = quote::format_ident!("{}Discriminant", name);
294
295 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 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 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 quote! {
331 #discriminant_name::#variant_name => {
332 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 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 panic!("Multiple unnamed fields in enum variants not supported");
377 }
378 },
379 syn::Fields::Named(fields) => {
380 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 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 #[derive(Clone, Copy, Debug, PartialEq, Eq, strum::IntoStaticStr, strum::VariantArray, Default)]
439 enum #discriminant_name {
440 #[default]
441 #(#discriminant_variants),*
442 }
443
444 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 <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 <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 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 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 #(#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}