jaeb_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{FnArg, ItemFn, PatType, Type, parse_macro_input};
4
5#[proc_macro_attribute]
6pub fn event_listener(_attr: TokenStream, item: TokenStream) -> TokenStream {
7    let input_fn = parse_macro_input!(item as ItemFn);
8
9    // Validate signature: exactly 1 argument
10    let args = &input_fn.sig.inputs;
11    if args.len() != 1 {
12        return syn::Error::new_spanned(&input_fn.sig.inputs, "#[event_listener] functions must take exactly one argument (event)")
13            .to_compile_error()
14            .into();
15    }
16
17    let is_async = input_fn.sig.asyncness.is_some();
18
19    // Extract arg pattern and type
20    let (arg_pat, arg_ty, is_ref): (Box<syn::Pat>, Box<Type>, bool) = match args.first().unwrap() {
21        FnArg::Typed(PatType { pat, ty, .. }) => {
22            let is_ref = matches!(**ty, Type::Reference(_));
23            (pat.clone(), ty.clone(), is_ref)
24        }
25        other => {
26            return syn::Error::new_spanned(other, "unsupported argument kind").to_compile_error().into();
27        }
28    };
29
30    // Determine event type (remove reference if present)
31    let event_ty: Box<Type> = match *arg_ty {
32        Type::Reference(ref tr) => tr.elem.clone(),
33        ref t => Box::new(t.clone()),
34    };
35
36    // Generate a unique registrar name based on function ident
37    let fn_ident = &input_fn.sig.ident;
38    let registrar_ident = format_ident!("__jaeb_register_{}", fn_ident);
39
40    // Build registration body depending on async/sync and ref-ness
41    let register_body = if is_async {
42        // async listener: require owned param; we will call subscribe_async
43        if is_ref {
44            // If user supplies &E for async, this would require Clone to move into async closure; disallow for now to keep semantics clear.
45            syn::Error::new_spanned(
46                &input_fn.sig.inputs,
47                "async #[event_listener] must take the event by value (e.g., e: MyEvent)",
48            )
49            .to_compile_error()
50        } else {
51            quote! {
52                // subscribe as async listener
53                bus.subscribe_async(|#arg_pat: #event_ty| async move {
54                    #fn_ident(#arg_pat).await;
55                }).await;
56            }
57        }
58    } else {
59        // sync listener: must accept &E
60        if !is_ref {
61            return syn::Error::new_spanned(
62                &input_fn.sig.inputs,
63                "sync #[event_listener] must take the event by reference (e.g., e: &MyEvent)",
64            )
65            .to_compile_error()
66            .into();
67        }
68        quote! {
69            bus.subscribe_sync::<#event_ty, _>(|#arg_pat| {
70                #fn_ident(#arg_pat);
71            }).await;
72        }
73    };
74
75    // Registrar function type: fn(&EventBus) -> Pin<Box<dyn Future<Output=()> + Send>>
76    let expanded = quote! {
77        #input_fn
78
79        #[doc(hidden)]
80        fn #registrar_ident(bus: &::jaeb::EventBus) -> ::jaeb::RegistrarFuture<'_> {
81            Box::pin(async move {
82                #register_body
83            })
84        }
85
86        // Submit to global inventory in jaeb crate
87        ::jaeb::_private::inventory::submit!(::jaeb::ListenerRegistrar { func: #registrar_ident });
88    };
89
90    expanded.into()
91}
92
93#[proc_macro]
94pub fn bootstrap_listeners(input: TokenStream) -> TokenStream {
95    // Expect a single expression representing the bus argument
96    // e.g., bootstrap_listeners!(bus) or bootstrap_listeners!(&bus)
97    let stream = proc_macro2::TokenStream::from(input);
98    let expanded = quote! {
99        ::jaeb::bootstrap_listeners(#stream).await
100    };
101    TokenStream::from(expanded)
102}