teloxide_plugins_macros/
lib.rs1#![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, ®ex, &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}