diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-10-23 01:36:39 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-10-23 01:36:39 +0200 |
| commit | 7bff22756beec82b4a1470e2d325b706dc56e5f2 (patch) | |
| tree | 1566965125cfd5fbd73d654e9ee6ca8256301411 /rocie-macros/src | |
| parent | feat(form): Re-write the form macro as a proc macro (diff) | |
| download | web-client-7bff22756beec82b4a1470e2d325b706dc56e5f2.zip | |
feat(buy): Provide basic buy interface
Diffstat (limited to 'rocie-macros/src')
| -rw-r--r-- | rocie-macros/src/form/generate.rs | 255 | ||||
| -rw-r--r-- | rocie-macros/src/form/mod.rs | 11 | ||||
| -rw-r--r-- | rocie-macros/src/form/parse.rs | 93 |
3 files changed, 276 insertions, 83 deletions
diff --git a/rocie-macros/src/form/generate.rs b/rocie-macros/src/form/generate.rs index 6673ba5..5642e6a 100644 --- a/rocie-macros/src/form/generate.rs +++ b/rocie-macros/src/form/generate.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; -use syn::{Ident, parse_macro_input}; +use syn::{Ident, Type, parse_macro_input}; use crate::form::{ParsedChild, ParsedInput}; @@ -10,6 +10,8 @@ pub fn form_impl(item: TokenStream) -> TokenStream { 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); @@ -20,6 +22,11 @@ pub fn form_impl(item: TokenStream) -> TokenStream { 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); @@ -50,11 +57,11 @@ pub fn form_impl(item: TokenStream) -> TokenStream { #( #node_refs - )* - #( + #signals - )* - #( + #reactive_signals + #auto_complete_signals + #type_verifications )* @@ -68,7 +75,7 @@ pub fn form_impl(item: TokenStream) -> TokenStream { #fetch_values )* - let real_on_submit = |Inputs {#(#bounds),*}| #on_submit_block; + let real_on_submit = #on_submit_move |Inputs {#(#bounds),*}| #on_submit_block; real_on_submit( #output_struct_construction ) @@ -76,16 +83,17 @@ pub fn form_impl(item: TokenStream) -> TokenStream { view! { - <form class="flex flex-col contents-start m-2 g-2" on:submit=on_submit> + <form class="relative flex flex-col contents-start m-2 g-2" on:submit=on_submit> #( #input_htmls )* - <div class="static"> + <div class="sticky bottom-2 left-0 flex flex-row contents-start"> + <div class="w-5/6" /> <input type="submit" value="Submit" - class="absolute bottom-0 right-0 h-20 w-20 rounded-lg bg-green-300 m-2" + class="pr-2 pl-2 rounded-lg bg-green-300 m-2" /> </div> </form> @@ -112,15 +120,20 @@ fn node_ref_name(name: &Ident) -> Ident { } fn node_ref(child: &ParsedChild) -> TokenStream2 { - let (name, ty) = match child { - ParsedChild::Input { name, .. } => (name, quote! {Input}), - ParsedChild::Select { name, .. } => (name, quote! {Select}), - }; + 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); + let node_ref_name = node_ref_name(name); - quote! { - let #node_ref_name: NodeRef<#ty> = NodeRef::new(); + quote! { + let #node_ref_name: NodeRef<#ty> = NodeRef::new(); + } } } @@ -137,42 +150,152 @@ fn signal(child: &ParsedChild) -> TokenStream2 { } } ParsedChild::Select { .. } => quote! {}, + ParsedChild::Show { children, .. } => children.iter().map(signal).collect(), } } -fn type_verification(child: &ParsedChild) -> TokenStream2 { + +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 { .. } => quote! {}, - ParsedChild::Select { - rust_type, options, .. + ParsedChild::Input { + name, + reactive, + auto_complete, + .. } => { - 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(); + 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<String> = #signal_reactive_base_get.get(); + + if let Some(output) = output { + let fin: Result<#rust_type, leptos::error::Error> = output + .parse() + .map_err(Into::<leptos::error::Error>::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 #names: #rust_type = #values; - )* + 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::<TokenStream2>(); + + 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 { - 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, - }); + 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 { @@ -183,10 +306,7 @@ fn output_struct_definition(children: &[ParsedChild]) -> TokenStream2 { } } fn output_struct_construction(children: &[ParsedChild]) -> TokenStream2 { - let names = children.iter().map(|child| match child { - ParsedChild::Input { name, .. } => name, - ParsedChild::Select { name, .. } => name, - }); + let names = children.iter().flat_map(get_names); quote! { Inputs { @@ -270,6 +390,9 @@ fn fetch_value(child: &ParsedChild) -> TokenStream2 { }; } } + ParsedChild::Show { children, .. } => { + children.iter().map(fetch_value).collect::<TokenStream2>() + } } } @@ -280,13 +403,39 @@ fn input_html(child: &ParsedChild) -> TokenStream2 { 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! { - <InputPlaceholder input_type=#html_type label=#label node_ref=#node_ref_name /> + <InputPlaceholder #reactive #auto_complete input_type=#html_type label=#label node_ref=#node_ref_name /> + <Show when=move || #signal_name_get.get().is_some() fallback=|| () @@ -308,14 +457,22 @@ fn input_html(child: &ParsedChild) -> TokenStream2 { .. } => { 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),*] /> + <SelectPlaceholder label=#label node_ref=#node_ref_name options=#options /> + } + } + ParsedChild::Show { when, children } => { + let rendered_children = children.iter().map(input_html).collect::<Vec<_>>(); + + quote! { + <Show + when=#when + > + #( + #rendered_children + )* + </Show> } } } diff --git a/rocie-macros/src/form/mod.rs b/rocie-macros/src/form/mod.rs index 2719826..7abd8f6 100644 --- a/rocie-macros/src/form/mod.rs +++ b/rocie-macros/src/form/mod.rs @@ -1,7 +1,7 @@ use syn::{Expr, Ident, LitStr, Type}; -mod parse; mod generate; +mod parse; pub use generate::form_impl; @@ -9,6 +9,7 @@ pub use generate::form_impl; pub struct ParsedOnSubmit { inputs: Vec<Ident>, block: Expr, + pub(crate) should_use_move: bool, } #[derive(Debug)] @@ -18,19 +19,19 @@ pub enum ParsedChild { rust_type: Type, html_type: LitStr, label: LitStr, + reactive: Option<Ident>, + auto_complete: Option<Ident>, }, Select { name: Ident, label: LitStr, rust_type: Type, - options: SelectOptions, + options: Expr, // Vec<(String, Uuid)> }, + Show { when: Expr, children: Vec<ParsedChild> }, } #[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 index ef2087b..b0ca58c 100644 --- a/rocie-macros/src/form/parse.rs +++ b/rocie-macros/src/form/parse.rs @@ -1,7 +1,10 @@ use quote::format_ident; -use syn::{bracketed, parenthesized, parse::{Parse, ParseStream}, Expr, Ident, LitStr, Token, Type}; +use syn::{ + Expr, Ident, LitStr, Token, Type, + parse::{Parse, ParseStream}, +}; -use crate::form::{ParsedChild, ParsedInput, ParsedOnSubmit, SelectOptions}; +use crate::form::{ParsedChild, ParsedInput, ParsedOnSubmit}; macro_rules! parse_key_value { ($input:expr, $name:ident as $ty:ty) => {{ @@ -19,27 +22,13 @@ macro_rules! parse_key_value { 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())) - } + (@option $input:expr, $name:ident as $ty:ty) => {{ + if $input.peek(Ident) { + Some(parse_key_value!($input, $name as $ty)) + } else { + None + } + }}; } impl Parse for ParsedInput { @@ -78,19 +67,33 @@ impl Parse for ParsedChild { 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); + let reactive = parse_key_value!(@option input, reactive as Ident); + let auto_complete = parse_key_value!(@option input, auto_complete as Ident); + + if auto_complete.is_some() && reactive.is_none() { + panic!("Cannot provide an autocomplet, without registering an reactive signal") + } + + input.parse::<Token![/]>()?; + input.parse::<Token![>]>()?; ParsedChild::Input { name, rust_type, html_type, label, + reactive, + auto_complete, } } 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); + let options = parse_key_value!(input, options as Expr); + + input.parse::<Token![/]>()?; + input.parse::<Token![>]>()?; ParsedChild::Select { name, @@ -99,11 +102,36 @@ impl Parse for ParsedChild { rust_type, } } - _ => panic!("Unkown form child variant: {variant}"), - }; + variant if variant == format_ident!("Show") => { + let when = parse_key_value!(input, when as Expr); - input.parse::<Token![/]>()?; - input.parse::<Token![>]>()?; + input.parse::<Token![>]>()?; + + let children = { + let mut children = vec![]; + + while !(input.peek(Token![<]) && input.peek2(Token![/])) { + children.push(input.parse::<ParsedChild>()?); + } + + { + input.parse::<Token![<]>()?; + input.parse::<Token![/]>()?; + let show_ident = input.parse::<Ident>()?; + + if show_ident != format_ident!("Show") { + panic!("Expected key name to be 'Show', but found: '{show_ident}'",); + } + input.parse::<Token![>]>()?; + + children + } + }; + + ParsedChild::Show { when, children } + } + _ => panic!("Unknown form child variant: {variant}"), + }; Ok(output) } @@ -113,6 +141,13 @@ impl Parse for ParsedOnSubmit { fn parse(input: ParseStream) -> syn::Result<Self> { let mut inputs = Vec::new(); + let should_use_move = if input.peek(Token![move]) { + input.parse::<Token![move]>()?; + true + } else { + false + }; + input.parse::<Token![|]>()?; while !input.peek(Token![|]) { inputs.push(input.parse::<Ident>()?); @@ -127,6 +162,6 @@ impl Parse for ParsedOnSubmit { input.parse::<Token![;]>()?; - Ok(Self { inputs, block }) + Ok(Self { inputs, block, should_use_move }) } } |
