teloxide_plugins_macros/
lib.rs

1#![allow(non_snake_case)]
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{parse_macro_input, ItemFn, Meta, MetaNameValue, Expr, ExprArray, ExprLit, Lit, punctuated::Punctuated, Token, parse::Parse, parse::ParseStream, LitStr};
6use proc_macro2;
7
8const COMMANDS_IDENT: &str = "commands";
9const PREFIXES_IDENT: &str = "prefixes";
10const REGEX_IDENT: &str = "regex";
11const CALLBACK_IDENT: &str = "callback";
12
13struct PluginArgs {
14    metas: Punctuated<Meta, Token![,]>,
15}
16
17impl Parse for PluginArgs {
18    fn parse(input: ParseStream) -> syn::Result<Self> {
19        Ok(PluginArgs {
20            metas: Punctuated::parse_terminated(input)?,
21        })
22    }
23}
24
25fn extract_strings_from_array(expr: &Expr) -> syn::Result<Vec<String>> {
26    match expr {
27        Expr::Array(ExprArray { elems, .. }) => {
28            let mut strings = Vec::new();
29            for elem in elems {
30                match elem {
31                    Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => {
32                        strings.push(lit_str.value());
33                    }
34                    _ => {
35                        return Err(syn::Error::new_spanned(
36                            elem,
37                            "expected string literal in array"
38                        ));
39                    }
40                }
41            }
42            Ok(strings)
43        }
44        _ => {
45            Err(syn::Error::new_spanned(
46                expr,
47                "expected array of string literals"
48            ))
49        }
50    }
51}
52
53fn create_optional_string_literal(value: Option<&String>) -> proc_macro2::TokenStream {
54    value.map(|s| {
55        let lit_str = LitStr::new(s, proc_macro2::Span::call_site());
56        quote! { Some(#lit_str) }
57    }).unwrap_or_else(|| quote! { None })
58}
59
60fn parse_plugin_args(args: TokenStream) -> syn::Result<(Vec<String>, Vec<String>, Option<String>, Option<String>)> {
61    let mut commands = Vec::new();
62    let mut prefixes = Vec::new();
63    let mut regex = None;
64    let mut callback_filter = None;
65
66    let plugin_args: PluginArgs = syn::parse(args)?;
67    
68    for meta in plugin_args.metas {
69        if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta {
70            if let Some(ident) = path.get_ident() {
71                match ident.to_string().as_str() {
72                    COMMANDS_IDENT => {
73                        commands = extract_strings_from_array(&value)?;
74                    }
75                    PREFIXES_IDENT => {
76                        prefixes = extract_strings_from_array(&value)?;
77                    }
78                    REGEX_IDENT => {
79                        let patterns = extract_strings_from_array(&value)?;
80                        if !patterns.is_empty() {
81                            if patterns.len() == 1 {
82                                regex = Some(patterns[0].clone());
83                            } else {
84                                let combined_pattern = patterns.join("|");
85                                regex = Some(combined_pattern);
86                            }
87                        }
88                    }
89                    CALLBACK_IDENT => {
90                        let patterns = extract_strings_from_array(&value)?;
91                        if !patterns.is_empty() {
92                            if patterns.len() == 1 {
93                                callback_filter = Some(patterns[0].clone());
94                            } else {
95                                let combined_pattern = patterns.join("|");
96                                callback_filter = Some(combined_pattern);
97                            }
98                        }
99                    }
100                    _ => {}
101                }
102            }
103        }
104    }
105    
106    Ok((commands, prefixes, regex, callback_filter))
107}
108
109fn determine_handler_type(commands: &[String], prefixes: &[String], regex: &Option<String>, callback_filter: &Option<String>) -> syn::Result<bool> {
110    let has_message_triggers = !commands.is_empty() || !prefixes.is_empty() || regex.is_some();
111    let has_callback_triggers = callback_filter.is_some();
112    
113    match (has_message_triggers, has_callback_triggers) {
114        (true, true) => {
115            Err(syn::Error::new(
116                proc_macro2::Span::call_site(),
117                "plugin cannot handle both message triggers (commands/prefixes/regex) and callback triggers simultaneously"
118            ))
119        }
120        (true, false) => Ok(false),  
121        (false, true) => Ok(true),  
122        (false, false) => {
123            Err(syn::Error::new(
124                proc_macro2::Span::call_site(),
125                "plugin must specify at least one trigger: commands, prefixes, regex, or callback"
126            ))
127        }
128    }
129}
130
131fn create_callback_handler(fn_name: &syn::Ident, is_callback: bool) -> proc_macro2::TokenStream {
132    if is_callback {
133        quote! {
134            |ctx| Box::pin(async move {
135                if let Some(cq) = ctx.callback_query {
136                    #fn_name(ctx.bot, cq).await;
137                }
138            })
139        }
140    } else {
141        quote! {
142            |ctx| Box::pin(async move {
143                if let Some(msg) = ctx.message {
144                    #fn_name(ctx.bot, msg).await;
145                }
146            })
147        }
148    }
149}
150
151#[proc_macro_attribute]
152pub fn TeloxidePlugin(args: TokenStream, input: TokenStream) -> TokenStream {
153    let input_fn = parse_macro_input!(input as ItemFn);
154    
155    let fn_name = &input_fn.sig.ident;
156    let fn_name_str = fn_name.to_string();
157    let ctor_fn_name = syn::Ident::new(&format!("{}_ctor", fn_name_str), fn_name.span());
158    let static_name = syn::Ident::new(&format!("{}_meta", fn_name_str), fn_name.span());
159    let vis = &input_fn.vis;
160    let sig = &input_fn.sig;
161    let block = &input_fn.block;
162
163    let (commands, prefixes, regex, callback_filter) = match parse_plugin_args(args) {
164        Ok(result) => result,
165        Err(err) => return err.to_compile_error().into(),
166    };
167
168    let is_callback_handler = match determine_handler_type(&commands, &prefixes, &regex, &callback_filter) {
169        Ok(is_callback) => is_callback,
170        Err(err) => return err.to_compile_error().into(),
171    };
172
173    let commands_lit = commands.iter().map(|c| LitStr::new(c, proc_macro2::Span::call_site()));
174    let prefixes_lit = prefixes.iter().map(|p| LitStr::new(p, proc_macro2::Span::call_site()));
175    let regex_lit = create_optional_string_literal(regex.as_ref());
176    let callback_filter_lit = create_optional_string_literal(callback_filter.as_ref());
177    
178    let callback_handler = create_callback_handler(fn_name, is_callback_handler);
179
180    let expanded = quote! {
181        #vis #sig #block
182
183        #[allow(non_upper_case_globals)]
184        #[doc(hidden)]
185        static #static_name: &teloxide_plugins::registry::PluginMeta = &teloxide_plugins::registry::PluginMeta {
186            name: #fn_name_str,
187            commands: &[#(#commands_lit),*],
188            prefixes: &[#(#prefixes_lit),*],
189            regex: #regex_lit,
190            callback_filter: #callback_filter_lit,
191            callback: #callback_handler,
192        };
193
194        #[ctor::ctor]
195        fn #ctor_fn_name() {
196            teloxide_plugins::registry::register_plugin(#static_name);
197        }
198    };
199
200    TokenStream::from(expanded)
201}