From a527b22515449d75a2c56b9d80aa630e6c16fff4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 29 Dec 2024 13:59:27 +0100 Subject: [PATCH] Construct clap commands from handlers too --- euphoxide-bot/examples/examplebot.rs | 23 +++++++++++ euphoxide-bot/src/command.rs | 5 +++ euphoxide-bot/src/command/clap.rs | 60 ++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/euphoxide-bot/examples/examplebot.rs b/euphoxide-bot/examples/examplebot.rs index 482dee8..68fed91 100644 --- a/euphoxide-bot/examples/examplebot.rs +++ b/euphoxide-bot/examples/examplebot.rs @@ -4,6 +4,7 @@ use euphoxide::api::Message; use euphoxide_bot::{ basic::FromHandler, botrulez::{FullHelp, Ping, ShortHelp}, + clap::FromClapHandler, CommandExt, Commands, Context, Propagate, }; use euphoxide_client::MultiClient; @@ -24,6 +25,21 @@ async fn pyramid(_arg: &str, msg: &Message, ctx: &Context) -> euphoxide::Result< Ok(Propagate::No) } +#[derive(clap::Parser)] +struct AddArgs { + lhs: i64, + rhs: i64, +} + +async fn add(args: AddArgs, msg: &Message, ctx: &Context) -> euphoxide::Result { + let result = args.lhs + args.rhs; + + ctx.reply_only(msg.id, format!("{} + {} = {result}", args.lhs, args.rhs)) + .await?; + + Ok(Propagate::No) +} + #[tokio::main] async fn main() { let (event_tx, mut event_rx) = mpsc::channel(10); @@ -48,6 +64,13 @@ async fn main() { .with_description("build a pyramid") .general("pyramid"), ) + .then( + FromClapHandler::new(add) + .clap() + .described() + .with_description("add two numbers") + .general("add"), + ) .build(); let clients = MultiClient::new(event_tx); diff --git a/euphoxide-bot/src/command.rs b/euphoxide-bot/src/command.rs index 082e8f0..52ca343 100644 --- a/euphoxide-bot/src/command.rs +++ b/euphoxide-bot/src/command.rs @@ -148,6 +148,11 @@ pub trait CommandExt: Sized { fn specific(self, name: impl ToString) -> Specific { Specific::new(name, self) } + + #[cfg(feature = "clap")] + fn clap(self) -> clap::Clap { + clap::Clap(self) + } } // Sadly this doesn't work: `impl> CommandExt for C {}` diff --git a/euphoxide-bot/src/command/clap.rs b/euphoxide-bot/src/command/clap.rs index 2e071b6..c0bc71b 100644 --- a/euphoxide-bot/src/command/clap.rs +++ b/euphoxide-bot/src/command/clap.rs @@ -1,5 +1,7 @@ //! [`clap`]-based commands. +use std::{future::Future, marker::PhantomData}; + use async_trait::async_trait; use clap::{CommandFactory, Parser}; use euphoxide::api::Message; @@ -136,6 +138,64 @@ where } } +// TODO Simplify all this once AsyncFn becomes stable + +pub trait ClapHandlerFn<'a0, 'a1, A, E>: + Fn(A, &'a0 Message, &'a1 Context) -> Self::Future +where + E: 'a1, +{ + type Future: Future> + Send; +} + +impl<'a0, 'a1, A, E, F, Fut> ClapHandlerFn<'a0, 'a1, A, E> for F +where + E: 'a1, + F: Fn(A, &'a0 Message, &'a1 Context) -> Fut + ?Sized, + Fut: Future> + Send, +{ + type Future = Fut; +} + +pub struct FromClapHandler { + _a: PhantomData, + pub handler: F, +} + +impl FromClapHandler { + // Artificially constrained so we don't accidentally choose an incorrect A. + // Relying on type inference of A can result in unknown type errors even + // though we know what A should be based on F. + pub fn new<'a0, 'a1, E, Fut>(handler: F) -> Self + where + F: Fn(A, &'a0 Message, &'a1 Context) -> Fut, + E: 'a1, + { + Self { + _a: PhantomData, + handler, + } + } +} + +#[async_trait] +impl ClapCommand for FromClapHandler +where + F: for<'a0, 'a1> ClapHandlerFn<'a0, 'a1, A, E> + Sync, + A: Send + Sync + 'static, +{ + type Args = A; + + async fn execute( + &self, + args: Self::Args, + msg: &Message, + ctx: &Context, + ) -> Result { + (self.handler)(args, msg, ctx).await + } +} + #[cfg(test)] mod test { use super::parse_quoted_args;