use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::{Ident, Type, parse_macro_input}; use crate::form::{ParsedChild, ParsedInput}; pub fn form_impl(item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as ParsedInput); let node_refs = input.children.iter().map(node_ref); let signals = input.children.iter().map(signal); let reactive_signals = input.children.iter().map(reactive_signal); let auto_complete_signals = input.children.iter().map(auto_complete_signal); let type_verifications = input.children.iter().map(type_verification); let output_struct_definition = output_struct_definition(&input.children); let output_struct_construction = output_struct_construction(&input.children); let fetch_values = input.children.iter().map(fetch_value); let bounds = input.on_submit.inputs; let on_submit_block = input.on_submit.block; let on_submit_move = if input.on_submit.should_use_move { quote! {move} } else { quote! {} }; let input_htmls = input.children.iter().map(input_html); let output = quote!({ use crate::components::{ input_placeholder::InputPlaceholder, select_placeholder::SelectPlaceholder }; use leptos::{ view, prelude::{ Get, NodeRef, ElementChild, ClassAttribute, OnAttribute, signal, Set, Show, }, html::{Input, Select}, web_sys::SubmitEvent }; use log::{info, error}; #( #node_refs #signals #reactive_signals #auto_complete_signals #type_verifications )* let on_submit = move |ev: SubmitEvent| { #output_struct_definition // stop the page from reloading! ev.prevent_default(); #( #fetch_values )* let real_on_submit = #on_submit_move |Inputs {#(#bounds),*}| #on_submit_block; real_on_submit( #output_struct_construction ) }; view! {
#( #input_htmls )*
} }); // { // match syn::parse_file(quote! {fn main() { #output }}.to_string().as_str()) { // Ok(tree) => { // let formatted = prettyplease::unparse(&tree); // eprint!("{}", formatted); // } // Err(err) => { // eprintln!("Error: {err}\n{output}"); // } // }; // } output.into() } fn node_ref_name(name: &Ident) -> Ident { format_ident!("{name}_node_ref") } fn node_ref(child: &ParsedChild) -> TokenStream2 { if let ParsedChild::Show { children, .. } = child { children.iter().map(node_ref).collect() } else { let (name, ty) = match child { ParsedChild::Input { name, .. } => (name, quote! {Input}), ParsedChild::Select { name, .. } => (name, quote! {Select}), ParsedChild::Show { .. } => unreachable!("Filtered out before"), }; let node_ref_name = node_ref_name(name); quote! { let #node_ref_name: NodeRef<#ty> = NodeRef::new(); } } } fn signal_names(name: &Ident) -> (Ident, Ident) { (format_ident!("{name}_get"), format_ident!("{name}_set")) } fn signal(child: &ParsedChild) -> TokenStream2 { match child { ParsedChild::Input { name, .. } => { let (signal_name_get, signal_name_set) = signal_names(name); quote! { let (#signal_name_get, #signal_name_set) = signal(None); } } ParsedChild::Select { .. } => quote! {}, ParsedChild::Show { children, .. } => children.iter().map(signal).collect(), } } fn signal_auto_complete_names(name: &Ident) -> (Ident, Ident) { ( format_ident!("{name}_auto_complete_get"), format_ident!("{name}_auto_complete_set"), ) } fn auto_complete_signal(child: &ParsedChild) -> TokenStream2 { match child { ParsedChild::Input { name, reactive, auto_complete, .. } => { if let Some(auto_complete) = auto_complete { let (signal_auto_complete_get, _) = signal_auto_complete_names(name); let reactive = reactive .as_ref() .expect("Must be some, if auto_complete is some"); quote! { let #signal_auto_complete_get = leptos::prelude::LocalResource::new( move || #auto_complete(#reactive()) ); } } else { quote! {} } } ParsedChild::Select { .. } => quote! {}, ParsedChild::Show { children, .. } => children.iter().map(auto_complete_signal).collect(), } } fn signal_reactive_base_names(name: &Ident) -> (Ident, Ident) { ( format_ident!("{name}_reactive_base_get"), format_ident!("{name}_reactive_base_set"), ) } fn reactive_signal(child: &ParsedChild) -> TokenStream2 { match child { ParsedChild::Input { name, reactive, rust_type, .. } => { if let Some(reactive) = reactive { let (signal_reactive_base_get, signal_reactive_base_set) = signal_reactive_base_names(name); quote! { let (#signal_reactive_base_get, #signal_reactive_base_set) = signal(None); #reactive = move || { { let output: Option = #signal_reactive_base_get.get(); if let Some(output) = output { let fin: Result<#rust_type, leptos::error::Error> = output .parse() .map_err(Into::::into); match fin { Ok(ok) => { Some(ok) }, Err(err) => { // Reset the signal None } } } else { None } } }; } } else { quote! {} } } ParsedChild::Select { .. } => quote! {}, ParsedChild::Show { children, .. } => children.iter().map(reactive_signal).collect(), } } fn type_verification(child: &ParsedChild) -> TokenStream2 { match child { ParsedChild::Input { .. } => { quote! {} } ParsedChild::Select { name, options, .. } => { let name = format_ident!("_select_val_{name}"); quote! { { let #name: leptos::prelude::LocalResource< Result< // TODO: Use #rust_type instead of the second String <2025-10-21> Vec<(String, String)>, leptos::error::Error > > = #options; } } } ParsedChild::Show { when: _, children } => { let base = children .iter() .map(type_verification) .collect::(); quote! { #base } } } } fn get_names(input: &ParsedChild) -> Vec<&Ident> { match input { ParsedChild::Input { name, .. } => vec![name], ParsedChild::Select { name, .. } => vec![name], ParsedChild::Show { children, .. } => children.iter().flat_map(get_names).collect(), } } fn output_struct_definition(children: &[ParsedChild]) -> TokenStream2 { fn get_rust_types(input: &ParsedChild) -> Vec<&Type> { match input { ParsedChild::Input { rust_type, .. } => vec![rust_type], ParsedChild::Select { rust_type, .. } => vec![rust_type], ParsedChild::Show { children, .. } => { children.iter().flat_map(get_rust_types).collect() } } } let names = children.iter().flat_map(get_names); let rust_types = children.iter().flat_map(get_rust_types); quote! { struct Inputs { #( #names: #rust_types ),* } } } fn output_struct_construction(children: &[ParsedChild]) -> TokenStream2 { let names = children.iter().flat_map(get_names); quote! { Inputs { #( #names ),* } } } fn fetch_value(child: &ParsedChild) -> TokenStream2 { match child { ParsedChild::Input { name, rust_type, .. } => { let node_ref_name = node_ref_name(name); let (_, signal_name_set) = signal_names(name); quote! { let #name: #rust_type = { let value = { let output = #node_ref_name .get() // event handlers can only fire after the view // is mounted to the DOM, so the `NodeRef` will be `Some` .expect(" to exist") .value(); let fin: Result<#rust_type, leptos::error::Error> = output .parse() .map_err(Into::::into); fin }; match value { Ok(ok) => { // Reset the signal #signal_name_set.set(None); ok } , Err(err) => { #signal_name_set.set(Some(err)); // Skip running the real `on_submit` return } } }; } } ParsedChild::Select { name, rust_type, .. } => { let node_ref_name = node_ref_name(name); quote! { let #name: #rust_type = { let value = { let output = #node_ref_name .get() // event handlers can only fire after the view // is mounted to the DOM, so the `NodeRef` will be `Some` .expect(" to exist") .value(); let fin: Result<#rust_type, leptos::error::Error> = output .parse() .map_err(Into::::into); fin }; match value { Ok(ok) => { ok } , Err(err) => { unreachable!("Should be ruled out at compile time: {err}") } } }; } } ParsedChild::Show { children, .. } => { children.iter().map(fetch_value).collect::() } } } fn input_html(child: &ParsedChild) -> TokenStream2 { match child { ParsedChild::Input { name, label, rust_type, html_type, reactive, auto_complete, .. } => { let node_ref_name = node_ref_name(name); let (signal_name_get, _) = signal_names(name); let reactive = { if reactive.is_some() { let (_, signal_reactive_set) = signal_reactive_base_names(name); quote! { reactive=Some(#signal_reactive_set) } } else { quote! {} } }; let auto_complete = { if auto_complete.is_some() { let (signal_auto_complete_get, _) = signal_auto_complete_names(name); quote! { auto_complete=Some(#signal_auto_complete_get) } } else { quote! {} } }; quote! {

{move || format!( "Input is invalid for type {}: {}", stringify!(#rust_type), #signal_name_get.get().expect("Was `is_some`") ) }

} } ParsedChild::Select { name, label, options, .. } => { let node_ref_name = node_ref_name(name); quote! { } } ParsedChild::Show { when, children } => { let rendered_children = children.iter().map(input_html).collect::>(); quote! { #( #rendered_children )* } } } }