diff options
Diffstat (limited to 'rocie-macros/src/form/generate.rs')
| -rw-r--r-- | rocie-macros/src/form/generate.rs | 322 |
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),*] /> + } + } + } +} |
