1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use proc_macro_error::{abort, proc_macro_error};
5use quote::quote;
6use syn::{FnArg, parse_macro_input, spanned::Spanned};
7
8macro_rules! emit {
11    ($tokens:expr) => {{
12        use proc_macro2_diagnostics::SpanDiagnosticExt;
13        let mut tokens = $tokens;
14        if std::env::var_os("RUNBOT_CODEGEN_DEBUG").is_some() {
15            let debug_tokens = proc_macro2::Span::call_site()
16                .note("emitting RUNBOT_CODEGEN_DEBUG code generation debug output")
17                .note(tokens.to_string())
18                .emit_as_item_tokens();
19            tokens.extend(debug_tokens);
20        }
21        tokens.into()
22    }};
23}
24
25#[proc_macro_error]
26#[proc_macro_attribute]
27pub fn message_processor(_args: TokenStream, input: TokenStream) -> TokenStream {
28    common_processor(
29        _args,
30        input,
31        Box::new(syn::parse_quote!(Arc<Message>)),
32        quote! {MessageProcessor},
33        quote! {process_message},
34    )
35}
36
37#[proc_macro_error]
38#[proc_macro_attribute]
39pub fn notice_processor(_args: TokenStream, input: TokenStream) -> TokenStream {
40    common_processor(
41        _args,
42        input,
43        Box::new(syn::parse_quote!(Arc<Notice>)),
44        quote! {NoticeProcessor},
45        quote! {process_notice},
46    )
47}
48
49fn common_processor(
50    _args: TokenStream,
51    input: TokenStream,
52    mtp: Box<syn::Type>,
53    trait_name: proc_macro2::TokenStream,
54    trait_fn_name: proc_macro2::TokenStream,
55) -> TokenStream {
56    let method = parse_macro_input!(input as syn::ItemFn);
57    let method_clone = method.clone();
58    if method.sig.asyncness.is_none() {
59        abort!(&method.sig.span(), "method must be async");
60    }
61
62    let sig_params = &method.sig.inputs;
66    if sig_params.len() != 2 {
67        abort!(&method.sig.span(), "method must have 2 parameters");
68    }
69
70    let first_param = &sig_params[0];
72    let t = match first_param {
73        FnArg::Receiver(_) => {
74            abort!(&first_param.span(), "first parameter must be a parameter");
75        }
76        FnArg::Typed(t) => t,
77    };
78    let first_param_type = &t.ty;
79    if first_param_type != &syn::parse_quote!(Arc<BotContext>) {
80        abort!(
81            &first_param.span(),
82            "first parameter must be Arc<BotContext>"
83        );
84    }
85
86    let second_param = &sig_params[1];
88    let second_param_type = match second_param {
89        FnArg::Receiver(_) => {
90            abort!(&second_param.span(), "second parameter must be a parameter");
91        }
92        FnArg::Typed(t) => t,
93    };
94    let second_param_type = &second_param_type.ty;
95    if second_param_type != &mtp {
96        abort!(&second_param.span(), "second parameter must be &Message");
97    }
98
99    let vis = method.vis;
100    let asyncness = method.sig.asyncness;
101    let fn_name = method.sig.ident.clone();
102    let block = &method.block;
103    let return_type = &method.sig.output;
104    let struct_name = snake_to_upper_camel(&fn_name.to_string());
105    let struct_name = proc_macro2::Ident::new(&struct_name, proc_macro2::Span::call_site());
106    let static_name = to_upper_snake(&fn_name.to_string());
107    let static_name = proc_macro2::Ident::new(&static_name, proc_macro2::Span::call_site());
108
109    emit!(quote::quote! {
110        #[derive(Copy, Clone, Default, Debug)]
111        #vis struct #struct_name;
112
113        #[::runbot::re_export::async_trait::async_trait]
114        impl #trait_name for #struct_name {
115            #asyncness fn #trait_fn_name(&self, #first_param, #second_param) #return_type #block
116        }
117
118        #[allow(non_upper_case_globals)]
119        #vis static #static_name: #struct_name = #struct_name;
120
121        #method_clone
122    })
123}
124
125fn snake_to_upper_camel(s: &str) -> String {
126    s.split('_')
127        .filter(|part| !part.is_empty())
128        .map(|part| {
129            let mut chars = part.chars();
130            match chars.next() {
131                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
132                None => String::new(),
133            }
134        })
135        .collect::<String>()
136}
137
138fn to_upper_snake(s: &str) -> String {
139    s.split('_')
140        .filter(|part| !part.is_empty())
141        .map(|part| part.to_uppercase())
142        .collect::<Vec<String>>()
143        .join("_")
144}