summary refs log tree commit diff stats
path: root/rocie-macros/src/form/generate.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rocie-macros/src/form/generate.rs')
-rw-r--r--rocie-macros/src/form/generate.rs322
1 files changed, 322 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),*] />
+            }
+        }
+    }
+}