Skip to content

Commit 29280a2

Browse files
committed
feat(macros): add handler proc-macro attribute decorators
- add procedural macros for handler types: new_message, message_edited, message_deleted, callback_query, inline_query and inline_send - macros accept optional filter expressions as attribute arguments - add proc-macro2 and serde dependencies to ferogram-macros - enable macros feature by default in ferogram - export UpdateType enum and handler::Result type publicly - fix has_url filter to strip trailing slashes and prevent duplicates - fix NotFilter to properly convert stopped flow to proceed - add CommandParams to prelude exports
1 parent fb57834 commit 29280a2

11 files changed

Lines changed: 351 additions & 13 deletions

File tree

ferogram-macros/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ proc-macro = true
1616
[dependencies]
1717
syn = "2.0"
1818
quote = "1.0"
19+
serde = { version = "1.0", features = ["derive"] }
20+
proc-macro2 = "1.0"

ferogram-macros/src/handler.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2024-2026 - Andriel Ferreira
2+
//
3+
// Licensed under the MIT license <LICENSE or https://opensource.org/licenses/MIT>.
4+
// This file may not be copied, modified, or distributed except according to those terms.
5+
6+
//! Handler builder macros.
7+
8+
use proc_macro::TokenStream;
9+
use proc_macro2::TokenStream as TokenStream2;
10+
use quote::quote;
11+
use serde::Serialize;
12+
use syn::{Expr, ItemFn};
13+
14+
/// Build a new handler.
15+
pub fn new_handler(update_type: UpdateType, filters: Option<Expr>, input: ItemFn) -> TokenStream {
16+
let ItemFn {
17+
attrs,
18+
vis,
19+
sig,
20+
block,
21+
} = input;
22+
23+
let name = sig.ident.clone();
24+
let inputs = sig.inputs.clone();
25+
let filter = filters.map_or(quote! { ::ferogram::filter::always }, |expr| {
26+
transform_filter(&expr)
27+
});
28+
29+
let handler = match update_type {
30+
UpdateType::NewMessage => quote! { ::ferogram::handler::new_message },
31+
UpdateType::MessageEdited => quote! { ::ferogram::handler::message_edited },
32+
UpdateType::MessageDeleted => quote! { ::ferogram::handler::message_deleted },
33+
UpdateType::CallbackQuery => quote! { ::ferogram::handler::callback_query },
34+
UpdateType::InlineQuery => quote! { ::ferogram::handler::inline_query },
35+
UpdateType::InlineSend => quote! { ::ferogram::handler::inline_send },
36+
UpdateType::Raw => quote! { ::ferogram::handler::new_raw },
37+
};
38+
39+
quote! {
40+
#(#attrs)*
41+
#vis fn #name() -> ::ferogram::Handler {
42+
43+
#handler(#filter)
44+
.then(|#inputs| async move {
45+
#block
46+
})
47+
}
48+
}
49+
.into()
50+
}
51+
52+
/// Recursively transforms macro filters syntax into ferogram actual filters syntax.
53+
fn transform_filter(expr: &Expr) -> TokenStream2 {
54+
match expr {
55+
// Handle parenthereses `(A || B) && C`.
56+
Expr::Paren(paren) => {
57+
let inner = transform_filter(&paren.expr);
58+
quote! { (#inner) }
59+
}
60+
// Handle unary operations (!).
61+
Expr::Unary(unary) => {
62+
// Recursively transform the inner expression first
63+
let inner = transform_filter(&unary.expr);
64+
65+
match unary.op {
66+
// Convert `!private` -> `private.not()`
67+
syn::UnOp::Not(_) => quote! { #inner.not() },
68+
// Fallback
69+
_ => quote! { #unary.op #inner },
70+
}
71+
}
72+
// Handle binary operations (&&, ||).
73+
Expr::Binary(bin) => {
74+
// Recursively transform the left and right sides.
75+
let left = transform_filter(&bin.left);
76+
let right = transform_filter(&bin.right);
77+
78+
match bin.op {
79+
// Convert `left && right` -> `left.and(right)`.
80+
syn::BinOp::And(_) => quote! { #left.and(#right) },
81+
// Convert `left || right` -> `left.or(right)`.
82+
syn::BinOp::Or(_) => quote! { #left.or(#right) },
83+
// Fallback
84+
_ => quote! { #left #bin.op #right },
85+
}
86+
}
87+
_ => quote! {#expr },
88+
}
89+
}
90+
91+
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
92+
pub enum UpdateType {
93+
NewMessage,
94+
MessageEdited,
95+
MessageDeleted,
96+
CallbackQuery,
97+
InlineQuery,
98+
InlineSend,
99+
#[default]
100+
Raw,
101+
}

ferogram-macros/src/lib.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,139 @@
1+
// Copyright 2024-2026 - Andriel Ferreira
2+
//
3+
// Licensed under the MIT license <LICENSE or https://opensource.org/licenses/MIT>.
4+
// This file may not be copied, modified, or distributed except according to those terms.
15

6+
//! General helper macros exports.
7+
8+
mod handler;
9+
10+
use proc_macro::TokenStream;
11+
use syn::{Expr, ItemFn};
12+
13+
use crate::handler::{UpdateType, new_handler};
14+
15+
/// Build a new `NewMessage` handler.
16+
///
17+
/// Note: `always` is the default filter; it will be used if no filter is specified.
18+
///
19+
/// # Examples
20+
///
21+
/// ```ignore
22+
/// use ferogram::prelude::*;
23+
/// #[handler::new_message(command("/start :id?"))]
24+
/// async fn start(message: Message) -> Result<()> {
25+
/// Ok(())
26+
/// }
27+
/// ```
28+
#[proc_macro_attribute]
29+
pub fn new_message(attr: TokenStream, input: TokenStream) -> TokenStream {
30+
let filters = syn::parse::<Expr>(attr).ok();
31+
let input = syn::parse_macro_input!(input as ItemFn);
32+
33+
new_handler(UpdateType::NewMessage, filters, input)
34+
}
35+
36+
/// Build a new `MessageEdited` handler.
37+
///
38+
/// Note: `always` is the default filter; it will be used if no filter is specified.
39+
///
40+
/// # Examples
41+
///
42+
/// ```ignore
43+
/// use ferogram::prelude::*;
44+
/// #[handler::message_edited(command("/start :id?"))]
45+
/// async fn start(message: Message) -> Result<()> {
46+
/// Ok(())
47+
/// }
48+
/// ```
49+
#[proc_macro_attribute]
50+
pub fn message_edited(attr: TokenStream, input: TokenStream) -> TokenStream {
51+
let filters = syn::parse::<Expr>(attr).ok();
52+
let input = syn::parse_macro_input!(input as ItemFn);
53+
54+
new_handler(UpdateType::MessageEdited, filters, input)
55+
}
56+
57+
/// Build a new `MessageDeleted` handler.
58+
///
59+
/// Note: `always` is the default filter; it will be used if no filter is specified.
60+
///
61+
/// # Examples
62+
///
63+
/// ```ignore
64+
/// use ferogram::prelude::*;
65+
/// #[handler::message_deleted]
66+
/// async fn start(message: Message) -> Result<()> {
67+
/// Ok(())
68+
/// }
69+
/// ```
70+
#[proc_macro_attribute]
71+
pub fn message_deleted(attr: TokenStream, input: TokenStream) -> TokenStream {
72+
let filters = syn::parse::<Expr>(attr).ok();
73+
let input = syn::parse_macro_input!(input as ItemFn);
74+
75+
new_handler(UpdateType::MessageDeleted, filters, input)
76+
}
77+
78+
/// Build a new `CallbackQuery` handler.
79+
///
80+
/// Note: `always` is the default filter; it will be used if no filter is specified.
81+
///
82+
/// # Examples
83+
///
84+
/// ```ignore
85+
/// use ferogram::prelude::*;
86+
/// #[handler::callback_query(command("start :id?"))]
87+
/// async fn start(query: CallbackQuery) -> Result<()> {
88+
/// Ok(())
89+
/// }
90+
/// ```
91+
#[proc_macro_attribute]
92+
pub fn callback_query(attr: TokenStream, input: TokenStream) -> TokenStream {
93+
let filters = syn::parse::<Expr>(attr).ok();
94+
let input = syn::parse_macro_input!(input as ItemFn);
95+
96+
new_handler(UpdateType::CallbackQuery, filters, input)
97+
}
98+
99+
/// Build a new `InlineQuery` handler.
100+
///
101+
/// Note: `always` is the default filter; it will be used if no filter is specified.
102+
///
103+
/// # Examples
104+
///
105+
/// ```ignore
106+
/// use ferogram::prelude::*;
107+
/// #[handler::inline_query(command("images *query?"))]
108+
/// async fn start(query: InlineQuery) -> Result<()> {
109+
/// Ok(())
110+
/// }
111+
/// ```
112+
#[proc_macro_attribute]
113+
pub fn inline_query(attr: TokenStream, input: TokenStream) -> TokenStream {
114+
let filters = syn::parse::<Expr>(attr).ok();
115+
let input = syn::parse_macro_input!(input as ItemFn);
116+
117+
new_handler(UpdateType::InlineQuery, filters, input)
118+
}
119+
120+
/// Build a new `InlineSend` handler.
121+
///
122+
/// Note: `always` is the default filter; it will be used if no filter is specified.
123+
///
124+
/// # Examples
125+
///
126+
/// ```ignore
127+
/// use ferogram::prelude::*;
128+
/// #[handler::inline_send(command("images *query?"))]
129+
/// async fn start(query: InlineQuery) -> Result<()> {
130+
/// Ok(())
131+
/// }
132+
/// ```
133+
#[proc_macro_attribute]
134+
pub fn inline_send(attr: TokenStream, input: TokenStream) -> TokenStream {
135+
let filters = syn::parse::<Expr>(attr).ok();
136+
let input = syn::parse_macro_input!(input as ItemFn);
137+
138+
new_handler(UpdateType::InlineSend, filters, input)
139+
}

ferogram/examples/dispatcher.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//!
77
//! Run it as:
88
//! ```sh
9-
//! API_ID=... API_HASH="..." BOT_TOKEN="..." PHONE_NUMBER="..." cargo run --example from-env
9+
//! API_ID=... API_HASH="..." BOT_TOKEN="..." PHONE_NUMBER="..." cargo run --example dispatcher
1010
//! ```
1111
1212
use std::error::Error;
@@ -23,6 +23,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
2323

2424
// Build and run the dispatcher.
2525
Dispatcher::builder()
26+
// You can try it by sending:
27+
// * `/start 123`: returns id
28+
// * `/start hi`: returns invalid id message
2629
.add_handler(handler::new_message(filter::command("/start :id")).then(
2730
|message: Message, params: CommandParams| async move {
2831
let Ok(id) = params.get_parsed::<i64>("id") else {

ferogram/src/di.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,22 +116,22 @@ impl Resource {
116116

117117
pub trait RequestHandler: Send + Sync + 'static {
118118
/// Handle the request.
119-
fn handle(&mut self, injector: Injector) -> BoxFuture<'_, crate::Result<()>>;
119+
fn handle(&mut self, injector: Injector) -> BoxFuture<'_, crate::handler::Result>;
120120
}
121121

122122
macro_rules! impl_request_handler {
123123
($($param:ident),*) => {
124124
impl<Fut, Output, $($param),*> RequestHandler for RequestHandlerFunc<($($param,)*), Fut>
125125
where
126126
Fut: FnMut($($param),*) -> Output + Send + Sync + 'static,
127-
Output: Future<Output = crate::Result<()>> + Send,
127+
Output: Future<Output = crate::handler::Result> + Send,
128128
$($param: Clone + Send + Sync + 'static,)*
129129
{
130130
#[inline]
131131
#[allow(unused_mut)]
132132
#[allow(non_snake_case)]
133133
#[allow(unused_variables)]
134-
fn handle(&mut self, mut injector: Injector) -> BoxFuture<'_, crate::Result<()>> {
134+
fn handle(&mut self, mut injector: Injector) -> BoxFuture<'_, crate::handler::Result> {
135135
$(
136136
let $param = Borrow::<$param>::borrow(match injector.take() {
137137
Some(ref value) => value,

ferogram/src/filter/default.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,13 @@ pub fn has_url(_: Client, update: Update) -> Flow {
219219
.skip(entity.offset() as usize)
220220
.take(entity.length() as usize)
221221
.collect::<String>();
222-
urls.push(url);
222+
let url = url
223+
.strip_suffix('/')
224+
.map_or_else(|| url.clone(), ToString::to_string);
225+
226+
if !urls.contains(&url) {
227+
urls.push(url);
228+
}
223229
}
224230
}
225231

@@ -230,6 +236,9 @@ pub fn has_url(_: Client, update: Update) -> Flow {
230236
for part in text.split_whitespace() {
231237
if let Ok(url) = Url::parse(part) {
232238
let url = url.to_string();
239+
let url = url
240+
.strip_suffix('/')
241+
.map_or_else(|| url.clone(), ToString::to_string);
233242

234243
if !urls.contains(&url) {
235244
urls.push(url);

ferogram/src/filter/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//! Filters traits and functions intended to be used in [`crate::Handler`]s
77
88
mod and;
9-
mod default;
9+
pub(crate) mod default;
1010
mod markers;
1111
mod not;
1212
mod or;

ferogram/src/filter/not.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ impl Filter for NotFilter {
2020
let update = update.clone();
2121

2222
Box::pin(async move {
23-
let flow = self.filter.run(&client, &update).await;
24-
25-
if flow.is_stop() { flow } else { super::stop() }
23+
let mut flow = self.filter.run(&client, &update).await;
24+
25+
if flow.is_stop() {
26+
flow.to_proceed();
27+
flow
28+
} else {
29+
super::stop()
30+
}
2631
})
2732
}
2833
}

0 commit comments

Comments
 (0)