1use proc_macro::TokenStream;
7use quote::quote;
8use syn::{Data, DeriveInput, Fields, ItemFn, Meta, parse_macro_input};
9
10#[proc_macro_derive(FormBuilder)]
34pub fn derive_form_builder(input: TokenStream) -> TokenStream {
35 let input = parse_macro_input!(input as DeriveInput);
36 let name = &input.ident;
37
38 let fields = match &input.data {
39 Data::Struct(data_struct) => match &data_struct.fields {
40 Fields::Named(fields) => &fields.named,
41 _ => {
42 return syn::Error::new_spanned(
43 input,
44 "FormBuilder can only be derived for structs with named fields",
45 )
46 .to_compile_error()
47 .into();
48 }
49 },
50 _ => {
51 return syn::Error::new_spanned(input, "FormBuilder can only be derived for structs")
52 .to_compile_error()
53 .into();
54 }
55 };
56
57 let field_views = fields.iter().map(|field| {
59 let field_name = field
60 .ident
61 .as_ref()
62 .expect("field should have an identifier");
63 let field_type = &field.ty;
64
65 let field_name_str = field_name.to_string();
67 let label_text = snake_to_title_case(&field_name_str);
68
69 let placeholder = field
71 .attrs
72 .iter()
73 .filter_map(|attr| {
74 if attr.path().is_ident("doc")
75 && let Meta::NameValue(meta) = &attr.meta
76 && let syn::Expr::Lit(expr_lit) = &meta.value
77 && let syn::Lit::Str(lit_str) = &expr_lit.lit
78 {
79 let doc = lit_str.value();
80 let cleaned = doc.trim();
82 if !cleaned.is_empty() {
83 return Some(cleaned.to_string());
84 }
85 }
86 None
87 })
88 .collect::<Vec<_>>()
89 .join(" ");
90
91 quote! {
94 <#field_type as crate::FormBuilder>::view(
95 &projected.#field_name,
96 ::waterui::AnyView::new(#label_text),
97 ::waterui::Str::from(#placeholder)
98 )
99 }
100 });
101
102 let requires_project = !fields.is_empty();
104
105 let view_body = if requires_project {
106 quote! {
107 let projected = <Self as ::waterui::reactive::project::Project>::project(binding);
109
110 ::waterui::component::stack::vstack((
112 #(#field_views,)*
113 ))
114 }
115 } else {
116 quote! {
118 ::waterui::component::stack::vstack(())
119 }
120 };
121
122 let field_types = fields.iter().map(|field| &field.ty);
123
124 let expanded = quote! {
126 impl crate::FormBuilder for #name {
127 type View = ::waterui::component::stack::VStack<((#(<#field_types as crate::FormBuilder>::View),*),)>;
128
129 fn view(binding: &::waterui::Binding<Self>, _label: ::waterui::AnyView, _placeholder: ::waterui::Str) -> Self::View {
130 #view_body
131 }
132 }
133 };
134
135 TokenStream::from(expanded)
136}
137
138fn snake_to_title_case(s: &str) -> String {
140 s.split('_')
141 .map(|word| {
142 let mut chars = word.chars();
143 chars.next().map_or_else(String::new, |first| {
144 first
145 .to_uppercase()
146 .chain(chars.as_str().to_lowercase().chars())
147 .collect()
148 })
149 })
150 .collect::<Vec<_>>()
151 .join(" ")
152}
153
154#[proc_macro_attribute]
201pub fn form(_args: TokenStream, input: TokenStream) -> TokenStream {
202 let input = parse_macro_input!(input as DeriveInput);
203 let _name = &input.ident;
204 let (_impl_generics, _ty_generics, _where_clause) = input.generics.split_for_impl();
205
206 let _fields = match &input.data {
208 Data::Struct(data_struct) => match &data_struct.fields {
209 Fields::Named(fields) => fields,
210 _ => {
211 return syn::Error::new_spanned(
212 input,
213 "The #[form] macro can only be applied to structs with named fields",
214 )
215 .to_compile_error()
216 .into();
217 }
218 },
219 _ => {
220 return syn::Error::new_spanned(
221 input,
222 "The #[form] macro can only be applied to structs",
223 )
224 .to_compile_error()
225 .into();
226 }
227 };
228
229 let expanded = quote! {
230 #[derive(Default, Clone, Debug, ::waterui::FormBuilder, ::waterui::Project)]
231 #input
232 };
233
234 TokenStream::from(expanded)
235}
236
237use syn::{Expr, LitStr, Token, Type, parse::Parse, punctuated::Punctuated};
238
239#[proc_macro_derive(Project)]
270pub fn derive_project(input: TokenStream) -> TokenStream {
271 let input = parse_macro_input!(input as DeriveInput);
272
273 match &input.data {
274 Data::Struct(data_struct) => match &data_struct.fields {
275 Fields::Named(fields_named) => derive_project_struct(&input, fields_named),
276 Fields::Unnamed(fields_unnamed) => derive_project_tuple_struct(&input, fields_unnamed),
277 Fields::Unit => derive_project_unit_struct(&input),
278 },
279 Data::Enum(_) => {
280 syn::Error::new_spanned(input, "Project derive macro does not support enums")
281 .to_compile_error()
282 .into()
283 }
284 Data::Union(_) => {
285 syn::Error::new_spanned(input, "Project derive macro does not support unions")
286 .to_compile_error()
287 .into()
288 }
289 }
290}
291
292fn derive_project_struct(input: &DeriveInput, fields: &syn::FieldsNamed) -> TokenStream {
293 let struct_name = &input.ident;
294 let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
295
296 let projected_struct_name =
298 syn::Ident::new(&format!("{struct_name}Projected"), struct_name.span());
299
300 let projected_fields = fields.named.iter().map(|field| {
302 let field_name = &field.ident;
303 let field_type = &field.ty;
304 quote! {
305 pub #field_name: ::waterui::reactive::Binding<#field_type>
306 }
307 });
308
309 let field_projections = fields.named.iter().map(|field| {
311 let field_name = &field.ident;
312 quote! {
313 #field_name: {
314 let source = source.clone();
315 ::waterui::reactive::Binding::mapping(
316 &source,
317 |value| value.#field_name.clone(),
318 move |binding, value| {
319 binding.get_mut().#field_name = value;
320 },
321 )
322 }
323 }
324 });
325
326 let mut generics_with_static = input.generics.clone();
328 for param in &mut generics_with_static.params {
329 if let syn::GenericParam::Type(type_param) = param {
330 type_param.bounds.push(syn::parse_quote!('static));
331 }
332 }
333 let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
334
335 let expanded = quote! {
336 #[derive(Debug)]
338 pub struct #projected_struct_name #ty_generics #where_clause {
339 #(#projected_fields,)*
340 }
341
342 impl #impl_generics_with_static ::waterui::reactive::project::Project for #struct_name #ty_generics #where_clause {
343 type Projected = #projected_struct_name #ty_generics;
344
345 fn project(source: &::waterui::reactive::Binding<Self>) -> Self::Projected {
346 #projected_struct_name {
347 #(#field_projections,)*
348 }
349 }
350 }
351 };
352
353 TokenStream::from(expanded)
354}
355
356fn derive_project_tuple_struct(input: &DeriveInput, fields: &syn::FieldsUnnamed) -> TokenStream {
357 let struct_name = &input.ident;
358 let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
359
360 let field_types: Vec<&Type> = fields.unnamed.iter().map(|field| &field.ty).collect();
362 let projected_tuple = if field_types.len() == 1 {
363 quote! { (::waterui::reactive::Binding<#(#field_types)*>,) }
364 } else {
365 quote! { (#(::waterui::reactive::Binding<#field_types>),*) }
366 };
367
368 let field_projections = fields.unnamed.iter().enumerate().map(|(index, _)| {
370 let idx = syn::Index::from(index);
371 quote! {
372 {
373 let source = source.clone();
374 ::waterui::reactive::Binding::mapping(
375 &source,
376 |value| value.#idx.clone(),
377 move |binding, value| {
378 binding.get_mut().#idx = value;
379 },
380 )
381 }
382 }
383 });
384
385 let mut generics_with_static = input.generics.clone();
387 for param in &mut generics_with_static.params {
388 if let syn::GenericParam::Type(type_param) = param {
389 type_param.bounds.push(syn::parse_quote!('static));
390 }
391 }
392 let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
393
394 let projection_tuple = if field_projections.len() == 1 {
395 quote! { (#(#field_projections)*,) }
396 } else {
397 quote! { (#(#field_projections),*) }
398 };
399
400 let expanded = quote! {
401 impl #impl_generics_with_static ::waterui::reactive::project::Project for #struct_name #ty_generics #where_clause {
402 type Projected = #projected_tuple;
403
404 fn project(source: &::waterui::reactive::Binding<Self>) -> Self::Projected {
405 #projection_tuple
406 }
407 }
408 };
409
410 TokenStream::from(expanded)
411}
412
413fn derive_project_unit_struct(input: &DeriveInput) -> TokenStream {
414 let struct_name = &input.ident;
415 let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
416
417 let mut generics_with_static = input.generics.clone();
419 for param in &mut generics_with_static.params {
420 if let syn::GenericParam::Type(type_param) = param {
421 type_param.bounds.push(syn::parse_quote!('static));
422 }
423 }
424 let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
425
426 let expanded = quote! {
427 impl #impl_generics_with_static ::waterui::reactive::project::Project for #struct_name #ty_generics #where_clause {
428 type Projected = ();
429
430 fn project(_source: &::waterui::reactive::Binding<Self>) -> Self::Projected {
431 ()
432 }
433 }
434 };
435
436 TokenStream::from(expanded)
437}
438
439struct SInput {
441 format_str: LitStr,
442 args: Punctuated<Expr, Token![,]>,
443}
444
445impl Parse for SInput {
446 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
447 let format_str: LitStr = input.parse()?;
448 let args = if input.peek(Token![,]) {
449 input.parse::<Token![,]>()?;
450 Punctuated::parse_terminated(input)?
451 } else {
452 Punctuated::new()
453 };
454
455 Ok(Self { format_str, args })
456 }
457}
458
459#[proc_macro]
479#[allow(clippy::similar_names, clippy::too_many_lines)]
480pub fn s(input: TokenStream) -> TokenStream {
481 let input = parse_macro_input!(input as SInput);
482 let format_str = input.format_str;
483 let format_value = format_str.value();
484
485 let (has_positional, has_named, positional_count, named_vars) =
487 analyze_format_string(&format_value);
488
489 if !input.args.is_empty() {
491 if has_named {
493 return syn::Error::new_spanned(
494 &format_str,
495 format!(
496 "Format string contains named arguments like {{{}}} but you provided positional arguments. \
497 Either use positional placeholders like {{}} or remove the explicit arguments to use automatic variable capture.",
498 named_vars.first().unwrap_or(&String::new())
499 )
500 )
501 .to_compile_error()
502 .into();
503 }
504
505 if positional_count != input.args.len() {
507 return syn::Error::new_spanned(
508 &format_str,
509 format!(
510 "Format string has {} positional placeholders but {} arguments were provided",
511 positional_count,
512 input.args.len()
513 ),
514 )
515 .to_compile_error()
516 .into();
517 }
518 let args: Vec<_> = input.args.iter().collect();
519 return match args.len() {
520 1 => {
521 let arg = &args[0];
522 quote! {
523 {
524 use ::waterui::reactive::SignalExt;
525 SignalExt::map(#arg.clone(), |arg| waterui::reactive::__format!(#format_str, arg))
526 }
527 }
528 .into()
529 }
530 2 => {
531 let arg1 = &args[0];
532 let arg2 = &args[1];
533 quote! {
534 {
535 use waterui::reactive::{SignalExt, zip::zip};
536 SignalExt::map(zip(#arg1.clone(), #arg2.clone()), |(arg1, arg2)| {
537 waterui::reactive::__format!(#format_str, arg1, arg2)
538 })
539 }
540 }
541 .into()
542 }
543 3 => {
544 let arg1 = &args[0];
545 let arg2 = &args[1];
546 let arg3 = &args[2];
547 quote! {
548 {
549 use ::waterui::reactive::{SignalExt, zip::zip};
550 SignalExt::map(
551 zip(zip(#arg1.clone(), #arg2.clone()), #arg3.clone()),
552 |((arg1, arg2), arg3)| waterui::reactive::__format!(#format_str, arg1, arg2, arg3)
553 )
554 }
555 }
556 .into()
557 }
558 4 => {
559 let arg1 = &args[0];
560 let arg2 = &args[1];
561 let arg3 = &args[2];
562 let arg4 = &args[3];
563 quote! {
564 {
565 use ::waterui::reactive::{SignalExt, zip::zip};
566 SignalExt::map(
567 zip(
568 zip(#arg1.clone(), #arg2.clone()),
569 zip(#arg3.clone(), #arg4.clone())
570 ),
571 |((arg1, arg2), (arg3, arg4))| waterui::reactive::__format!(#format_str, arg1, arg2, arg3, arg4)
572 )
573 }
574 }.into()
575 }
576 _ => syn::Error::new_spanned(format_str, "Too many arguments, maximum 4 supported")
577 .to_compile_error()
578 .into(),
579 };
580 }
581
582 if has_positional && has_named {
584 return syn::Error::new_spanned(
585 &format_str,
586 "Format string mixes positional {{}} and named {{var}} placeholders. \
587 Use either all positional with explicit arguments, or all named for automatic capture.",
588 )
589 .to_compile_error()
590 .into();
591 }
592
593 if has_positional && input.args.is_empty() {
595 return syn::Error::new_spanned(
596 &format_str,
597 format!(
598 "Format string has {positional_count} positional placeholder(s) {{}} but no arguments provided. \
599 Either provide arguments or use named placeholders like {{variable}} for automatic capture."
600 )
601 )
602 .to_compile_error()
603 .into();
604 }
605
606 let var_names = named_vars;
608
609 if var_names.is_empty() {
611 return quote! {
612 {
613 use ::waterui::reactive::constant;
614 constant(waterui::reactive::__format!(#format_str))
615 }
616 }
617 .into();
618 }
619
620 let var_idents: Vec<syn::Ident> = var_names
622 .iter()
623 .map(|name| syn::Ident::new(name, format_str.span()))
624 .collect();
625
626 match var_names.len() {
627 1 => {
628 let var = &var_idents[0];
629 quote! {
630 {
631 use ::waterui::reactive::SignalExt;
632 SignalExt::map(#var.clone(), |#var| {
633 waterui::reactive::__format!(#format_str)
634 })
635 }
636 }
637 .into()
638 }
639 2 => {
640 let var1 = &var_idents[0];
641 let var2 = &var_idents[1];
642 quote! {
643 {
644 use ::waterui::reactive::{SignalExt, zip::zip};
645 SignalExt::map(zip(#var1.clone(), #var2.clone()), |(#var1, #var2)| {
646 waterui::reactive::__format!(#format_str)
647 })
648 }
649 }
650 .into()
651 }
652 3 => {
653 let var1 = &var_idents[0];
654 let var2 = &var_idents[1];
655 let var3 = &var_idents[2];
656 quote! {
657 {
658 use ::waterui::reactive::{SignalExt, zip::zip};
659 SignalExt::map(
660 zip(zip(#var1.clone(), #var2.clone()), #var3.clone()),
661 |((#var1, #var2), #var3)| {
662 ::waterui::reactive::__format!(#format_str)
663 }
664 )
665 }
666 }
667 .into()
668 }
669 4 => {
670 let var1 = &var_idents[0];
671 let var2 = &var_idents[1];
672 let var3 = &var_idents[2];
673 let var4 = &var_idents[3];
674 quote! {
675 {
676 use ::waterui::reactive::{SignalExt, zip::zip};
677 SignalExt::map(
678 zip(
679 zip(#var1.clone(), #var2.clone()),
680 zip(#var3.clone(), #var4.clone())
681 ),
682 |((#var1, #var2), (#var3, #var4))| {
683 ::waterui::reactive::__format!(#format_str)
684 }
685 )
686 }
687 }
688 .into()
689 }
690 _ => syn::Error::new_spanned(format_str, "Too many named variables, maximum 4 supported")
691 .to_compile_error()
692 .into(),
693 }
694}
695
696fn analyze_format_string(format_str: &str) -> (bool, bool, usize, Vec<String>) {
698 let mut has_positional = false;
699 let mut has_named = false;
700 let mut positional_count = 0;
701 let mut named_vars = Vec::new();
702 let mut chars = format_str.chars().peekable();
703
704 while let Some(c) = chars.next() {
705 if c == '{' && chars.peek() == Some(&'{') {
706 chars.next();
708 } else if c == '{' {
709 let mut content = String::new();
710 let mut has_content = false;
711
712 while let Some(&next_char) = chars.peek() {
713 if next_char == '}' {
714 chars.next(); break;
716 } else if next_char == ':' {
717 chars.next(); while let Some(&spec_char) = chars.peek() {
720 if spec_char == '}' {
721 chars.next(); break;
723 }
724 chars.next();
725 }
726 break;
727 }
728 content.push(chars.next().unwrap());
729 has_content = true;
730 }
731
732 if !has_content || content.is_empty() {
734 has_positional = true;
736 positional_count += 1;
737 } else if content.chars().all(|ch| ch.is_ascii_digit()) {
738 has_positional = true;
740 positional_count += 1;
741 } else if content
742 .chars()
743 .next()
744 .is_some_and(|ch| ch.is_ascii_alphabetic() || ch == '_')
745 {
746 has_named = true;
748 if !named_vars.contains(&content) {
749 named_vars.push(content);
750 }
751 } else {
752 has_positional = true;
754 positional_count += 1;
755 }
756 }
757 }
758
759 (has_positional, has_named, positional_count, named_vars)
760}
761
762#[proc_macro_attribute]
804pub fn hot_reload(_args: TokenStream, input: TokenStream) -> TokenStream {
805 let input_fn = parse_macro_input!(input as ItemFn);
806
807 let fn_name = &input_fn.sig.ident;
808 let fn_vis = &input_fn.vis;
809 let fn_attrs = &input_fn.attrs;
810 let fn_sig = &input_fn.sig;
811 let fn_block = &input_fn.block;
812
813 let fn_name_str = fn_name.to_string();
815
816 let export_fn_name =
818 syn::Ident::new(&format!("waterui_hot_reload_{fn_name_str}"), fn_name.span());
819
820 if std::env::var("WATERUI_ENABLE_HOT_RELOAD").unwrap_or_default() != "1" {
821 let expanded = quote! {
823 #(#fn_attrs)*
824 #fn_vis #fn_sig #fn_block
825 };
826 return TokenStream::from(expanded);
827 }
828
829 let expanded = quote! {
830 #(#fn_attrs)*
831 #fn_vis #fn_sig {
832 ::waterui::debug::HotReloadView::new(
833 concat!(module_path!(), "::", #fn_name_str),
834 || #fn_block
835 )
836 }
837
838 #[cfg(waterui_hot_reload_lib)]
841 #[doc(hidden)]
842 #[unsafe(no_mangle)]
843 pub unsafe extern "C" fn #export_fn_name() -> *mut () {
844 let view = #fn_block;
845 Box::into_raw(Box::new(::waterui::AnyView::new(view))).cast()
846 }
847 };
848
849 TokenStream::from(expanded)
850}