Skip to main content

botkit_core/
handler.rs

1use std::future::Future;
2use std::marker::PhantomData;
3use std::pin::Pin;
4use std::sync::Arc;
5
6use crate::context::Context;
7use crate::extractor::FromContext;
8use crate::responder::IntoResponse;
9use crate::response::Response;
10
11#[cfg(not(target_arch = "wasm32"))]
12type HandlerCallFuture<'a> = Pin<Box<dyn Future<Output = Response> + Send + 'a>>;
13#[cfg(target_arch = "wasm32")]
14type HandlerCallFuture<'a> = Pin<Box<dyn Future<Output = Response> + 'a>>;
15
16#[cfg(not(target_arch = "wasm32"))]
17pub trait HandlerCallFutureBounds: Future + Send {}
18#[cfg(not(target_arch = "wasm32"))]
19impl<T: Future + Send + ?Sized> HandlerCallFutureBounds for T {}
20
21#[cfg(target_arch = "wasm32")]
22pub trait HandlerCallFutureBounds: Future {}
23#[cfg(target_arch = "wasm32")]
24impl<T: Future + ?Sized> HandlerCallFutureBounds for T {}
25
26#[cfg(not(target_arch = "wasm32"))]
27pub trait HandlerBounds: Send + Sync {}
28#[cfg(not(target_arch = "wasm32"))]
29impl<T: Send + Sync + ?Sized> HandlerBounds for T {}
30
31#[cfg(target_arch = "wasm32")]
32pub trait HandlerBounds {}
33#[cfg(target_arch = "wasm32")]
34impl<T: ?Sized> HandlerBounds for T {}
35
36#[cfg(not(target_arch = "wasm32"))]
37pub trait HandlerFnBounds: Send + Sync {}
38#[cfg(not(target_arch = "wasm32"))]
39impl<T: Send + Sync + ?Sized> HandlerFnBounds for T {}
40
41#[cfg(target_arch = "wasm32")]
42pub trait HandlerFnBounds {}
43#[cfg(target_arch = "wasm32")]
44impl<T: ?Sized> HandlerFnBounds for T {}
45
46#[cfg(not(target_arch = "wasm32"))]
47pub trait HandlerFutureBounds: Future + Send {}
48#[cfg(not(target_arch = "wasm32"))]
49impl<T: Future + Send + ?Sized> HandlerFutureBounds for T {}
50
51#[cfg(target_arch = "wasm32")]
52pub trait HandlerFutureBounds: Future {}
53#[cfg(target_arch = "wasm32")]
54impl<T: Future + ?Sized> HandlerFutureBounds for T {}
55
56/// Trait for bot event handlers
57///
58/// Handlers use the extractor/responder pattern - they extract typed
59/// data from context and return types that implement IntoResponse.
60///
61/// # Example
62/// ```ignore
63/// // No parameters
64/// async fn ping() -> &'static str {
65///     "Pong!"
66/// }
67///
68/// // With extractors
69/// async fn greet(user: User) -> String {
70///     format!("Hello, {}!", user.name)
71/// }
72///
73/// // Multiple extractors
74/// async fn info(user: User, channel: Channel) -> String {
75///     format!("User {} in channel {}", user.name, channel.id)
76/// }
77///
78/// // Full context access when needed
79/// async fn advanced(ctx: Context) -> Response {
80///     // ... complex logic
81///     Response::text("Done")
82/// }
83/// ```
84pub trait Handler: HandlerBounds + 'static {
85    /// Handle the event and produce a response
86    fn call(&self, ctx: Context) -> impl HandlerCallFutureBounds<Output = Response> + '_;
87}
88
89/// Object-safe twin of [`Handler`] backing [`AnyHandler`].
90trait HandlerImpl: HandlerBounds {
91    fn call_boxed<'a>(&'a self, ctx: Context) -> HandlerCallFuture<'a>;
92}
93
94impl<T: Handler> HandlerImpl for T {
95    fn call_boxed<'a>(&'a self, ctx: Context) -> HandlerCallFuture<'a> {
96        Box::pin(Handler::call(self, ctx))
97    }
98}
99
100/// Type-erased [`Handler`] handle
101///
102/// What the router stores and dispatches on. Cloning shares the underlying
103/// handler.
104#[derive(Clone)]
105pub struct AnyHandler {
106    inner: Arc<dyn HandlerImpl>,
107}
108
109impl AnyHandler {
110    /// Erase `handler` into a shareable handle.
111    pub fn new(handler: impl Handler) -> Self {
112        Self {
113            inner: Arc::new(handler),
114        }
115    }
116
117    /// Handle the event and produce a response
118    pub fn call(&self, ctx: Context) -> impl HandlerCallFutureBounds<Output = Response> + '_ {
119        self.inner.call_boxed(ctx)
120    }
121}
122
123/// Trait to convert functions into handlers
124pub trait IntoHandler<Args> {
125    /// Convert this function into a type-erased handler
126    fn into_handler(self) -> AnyHandler;
127}
128
129/// Adapter that pairs a handler function with the extractors it asks for.
130///
131/// `PhantomData<fn() -> Args>` is covariant in `Args` and unconditionally
132/// `Send + Sync`, so the auto trait impls fall out of `F` alone — the extractor
133/// types never have to be thread-safe themselves.
134struct FnHandler<F, Args>(F, PhantomData<fn() -> Args>);
135
136impl<F, Args> FnHandler<F, Args> {
137    fn new(f: F) -> Self {
138        Self(f, PhantomData)
139    }
140}
141
142/// Generate an `IntoHandler` impl for a handler function of the given arity.
143macro_rules! impl_into_handler {
144    ($($ty:ident $arg:ident),*) => {
145        impl<F, Fut, R, $($ty,)*> IntoHandler<($($ty,)*)> for F
146        where
147            F: Fn($($ty,)*) -> Fut + HandlerFnBounds + 'static,
148            Fut: HandlerFutureBounds<Output = R> + 'static,
149            R: IntoResponse + 'static,
150            $($ty: FromContext + 'static,)*
151        {
152            fn into_handler(self) -> AnyHandler {
153                AnyHandler::new(FnHandler::new(self))
154            }
155        }
156
157        impl<F, Fut, R, $($ty,)*> Handler for FnHandler<F, ($($ty,)*)>
158        where
159            F: Fn($($ty,)*) -> Fut + HandlerFnBounds + 'static,
160            Fut: HandlerFutureBounds<Output = R> + 'static,
161            R: IntoResponse + 'static,
162            $($ty: FromContext + 'static,)*
163        {
164            fn call(&self, ctx: Context) -> impl HandlerCallFutureBounds<Output = Response> + '_ {
165                Box::pin(async move {
166                    // `ctx` is unused at arity zero.
167                    let _ = &ctx;
168                    $(let $arg = $ty::from_context(&ctx).await;)*
169                    (self.0)($($arg,)*).await.into_response()
170                })
171            }
172        }
173    };
174}
175
176impl_into_handler!();
177impl_into_handler!(T1 t1);
178impl_into_handler!(T1 t1, T2 t2);
179impl_into_handler!(T1 t1, T2 t2, T3 t3);
180impl_into_handler!(T1 t1, T2 t2, T3 t3, T4 t4);
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::extractor::{Channel, CommandArgs, CommandName, User};
186    use crate::test_util::StubData;
187    use futures_lite::future::block_on;
188
189    fn call<H: IntoHandler<Args>, Args>(handler: H) -> Response {
190        let handler = handler.into_handler();
191        block_on(handler.call(Context::new(StubData)))
192    }
193
194    #[test]
195    fn zero_arg_handler() {
196        async fn ping() -> &'static str {
197            "Pong!"
198        }
199        assert_eq!(call(ping).content(), Some("Pong!"));
200    }
201
202    #[test]
203    fn extractors_are_applied_in_order() {
204        async fn four(a: CommandName, b: CommandArgs, c: User, d: Channel) -> String {
205            format!("{} {} {} {}", a.0, b.0, c.name, d.id)
206        }
207        assert_eq!(
208            call(four).content(),
209            Some("cmd args stub-user stub-channel")
210        );
211    }
212
213    #[test]
214    fn closures_are_handlers_too() {
215        let handler = || async { String::from("closure") };
216        assert_eq!(call(handler).content(), Some("closure"));
217    }
218
219    #[test]
220    fn handlers_may_capture_non_thread_safe_extractors() {
221        // `Context` is not `Sync`-bound by the extractor itself; this compiles
222        // only because `FnHandler`'s auto traits depend on `F` alone.
223        async fn with_ctx(ctx: Context) -> String {
224            ctx.user_id().to_string()
225        }
226        assert_eq!(call(with_ctx).content(), Some("stub-user"));
227    }
228}