Skip to main content

wowlab_engine_macros_impl/
spec_handler_delegation.rs

1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use syn::{FnArg, ItemTrait, Pat, ReturnType, TraitItem, Type, visit_mut::VisitMut as _};
4
5const TYPES_CRATE_TYPES: &[&str] = &[
6    "ActorId",
7    "EnemyIdx",
8    "GroupId",
9    "PositionedActorRef",
10    "SimTime",
11    "SpatialTransform",
12    "SpellIdx",
13    "WaveId",
14];
15
16fn qualify_path_root(path: &mut syn::Path) {
17    if path.leading_colon.is_some() || path.segments.len() != 1 {
18        return;
19    }
20
21    let ident = path.segments[0].ident.to_string();
22
23    if TYPES_CRATE_TYPES.contains(&ident.as_str()) {
24        let original = path.segments[0].clone();
25
26        *path = syn::parse_quote!(::wowlab_types::sim::#original);
27    } else if !matches!(
28        ident.as_str(),
29        "bool"
30            | "dyn"
31            | "f32"
32            | "f64"
33            | "Fn"
34            | "FnMut"
35            | "FnOnce"
36            | "i8"
37            | "i16"
38            | "i32"
39            | "i64"
40            | "i128"
41            | "isize"
42            | "Option"
43            | "Result"
44            | "Self"
45            | "String"
46            | "u8"
47            | "u16"
48            | "u32"
49            | "u64"
50            | "u128"
51            | "usize"
52            | "Vec"
53    ) {
54        let original = path.segments[0].clone();
55
56        *path = syn::parse_quote!(::wowlab_engine_ports::#original);
57    }
58}
59
60struct TypeQualifier;
61
62impl syn::visit_mut::VisitMut for TypeQualifier {
63    fn visit_path_mut(&mut self, i: &mut syn::Path) {
64        syn::visit_mut::visit_path_mut(self, i);
65        qualify_path_root(i);
66    }
67}
68
69fn qualify_type(ty: &mut Type) {
70    TypeQualifier.visit_type_mut(ty);
71}
72
73fn delegated_method(item: &syn::TraitItemFn) -> syn::Result<TokenStream> {
74    let mut signature = item.sig.clone();
75    let mut arguments = Vec::with_capacity(signature.inputs.len());
76
77    for input in &mut signature.inputs {
78        match input {
79            FnArg::Receiver(_) => {}
80            FnArg::Typed(argument) => {
81                qualify_type(&mut argument.ty);
82
83                let Pat::Ident(pattern) = argument.pat.as_ref() else {
84                    return Err(syn::Error::new_spanned(
85                        &argument.pat,
86                        "SpecHandler delegation requires identifier argument patterns",
87                    ));
88                };
89
90                arguments.push(quote!(#pattern));
91            }
92        }
93    }
94
95    if let ReturnType::Type(_, output) = &mut signature.output {
96        qualify_type(output);
97    }
98
99    let method = &signature.ident;
100
101    Ok(quote! {
102        #signature {
103            self.$inner.#method(#(#arguments),*)
104        }
105    })
106}
107
108pub(crate) fn expand_spec_handler_delegation(
109    _attribute: TokenStream,
110    mut item: ItemTrait,
111) -> syn::Result<TokenStream> {
112    let mut helpers = Vec::with_capacity(item.items.len());
113    let mut helper_calls = Vec::with_capacity(item.items.len());
114
115    for trait_item in &item.items {
116        let TraitItem::Fn(method) = trait_item else {
117            continue;
118        };
119
120        let name = &method.sig.ident;
121        let helper = format_ident!("__wowlab_delegate_spec_handler_{name}");
122        let delegated = delegated_method(method)?;
123
124        helpers.push(quote! {
125            #[doc(hidden)]
126            #[macro_export]
127            macro_rules! #helper {
128                ($inner:ident; #name $(, $rest:ident)*) => {};
129                ($inner:ident; $other:ident $(, $rest:ident)*) => {
130                    $crate::#helper!($inner; $($rest),*);
131                };
132                ($inner:ident;) => {
133                    #delegated
134                };
135            }
136        });
137        helper_calls.push(quote! {
138            $crate::#helper!($inner; $($skip),*);
139        });
140    }
141
142    item.attrs
143        .retain(|attr| !attr.path().is_ident("spec_handler_delegation"));
144
145    Ok(quote! {
146        #item
147
148        #(#helpers)*
149
150        /// Forward every `SpecHandler` method not named in `skip` to an inner handler field.
151        #[doc(hidden)]
152        #[macro_export]
153        macro_rules! delegate_spec_handler {
154            ($inner:ident; skip [$($skip:ident),* $(,)?]) => {
155                #(#helper_calls)*
156            };
157        }
158    })
159}
160
161#[cfg(test)]
162mod tests {
163    use googletest::prelude::*;
164    use syn::parse_quote;
165
166    use super::*;
167
168    #[gtest]
169    fn trait_methods_drive_forwarder_signatures() -> Result<()> {
170        let item: ItemTrait = parse_quote! {
171            pub trait SpecHandler {
172                fn on_event(&mut self, event: Event, ctx: &mut SimContext);
173                fn total_damage(&self) -> f64;
174            }
175        };
176
177        let output = expand_spec_handler_delegation(TokenStream::new(), item)?.to_string();
178
179        verify_that!(
180            output,
181            contains_substring("fn on_event (& mut self , event : :: wowlab_engine_ports :: Event")
182        )?;
183        verify_that!(output, contains_substring("self . $ inner . on_event"))?;
184
185        verify_that!(
186            output,
187            contains_substring("macro_rules ! delegate_spec_handler")
188        )
189    }
190}