1use std::collections::HashMap;
2use std::future::Future;
3
4use crate::BotError;
5use crate::handler::{AnyHandler, IntoHandler};
6use crate::shutdown::Shutdown;
7
8#[cfg(not(target_arch = "wasm32"))]
11pub trait BotBounds: Send {}
12#[cfg(not(target_arch = "wasm32"))]
13impl<T: Send + ?Sized> BotBounds for T {}
14
15#[cfg(target_arch = "wasm32")]
16pub trait BotBounds {}
17#[cfg(target_arch = "wasm32")]
18impl<T: ?Sized> BotBounds for T {}
19
20#[cfg(not(target_arch = "wasm32"))]
21pub trait BotFutureBounds: Future + Send {}
22#[cfg(not(target_arch = "wasm32"))]
23impl<T: Future + Send + ?Sized> BotFutureBounds for T {}
24
25#[cfg(target_arch = "wasm32")]
26pub trait BotFutureBounds: Future {}
27#[cfg(target_arch = "wasm32")]
28impl<T: Future + ?Sized> BotFutureBounds for T {}
29
30pub trait Bot: Sized + BotBounds {
49 fn run_until(self, shutdown: Shutdown) -> impl BotFutureBounds<Output = Result<(), BotError>>;
53
54 fn run(self) -> impl BotFutureBounds<Output = Result<(), BotError>> {
58 self.run_until(Shutdown::never())
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum Event<'a> {
68 Command(&'a str),
70 Button(&'a str),
72 Message,
74}
75
76#[derive(Debug, Clone, Copy)]
78pub struct CommandInfo<'a> {
79 pub name: &'a str,
81 pub description: &'a str,
83}
84
85#[derive(Default)]
91pub struct BotBuilder {
92 commands: HashMap<String, CommandEntry>,
94 buttons: HashMap<String, AnyHandler>,
96 button_prefixes: Vec<(String, AnyHandler)>,
98 message: Option<AnyHandler>,
100 fallback: Option<AnyHandler>,
102}
103
104struct CommandEntry {
105 handler: AnyHandler,
106 description: Option<String>,
107 order: usize,
109}
110
111impl BotBuilder {
112 pub fn new() -> Self {
114 Self::default()
115 }
116
117 pub fn command<H, Args>(self, name: impl Into<String>, handler: H) -> Self
135 where
136 H: IntoHandler<Args>,
137 {
138 self.insert_command(name.into(), None, handler.into_handler())
139 }
140
141 pub fn command_with_description<H, Args>(
145 self,
146 name: impl Into<String>,
147 description: impl Into<String>,
148 handler: H,
149 ) -> Self
150 where
151 H: IntoHandler<Args>,
152 {
153 self.insert_command(
154 name.into(),
155 Some(description.into()),
156 handler.into_handler(),
157 )
158 }
159
160 fn insert_command(
161 mut self,
162 name: String,
163 description: Option<String>,
164 handler: AnyHandler,
165 ) -> Self {
166 let order = self.commands.len();
167 self.commands.entry(name).or_insert(CommandEntry {
168 handler,
169 description,
170 order,
171 });
172 self
173 }
174
175 pub fn button<H, Args>(mut self, pattern: impl Into<String>, handler: H) -> Self
180 where
181 H: IntoHandler<Args>,
182 {
183 let pattern = pattern.into();
184 let handler = handler.into_handler();
185
186 match pattern.strip_suffix('*') {
187 Some(prefix) => self.button_prefixes.push((prefix.to_string(), handler)),
188 None => {
189 self.buttons.entry(pattern).or_insert(handler);
190 }
191 }
192 self
193 }
194
195 pub fn message<H, Args>(mut self, handler: H) -> Self
199 where
200 H: IntoHandler<Args>,
201 {
202 self.message.get_or_insert_with(|| handler.into_handler());
203 self
204 }
205
206 pub fn fallback<H, Args>(mut self, handler: H) -> Self
215 where
216 H: IntoHandler<Args>,
217 {
218 self.fallback.get_or_insert_with(|| handler.into_handler());
219 self
220 }
221
222 pub fn commands(&self) -> impl Iterator<Item = CommandInfo<'_>> {
224 let mut entries: Vec<_> = self.commands.iter().collect();
225 entries.sort_by_key(|(_, entry)| entry.order);
226 entries.into_iter().map(|(name, entry)| CommandInfo {
227 name: name.as_str(),
228 description: entry.description.as_deref().unwrap_or(""),
229 })
230 }
231
232 pub fn has_commands(&self) -> bool {
234 !self.commands.is_empty()
235 }
236
237 pub fn route(&self, event: Event<'_>) -> Option<&AnyHandler> {
243 let routed = match event {
244 Event::Command(name) => self.commands.get(name).map(|entry| &entry.handler),
245 Event::Button(id) => self.buttons.get(id).or_else(|| {
246 self.button_prefixes
247 .iter()
248 .find(|(prefix, _)| id.starts_with(prefix.as_str()))
249 .map(|(_, handler)| handler)
250 }),
251 Event::Message => self.message.as_ref(),
252 };
253 routed.or(self.fallback.as_ref())
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::Context;
261
262 async fn reply() -> &'static str {
263 "reply"
264 }
265
266 fn builder() -> BotBuilder {
267 BotBuilder::new()
268 .command("ping", reply)
269 .command_with_description("help", "Show help", reply)
270 .button("exact", reply)
271 .button("confirm_*", reply)
272 .message(reply)
273 }
274
275 #[test]
276 fn routes_commands_by_name() {
277 let builder = builder();
278 assert!(builder.route(Event::Command("ping")).is_some());
279 assert!(builder.route(Event::Command("help")).is_some());
280 assert!(builder.route(Event::Command("missing")).is_none());
281 }
282
283 #[test]
284 fn routes_buttons_exactly_then_by_prefix() {
285 let builder = builder();
286 assert!(builder.route(Event::Button("exact")).is_some());
287 assert!(builder.route(Event::Button("confirm_yes")).is_some());
288 assert!(builder.route(Event::Button("confirm_")).is_some());
289 assert!(builder.route(Event::Button("cancel")).is_none());
290 }
291
292 #[test]
293 fn command_and_button_namespaces_do_not_collide() {
294 let builder = builder();
295 assert!(builder.route(Event::Button("ping")).is_none());
296 assert!(builder.route(Event::Command("exact")).is_none());
297 }
298
299 #[test]
300 fn message_handler_is_a_catch_all() {
301 assert!(builder().route(Event::Message).is_some());
302 assert!(BotBuilder::new().route(Event::Message).is_none());
303 }
304
305 #[test]
306 fn fallback_catches_unrouted_events_only() {
307 assert!(builder().route(Event::Command("unknown")).is_none());
309 assert!(builder().route(Event::Button("unmatched")).is_none());
310
311 let routed = builder().fallback(reply);
312 assert!(routed.route(Event::Command("ping")).is_some());
314 assert!(routed.route(Event::Button("exact")).is_some());
315 assert!(routed.route(Event::Command("unknown")).is_some());
317 assert!(routed.route(Event::Button("unmatched")).is_some());
318 }
319
320 #[test]
321 fn first_registration_wins() {
322 async fn first() -> &'static str {
323 "first"
324 }
325 async fn second() -> &'static str {
326 "second"
327 }
328
329 let builder = BotBuilder::new()
330 .command("dup", first)
331 .command("dup", second);
332 let handler = builder.route(Event::Command("dup")).unwrap().clone();
333 let response =
334 futures_lite::future::block_on(handler.call(Context::new(crate::test_util::StubData)));
335 assert_eq!(response.content(), Some("first"));
336 }
337
338 #[test]
339 fn commands_keep_registration_order_and_descriptions() {
340 let commands: Vec<_> = builder()
341 .commands()
342 .map(|c| (c.name.to_string(), c.description.to_string()))
343 .collect();
344 assert_eq!(
345 commands,
346 vec![
347 ("ping".to_string(), String::new()),
348 ("help".to_string(), "Show help".to_string()),
349 ]
350 );
351 }
352}