summary refs log tree commit diff stats
path: root/rocie-macros/src/form
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-10-23 01:36:39 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-10-23 01:36:39 +0200
commit7bff22756beec82b4a1470e2d325b706dc56e5f2 (patch)
tree1566965125cfd5fbd73d654e9ee6ca8256301411 /rocie-macros/src/form
parentfeat(form): Re-write the form macro as a proc macro (diff)
downloadweb-client-7bff22756beec82b4a1470e2d325b706dc56e5f2.zip
feat(buy): Provide basic buy interface
Diffstat (limited to 'rocie-macros/src/form')
-rw-r--r--rocie-macros/src/form/generate.rs255
-rw-r--r--rocie-macros/src/form/mod.rs11
-rw-r--r--rocie-macros/src/form/parse.rs93
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 })
     }
 }