diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-10-05 18:27:05 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-10-05 18:27:05 +0200 |
| commit | 1fc165f2a5a3b6d77da2cfea2aa05e1db1c73577 (patch) | |
| tree | 2ea10b7aa960ecfa932b1091a43f101c5668cea8 /rocie-macros/src/form | |
| parent | feat(form): Provide basic form framework (diff) | |
| download | web-client-1fc165f2a5a3b6d77da2cfea2aa05e1db1c73577.zip | |
feat(form): Re-write the form macro as a proc macro
This allows more possibilities.
Diffstat (limited to 'rocie-macros/src/form')
| -rw-r--r-- | rocie-macros/src/form/generate.rs | 322 | ||||
| -rw-r--r-- | rocie-macros/src/form/mod.rs | 37 | ||||
| -rw-r--r-- | rocie-macros/src/form/parse.rs | 132 |
3 files changed, 491 insertions, 0 deletions
diff --git a/rocie-macros/src/form/generate.rs b/rocie-macros/src/form/generate.rs new file mode 100644 index 0000000..6673ba5 --- /dev/null +++ b/rocie-macros/src/form/generate.rs @@ -0,0 +1,322 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{Ident, 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 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 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 + )* + #( + #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 = |Inputs {#(#bounds),*}| #on_submit_block; + real_on_submit( + #output_struct_construction + ) + }; + + + view! { + <form class="flex flex-col contents-start m-2 g-2" on:submit=on_submit> + #( + #input_htmls + )* + + <div class="static"> + <input + type="submit" + value="Submit" + class="absolute bottom-0 right-0 h-20 w-20 rounded-lg bg-green-300 m-2" + /> + </div> + </form> + } + }); + + // { + // 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 { + let (name, ty) = match child { + ParsedChild::Input { name, .. } => (name, quote! {Input}), + ParsedChild::Select { name, .. } => (name, quote! {Select}), + }; + + 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! {}, + } +} +fn type_verification(child: &ParsedChild) -> TokenStream2 { + match child { + ParsedChild::Input { .. } => quote! {}, + ParsedChild::Select { + rust_type, options, .. + } => { + let names: Vec<_> = options + .0 + .iter() + .enumerate() + .map(|(index, _)| format_ident!("name_{index}")) + .collect(); + let values: Vec<_> = options.0.iter().map(|option| option.1.clone()).collect(); + + quote! { + { + #( + let #names: #rust_type = #values; + )* + } + } + } + } +} + +fn output_struct_definition(children: &[ParsedChild]) -> TokenStream2 { + let names = children.iter().map(|child| match child { + ParsedChild::Input { name, .. } => name, + ParsedChild::Select { name, .. } => name, + }); + let rust_types = children.iter().map(|child| match child { + ParsedChild::Input { rust_type, .. } => rust_type, + ParsedChild::Select { rust_type, .. } => rust_type, + }); + + quote! { + struct Inputs { + #( + #names: #rust_types + ),* + } + } +} +fn output_struct_construction(children: &[ParsedChild]) -> TokenStream2 { + let names = children.iter().map(|child| match child { + ParsedChild::Input { name, .. } => name, + ParsedChild::Select { name, .. } => name, + }); + + 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("<input> to exist") + .value(); + + let fin: Result<#rust_type, leptos::error::Error> = output + .parse() + .map_err(Into::<leptos::error::Error>::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("<input> to exist") + .value(); + + let fin: Result<#rust_type, leptos::error::Error> = output + .parse() + .map_err(Into::<leptos::error::Error>::into); + fin + }; + + match value { + Ok(ok) => { + ok + } , + Err(err) => { + unreachable!("Should be ruled out at compile time: {err}") + } + } + }; + } + } + } +} + +fn input_html(child: &ParsedChild) -> TokenStream2 { + match child { + ParsedChild::Input { + name, + label, + rust_type, + html_type, + .. + } => { + let node_ref_name = node_ref_name(name); + let (signal_name_get, _) = signal_names(name); + + quote! { + <InputPlaceholder input_type=#html_type label=#label node_ref=#node_ref_name /> + <Show + when=move || #signal_name_get.get().is_some() + fallback=|| () + > + <p class="ps-2 text-red-300">{move || + format!( + "Input is invalid for type {}: {}", + stringify!(#rust_type), + #signal_name_get.get().expect("Was `is_some`") + ) + }</p> + </Show> + } + } + ParsedChild::Select { + name, + label, + options, + .. + } => { + let node_ref_name = node_ref_name(name); + let options = options.0.iter().map(|(lit, expr)| { + quote! { + (#lit, #expr.to_string()) + } + }); + + quote! { + <SelectPlaceholder label=#label node_ref=#node_ref_name options=vec![#(#options),*] /> + } + } + } +} diff --git a/rocie-macros/src/form/mod.rs b/rocie-macros/src/form/mod.rs new file mode 100644 index 0000000..2719826 --- /dev/null +++ b/rocie-macros/src/form/mod.rs @@ -0,0 +1,37 @@ +use syn::{Expr, Ident, LitStr, Type}; + +mod parse; +mod generate; + +pub use generate::form_impl; + +#[derive(Debug)] +pub struct ParsedOnSubmit { + inputs: Vec<Ident>, + block: Expr, +} + +#[derive(Debug)] +pub enum ParsedChild { + Input { + name: Ident, + rust_type: Type, + html_type: LitStr, + label: LitStr, + }, + Select { + name: Ident, + label: LitStr, + rust_type: Type, + options: SelectOptions, + }, +} + +#[derive(Debug)] +pub struct SelectOptions(Vec<(LitStr, Expr)>); + +#[derive(Debug)] +pub struct ParsedInput { + on_submit: ParsedOnSubmit, + children: Vec<ParsedChild>, +} diff --git a/rocie-macros/src/form/parse.rs b/rocie-macros/src/form/parse.rs new file mode 100644 index 0000000..ef2087b --- /dev/null +++ b/rocie-macros/src/form/parse.rs @@ -0,0 +1,132 @@ +use quote::format_ident; +use syn::{bracketed, parenthesized, parse::{Parse, ParseStream}, Expr, Ident, LitStr, Token, Type}; + +use crate::form::{ParsedChild, ParsedInput, ParsedOnSubmit, SelectOptions}; + +macro_rules! parse_key_value { + ($input:expr, $name:ident as $ty:ty) => {{ + let key = $input.parse::<Ident>()?; + $input.parse::<Token![=]>()?; + let value = $input.parse::<$ty>()?; + + if key != format_ident!(stringify!($name)) { + panic!("Expected key name to be {}, but found: {key}", stringify!($name)); + } + + if $input.peek(Token![,]) { + $input.parse::<Token![,]>()?; + } + + value + }}; +} + +impl Parse for SelectOptions { + fn parse(input: ParseStream) -> syn::Result<Self> { + let content; + bracketed!(content in input); + let inner = content.parse_terminated( + |arg| { + let paren; + parenthesized!(paren in arg); + let lit = paren.parse::<LitStr>()?; + paren.parse::<Token![,]>()?; + let expr = paren.parse::<Expr>()?; + + Ok((lit, expr)) + }, + Token![,], + )?; + + Ok(Self(inner.into_iter().collect())) + } +} + +impl Parse for ParsedInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let on_submit = input.parse::<syn::Ident>()?; + + if on_submit != format_ident!("on_submit") { + panic!("Did not find correct `on_submit`: {on_submit}") + } + + input.parse::<Token![=]>()?; + + let on_submit = input.parse::<ParsedOnSubmit>()?; + + let mut children = Vec::new(); + while !input.is_empty() { + children.push(input.parse::<ParsedChild>()?); + } + + Ok(Self { + on_submit, + children, + }) + } +} + +impl Parse for ParsedChild { + fn parse(input: ParseStream) -> syn::Result<Self> { + input.parse::<Token![<]>()?; + + let variant = input.parse::<Ident>()?; + + let output = match variant { + variant if variant == format_ident!("Input") => { + let name = parse_key_value!(input, name as Ident); + let rust_type = parse_key_value!(input, rust_type as Type); + let html_type = parse_key_value!(input, html_type as LitStr); + let label = parse_key_value!(input, label as LitStr); + + ParsedChild::Input { + name, + rust_type, + html_type, + label, + } + } + variant if variant == format_ident!("Select") => { + let name = parse_key_value!(input, name as Ident); + let rust_type = parse_key_value!(input, rust_type as Type); + let label = parse_key_value!(input, label as LitStr); + let options = parse_key_value!(input, options as SelectOptions); + + ParsedChild::Select { + name, + label, + options, + rust_type, + } + } + _ => panic!("Unkown form child variant: {variant}"), + }; + + input.parse::<Token![/]>()?; + input.parse::<Token![>]>()?; + + Ok(output) + } +} + +impl Parse for ParsedOnSubmit { + fn parse(input: ParseStream) -> syn::Result<Self> { + let mut inputs = Vec::new(); + + input.parse::<Token![|]>()?; + while !input.peek(Token![|]) { + inputs.push(input.parse::<Ident>()?); + + if input.peek(Token![,]) { + input.parse::<Token![,]>()?; + } + } + input.parse::<Token![|]>()?; + + let block = input.parse::<Expr>()?; + + input.parse::<Token![;]>()?; + + Ok(Self { inputs, block }) + } +} |
