diff options
Diffstat (limited to 'rocie-macros')
| -rw-r--r-- | rocie-macros/.gitignore | 1 | ||||
| -rw-r--r-- | rocie-macros/Cargo.lock | 58 | ||||
| -rw-r--r-- | rocie-macros/Cargo.toml | 13 | ||||
| -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 | ||||
| -rw-r--r-- | rocie-macros/src/lib.rs | 11 |
7 files changed, 574 insertions, 0 deletions
diff --git a/rocie-macros/.gitignore b/rocie-macros/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/rocie-macros/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rocie-macros/Cargo.lock b/rocie-macros/Cargo.lock new file mode 100644 index 0000000..dbce2cb --- /dev/null +++ b/rocie-macros/Cargo.lock @@ -0,0 +1,58 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rocie-macros" +version = "0.1.0" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" diff --git a/rocie-macros/Cargo.toml b/rocie-macros/Cargo.toml new file mode 100644 index 0000000..01e9e5c --- /dev/null +++ b/rocie-macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rocie-macros" +version = "0.1.0" +edition = "2024" + +[dependencies] +proc-macro2 = "1.0.101" +quote = "1.0.41" +syn = {version = "2.0.106", features = []} +prettyplease = "0.2" + +[lib] +proc-macro = true 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 }) + } +} diff --git a/rocie-macros/src/lib.rs b/rocie-macros/src/lib.rs new file mode 100644 index 0000000..4fb4340 --- /dev/null +++ b/rocie-macros/src/lib.rs @@ -0,0 +1,11 @@ +use proc_macro::TokenStream; + +use crate::form::form_impl; + +mod form; + +#[proc_macro] +#[allow(non_snake_case)] +pub fn Form(input: TokenStream) -> TokenStream { + form_impl(input) +} |
