use std::collections::HashMap;
use std::future::Future;
use crate::BotError;
use crate::handler::{AnyHandler, IntoHandler};
use crate::shutdown::Shutdown;
#[cfg(not(target_arch = "wasm32"))]
pub trait BotBounds: Send {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Send + ?Sized> BotBounds for T {}
#[cfg(target_arch = "wasm32")]
pub trait BotBounds {}
#[cfg(target_arch = "wasm32")]
impl<T: ?Sized> BotBounds for T {}
#[cfg(not(target_arch = "wasm32"))]
pub trait BotFutureBounds: Future + Send {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Future + Send + ?Sized> BotFutureBounds for T {}
#[cfg(target_arch = "wasm32")]
pub trait BotFutureBounds: Future {}
#[cfg(target_arch = "wasm32")]
impl<T: Future + ?Sized> BotFutureBounds for T {}
pub trait Bot: Sized + BotBounds {
fn run_until(self, shutdown: Shutdown) -> impl BotFutureBounds<Output = Result<(), BotError>>;
fn run(self) -> impl BotFutureBounds<Output = Result<(), BotError>> {
self.run_until(Shutdown::never())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Event<'a> {
Command(&'a str),
Button(&'a str),
Message,
}
#[derive(Debug, Clone, Copy)]
pub struct CommandInfo<'a> {
pub name: &'a str,
pub description: &'a str,
}
#[derive(Default)]
pub struct BotBuilder {
commands: HashMap<String, CommandEntry>,
buttons: HashMap<String, AnyHandler>,
button_prefixes: Vec<(String, AnyHandler)>,
message: Option<AnyHandler>,
fallback: Option<AnyHandler>,
}
struct CommandEntry {
handler: AnyHandler,
description: Option<String>,
order: usize,
}
impl BotBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn command<H, Args>(self, name: impl Into<String>, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.insert_command(name.into(), None, handler.into_handler())
}
pub fn command_with_description<H, Args>(
self,
name: impl Into<String>,
description: impl Into<String>,
handler: H,
) -> Self
where
H: IntoHandler<Args>,
{
self.insert_command(
name.into(),
Some(description.into()),
handler.into_handler(),
)
}
fn insert_command(
mut self,
name: String,
description: Option<String>,
handler: AnyHandler,
) -> Self {
let order = self.commands.len();
self.commands.entry(name).or_insert(CommandEntry {
handler,
description,
order,
});
self
}
pub fn button<H, Args>(mut self, pattern: impl Into<String>, handler: H) -> Self
where
H: IntoHandler<Args>,
{
let pattern = pattern.into();
let handler = handler.into_handler();
match pattern.strip_suffix('*') {
Some(prefix) => self.button_prefixes.push((prefix.to_string(), handler)),
None => {
self.buttons.entry(pattern).or_insert(handler);
}
}
self
}
pub fn message<H, Args>(mut self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.message.get_or_insert_with(|| handler.into_handler());
self
}
pub fn fallback<H, Args>(mut self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.fallback.get_or_insert_with(|| handler.into_handler());
self
}
pub fn commands(&self) -> impl Iterator<Item = CommandInfo<'_>> {
let mut entries: Vec<_> = self.commands.iter().collect();
entries.sort_by_key(|(_, entry)| entry.order);
entries.into_iter().map(|(name, entry)| CommandInfo {
name: name.as_str(),
description: entry.description.as_deref().unwrap_or(""),
})
}
pub fn has_commands(&self) -> bool {
!self.commands.is_empty()
}
pub fn route(&self, event: Event<'_>) -> Option<&AnyHandler> {
let routed = match event {
Event::Command(name) => self.commands.get(name).map(|entry| &entry.handler),
Event::Button(id) => self.buttons.get(id).or_else(|| {
self.button_prefixes
.iter()
.find(|(prefix, _)| id.starts_with(prefix.as_str()))
.map(|(_, handler)| handler)
}),
Event::Message => self.message.as_ref(),
};
routed.or(self.fallback.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Context;
async fn reply() -> &'static str {
"reply"
}
fn builder() -> BotBuilder {
BotBuilder::new()
.command("ping", reply)
.command_with_description("help", "Show help", reply)
.button("exact", reply)
.button("confirm_*", reply)
.message(reply)
}
#[test]
fn routes_commands_by_name() {
let builder = builder();
assert!(builder.route(Event::Command("ping")).is_some());
assert!(builder.route(Event::Command("help")).is_some());
assert!(builder.route(Event::Command("missing")).is_none());
}
#[test]
fn routes_buttons_exactly_then_by_prefix() {
let builder = builder();
assert!(builder.route(Event::Button("exact")).is_some());
assert!(builder.route(Event::Button("confirm_yes")).is_some());
assert!(builder.route(Event::Button("confirm_")).is_some());
assert!(builder.route(Event::Button("cancel")).is_none());
}
#[test]
fn command_and_button_namespaces_do_not_collide() {
let builder = builder();
assert!(builder.route(Event::Button("ping")).is_none());
assert!(builder.route(Event::Command("exact")).is_none());
}
#[test]
fn message_handler_is_a_catch_all() {
assert!(builder().route(Event::Message).is_some());
assert!(BotBuilder::new().route(Event::Message).is_none());
}
#[test]
fn fallback_catches_unrouted_events_only() {
assert!(builder().route(Event::Command("unknown")).is_none());
assert!(builder().route(Event::Button("unmatched")).is_none());
let routed = builder().fallback(reply);
assert!(routed.route(Event::Command("ping")).is_some());
assert!(routed.route(Event::Button("exact")).is_some());
assert!(routed.route(Event::Command("unknown")).is_some());
assert!(routed.route(Event::Button("unmatched")).is_some());
}
#[test]
fn first_registration_wins() {
async fn first() -> &'static str {
"first"
}
async fn second() -> &'static str {
"second"
}
let builder = BotBuilder::new()
.command("dup", first)
.command("dup", second);
let handler = builder.route(Event::Command("dup")).unwrap().clone();
let response =
futures_lite::future::block_on(handler.call(Context::new(crate::test_util::StubData)));
assert_eq!(response.content(), Some("first"));
}
#[test]
fn commands_keep_registration_order_and_descriptions() {
let commands: Vec<_> = builder()
.commands()
.map(|c| (c.name.to_string(), c.description.to_string()))
.collect();
assert_eq!(
commands,
vec![
("ping".to_string(), String::new()),
("help".to_string(), "Show help".to_string()),
]
);
}
}