summaryrefslogtreecommitdiffstats
path: root/rocie-macros
diff options
context:
space:
mode:
Diffstat (limited to 'rocie-macros')
-rw-r--r--rocie-macros/.gitignore1
-rw-r--r--rocie-macros/Cargo.lock58
-rw-r--r--rocie-macros/Cargo.toml13
-rw-r--r--rocie-macros/src/form/generate.rs322
-rw-r--r--rocie-macros/src/form/mod.rs37
-rw-r--r--rocie-macros/src/form/parse.rs132
-rw-r--r--rocie-macros/src/lib.rs11
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)
+}