Write procedural macro to reduce boiler plate for actions
This commit is contained in:
12
macros/Cargo.toml
Normal file
12
macros/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "brittling_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
quote = "1.0.42"
|
||||
syn = "2.0.112"
|
||||
|
||||
126
macros/src/lib.rs
Normal file
126
macros/src/lib.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
// TODO: Clean this up
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{FnArg, ImplItem, ItemImpl, Pat, Type, parse_macro_input};
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let mut input = parse_macro_input!(item as ItemImpl);
|
||||
let struct_name = &input.self_ty;
|
||||
|
||||
// Use the attribute argument as the Enum name,
|
||||
// or fallback to the [Struct]Action logic if empty.
|
||||
let enum_name = if !attr.is_empty() {
|
||||
format_ident!("{}", attr.to_string().trim())
|
||||
} else {
|
||||
let struct_name_str = quote!(#struct_name).to_string().replace(" ", "");
|
||||
format_ident!("{}Action", struct_name_str)
|
||||
};
|
||||
|
||||
let mut enum_variants = Vec::new();
|
||||
let mut match_arms = Vec::new();
|
||||
|
||||
for item in &mut input.items {
|
||||
if let ImplItem::Fn(method) = item {
|
||||
// Check for #[action]
|
||||
let has_action = method
|
||||
.attrs
|
||||
.iter()
|
||||
.any(|attr| attr.path().is_ident("action"));
|
||||
|
||||
if has_action {
|
||||
// Remove the #[action] attribute so it doesn't cause compilation errors
|
||||
method.attrs.retain(|attr| !attr.path().is_ident("action"));
|
||||
|
||||
let method_name = &method.sig.ident;
|
||||
let variant_name = format_ident!(
|
||||
"{}",
|
||||
snake_to_pascal(&method_name.to_string())
|
||||
);
|
||||
|
||||
let mut variant_fields = Vec::new();
|
||||
let mut call_args = Vec::new();
|
||||
|
||||
for arg in method.sig.inputs.iter().skip(1) {
|
||||
// Skip 'self'
|
||||
if let FnArg::Typed(pat_type) = arg {
|
||||
if is_sender_type(&pat_type.ty) {
|
||||
call_args.push(quote!(tx));
|
||||
continue;
|
||||
}
|
||||
|
||||
let ty = &pat_type.ty;
|
||||
variant_fields.push(quote!(#ty));
|
||||
|
||||
if let Pat::Ident(pi) = &*pat_type.pat {
|
||||
let arg_ident = &pi.ident;
|
||||
call_args.push(quote!(#arg_ident));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if variant_fields.is_empty() {
|
||||
enum_variants.push(quote!(#variant_name));
|
||||
match_arms.push(quote! {
|
||||
#enum_name::#variant_name => self.#method_name(tx)
|
||||
});
|
||||
} else {
|
||||
enum_variants
|
||||
.push(quote!(#variant_name(#(#variant_fields),*)));
|
||||
// Extracting bindings for the match arm: Enum::Var(a, b) -> self.method(a, b, tx)
|
||||
let pattern_args = (0..variant_fields.len())
|
||||
.map(|i| format_ident!("arg_{}", i));
|
||||
let pattern_args_clone = pattern_args.clone();
|
||||
|
||||
match_arms.push(quote! {
|
||||
#enum_name::#variant_name(#(#pattern_args),*) => self.#method_name(#(#pattern_args_clone,)* tx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let expanded = quote! {
|
||||
#input
|
||||
|
||||
#[derive(::core::clone::Clone, ::core::fmt::Debug)]
|
||||
pub enum #enum_name {
|
||||
#(#enum_variants),*
|
||||
}
|
||||
|
||||
impl crate::app::common::Component<#enum_name> for #struct_name {
|
||||
fn handle_action(
|
||||
&mut self,
|
||||
action: #enum_name,
|
||||
tx: &'static ::tokio::sync::mpsc::UnboundedSender<crate::app::Action>,
|
||||
) -> ::core::result::Result<(), ::tokio::sync::mpsc::error::SendError<crate::app::Action>> {
|
||||
match action {
|
||||
#(#match_arms),*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
fn snake_to_pascal(s: &str) -> String {
|
||||
s.split('_')
|
||||
.map(|word| {
|
||||
let mut chars = word.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(f) => {
|
||||
f.to_uppercase().collect::<String>() + chars.as_str()
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Helper to detect if an argument is the Sender to inject it from handle_action
|
||||
fn is_sender_type(ty: &Type) -> bool {
|
||||
let s = quote!(#ty).to_string();
|
||||
s.contains("UnboundedSender")
|
||||
}
|
||||
Reference in New Issue
Block a user