From a21cc39f69378e85530a5f225920a5678a8408de Mon Sep 17 00:00:00 2001 From: GnomedDev Date: Fri, 1 Nov 2024 01:09:22 +0000 Subject: [PATCH 1/4] Get rid of autoref-specialisation in slash_argument --- macros/src/command/slash.rs | 4 +- src/slash_argument/slash_macro.rs | 5 +- src/slash_argument/slash_trait.rs | 168 +++++++++--------------------- 3 files changed, 55 insertions(+), 122 deletions(-) diff --git a/macros/src/command/slash.rs b/macros/src/command/slash.rs index d2be02a430f..7d8a71be6ea 100644 --- a/macros/src/command/slash.rs +++ b/macros/src/command/slash.rs @@ -80,7 +80,7 @@ pub fn generate_parameters(inv: &Invocation) -> Result::create(o) #min_value_setter #max_value_setter #min_length_setter #max_length_setter }) } @@ -101,7 +101,7 @@ pub fn generate_parameters(inv: &Invocation) -> Result::choices() } } } else { quote::quote! { Cow::Borrowed(&[]) } diff --git a/src/slash_argument/slash_macro.rs b/src/slash_argument/slash_macro.rs index d25058b449d..0ab69677ecc 100644 --- a/src/slash_argument/slash_macro.rs +++ b/src/slash_argument/slash_macro.rs @@ -130,8 +130,7 @@ macro_rules! _parse_slash { // Extract Option ($ctx:ident, $interaction:ident, $args:ident => $name:literal: Option<$type:ty $(,)*>) => { if let Some(arg) = $args.iter().find(|arg| arg.name == $name) { - Some($crate::extract_slash_argument!($type, $ctx, $interaction, &arg.value) - .await?) + Some(<$type as $crate::SlashArgument>::extract($ctx, $interaction, &arg.value).await?) } else { None } @@ -187,8 +186,6 @@ macro_rules! parse_slash_args { ( $name:literal: $($type:tt)* ) ),* $(,)? ) => { async /* not move! */ { - use $crate::SlashArgumentHack; - // ctx here is a serenity::Context, so it doesn't already contain interaction! let (ctx, interaction, args) = ($ctx, $interaction, $args); diff --git a/src/slash_argument/slash_trait.rs b/src/slash_argument/slash_trait.rs index fda0001552b..b11aa2e0db7 100644 --- a/src/slash_argument/slash_trait.rs +++ b/src/slash_argument/slash_trait.rs @@ -1,8 +1,7 @@ //! Traits for slash command parameters and a macro to wrap the auto-deref specialization hack use super::SlashArgError; -use std::convert::TryInto as _; -use std::marker::PhantomData; +use std::{borrow::Cow, convert::TryInto as _}; #[allow(unused_imports)] // import is required if serenity simdjson feature is enabled use crate::serenity::json::*; @@ -11,10 +10,7 @@ use crate::{serenity_prelude as serenity, CowVec}; /// Implement this trait on types that you want to use as a slash command parameter. #[async_trait::async_trait] pub trait SlashArgument: Sized { - /// Extract a Rust value of type T from the slash command argument, given via a - /// [`serenity::json::Value`]. - /// - /// Don't call this method directly! Use [`crate::extract_slash_argument!`] + /// Extract a Rust value of type T from the slash command argument, given via a [`serenity::ResolvedValue`]. async fn extract( ctx: &serenity::Context, interaction: &serenity::CommandInteraction, @@ -25,110 +21,68 @@ pub trait SlashArgument: Sized { /// /// Only fields about the argument type are filled in. The caller is still responsible for /// filling in `name()`, `description()`, and possibly `required()` or other fields. - /// - /// Don't call this method directly! Use [`crate::create_slash_argument!`] fn create(builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption; /// If this is a choice parameter, returns the choices - /// - /// Don't call this method directly! Use [`crate::slash_argument_choices!`] fn choices() -> CowVec { - CowVec::default() - } -} - -/// Implemented for all types that can be used as a function parameter in a slash command. -/// -/// Currently marked `#[doc(hidden)]` because implementing this trait requires some jank due to a -/// `PhantomData` hack and the auto-deref specialization hack. -#[doc(hidden)] -#[async_trait::async_trait] -pub trait SlashArgumentHack: Sized { - async fn extract( - self, - ctx: &serenity::Context, - interaction: &serenity::CommandInteraction, - value: &serenity::ResolvedValue<'_>, - ) -> Result; - - fn create(self, builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption; - - fn choices(self) -> CowVec { - CowVec::default() + Cow::Borrowed(&[]) } } -/// Full version of [`crate::SlashArgument::extract`]. -/// -/// Uses specialization to get full coverage of types. Pass the type as the first argument -#[macro_export] -macro_rules! extract_slash_argument { - ($target:ty, $ctx:expr, $interaction:expr, $value:expr) => {{ - use $crate::SlashArgumentHack as _; - (&&std::marker::PhantomData::<$target>).extract($ctx, $interaction, $value) - }}; -} -/// Full version of [`crate::SlashArgument::create`]. -/// -/// Uses specialization to get full coverage of types. Pass the type as the first argument -#[macro_export] -macro_rules! create_slash_argument { - ($target:ty, $builder:expr) => {{ - use $crate::SlashArgumentHack as _; - (&&std::marker::PhantomData::<$target>).create($builder) - }}; -} -/// Full version of [`crate::SlashArgument::choices`]. -/// -/// Uses specialization to get full coverage of types. Pass the type as the first argument -#[macro_export] -macro_rules! slash_argument_choices { - ($target:ty) => {{ - use $crate::SlashArgumentHack as _; - (&&std::marker::PhantomData::<$target>).choices() - }}; -} - -/// Handles arbitrary types that can be parsed from string. -#[async_trait::async_trait] -impl SlashArgumentHack for PhantomData +/// Converts a Command value via serenity's ArgumentConvert trait +async fn extract_via_argumentconvert( + ctx: &serenity::Context, + interaction: &serenity::CommandInteraction, + value: &serenity::ResolvedValue<'_>, +) -> Result where T: serenity::ArgumentConvert + Send + Sync, T::Err: std::error::Error + Send + Sync + 'static, { - async fn extract( - self, - ctx: &serenity::Context, - interaction: &serenity::CommandInteraction, - value: &serenity::ResolvedValue<'_>, - ) -> Result { - let string = match value { - serenity::ResolvedValue::String(str) => *str, - _ => { - return Err(SlashArgError::CommandStructureMismatch { - description: "expected string", - }) + let string = match value { + serenity::ResolvedValue::String(str) => *str, + _ => { + return Err(SlashArgError::CommandStructureMismatch { + description: "expected string", + }) + } + }; + + T::convert( + ctx, + interaction.guild_id, + Some(interaction.channel_id), + string, + ) + .await + .map_err(|e| SlashArgError::Parse { + error: e.into(), + input: string.into(), + }) +} + +/// Implements `SlashArgument` via `serenity::ArgumentConvert` +macro_rules! impl_for_argumentconvert { + ($type:ty) => { + #[async_trait::async_trait] + impl SlashArgument for $type { + async fn extract( + ctx: &serenity::Context, + interaction: &serenity::CommandInteraction, + value: &serenity::ResolvedValue<'_>, + ) -> Result { + extract_via_argumentconvert::<$type>(ctx, interaction, value).await } - }; - - T::convert( - ctx, - interaction.guild_id, - Some(interaction.channel_id), - string, - ) - .await - .map_err(|e| SlashArgError::Parse { - error: e.into(), - input: string.into(), - }) - } - fn create(self, builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption { - builder.kind(serenity::CommandOptionType::String) - } + fn create(builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption { + builder.kind(serenity::CommandOptionType::String) + } + } + }; } +impl_for_argumentconvert!(serenity::Message); + /// Implements slash argument trait for integer types macro_rules! impl_for_integer { ($($t:ty)*) => { $( @@ -160,29 +114,10 @@ macro_rules! impl_for_integer { } )* }; } -impl_for_integer!(i8 i16 i32 i64 isize u8 u16 u32 u64 usize); -#[async_trait::async_trait] -impl SlashArgumentHack for &PhantomData { - async fn extract( - self, - ctx: &serenity::Context, - interaction: &serenity::CommandInteraction, - value: &serenity::ResolvedValue<'_>, - ) -> Result { - ::extract(ctx, interaction, value).await - } - - fn create(self, builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption { - ::create(builder) - } - - fn choices(self) -> CowVec { - ::choices() - } -} +impl_for_integer!(i8 i16 i32 i64 isize u8 u16 u32 u64 usize); -/// Versatile macro to implement `SlashArgumentHack` for simple types +/// Versatile macro to implement `SlashArgument` for simple types macro_rules! impl_slash_argument { ($type:ty, |$ctx:pat, $interaction:pat, $slash_param_type:ident ( $($arg:pat),* )| $extractor:expr) => { #[async_trait::async_trait] @@ -210,6 +145,7 @@ macro_rules! impl_slash_argument { impl_slash_argument!(f32, |_, _, Number(x)| x as f32); impl_slash_argument!(f64, |_, _, Number(x)| x); impl_slash_argument!(bool, |_, _, Boolean(x)| x); +impl_slash_argument!(String, |_, _, String(x)| x.into()); impl_slash_argument!(serenity::Attachment, |_, _, Attachment(att)| att.clone()); impl_slash_argument!(serenity::Member, |ctx, interaction, User(user, _)| { interaction From 02e69ffbf684740e83782af40c9ac7bf70bef298 Mon Sep 17 00:00:00 2001 From: GnomedDev Date: Fri, 1 Nov 2024 01:31:50 +0000 Subject: [PATCH 2/4] Get rid of autoref-specialisation in prefix_argument --- src/choice_parameter.rs | 7 +- src/prefix_argument/argument_trait.rs | 166 +++++++++----------------- src/prefix_argument/code_block.rs | 3 +- src/prefix_argument/key_value_args.rs | 3 +- src/prefix_argument/macros.rs | 10 +- 5 files changed, 66 insertions(+), 123 deletions(-) diff --git a/src/choice_parameter.rs b/src/choice_parameter.rs index 32f0b168cac..a625c6723ae 100644 --- a/src/choice_parameter.rs +++ b/src/choice_parameter.rs @@ -1,7 +1,7 @@ //! Contains the [`ChoiceParameter`] trait and the blanket [`crate::SlashArgument`] and //! [`crate::PopArgument`] impl -use crate::{serenity_prelude as serenity, CowVec}; +use crate::{serenity_prelude as serenity, CowVec, PopArgumentResult}; /// This trait is implemented by [`crate::macros::ChoiceParameter`]. See its docs for more /// information @@ -62,10 +62,9 @@ impl<'a, T: ChoiceParameter> crate::PopArgument<'a> for T { attachment_index: usize, ctx: &serenity::Context, msg: &serenity::Message, - ) -> Result<(&'a str, usize, Self), (Box, Option)> - { + ) -> PopArgumentResult<'a, Self> { let (args, attachment_index, s) = - crate::pop_prefix_argument!(String, args, attachment_index, ctx, msg).await?; + >::pop_from(args, attachment_index, ctx, msg).await?; Ok(( args, diff --git a/src/prefix_argument/argument_trait.rs b/src/prefix_argument/argument_trait.rs index 8a6bf6fee7a..76ec739c25d 100644 --- a/src/prefix_argument/argument_trait.rs +++ b/src/prefix_argument/argument_trait.rs @@ -4,103 +4,57 @@ use super::{pop_string, InvalidBool, MissingAttachment, TooFewArguments}; use crate::serenity_prelude as serenity; -use std::marker::PhantomData; -/// Full version of [`crate::PopArgument::pop_from`]. +/// The result of `::pop_from`. /// -/// Uses specialization to get full coverage of types. Pass the type as the first argument -#[macro_export] -macro_rules! pop_prefix_argument { - ($target:ty, $args:expr, $attachment_id:expr, $ctx:expr, $msg:expr) => {{ - use $crate::PopArgumentHack as _; - (&std::marker::PhantomData::<$target>).pop_from($args, $attachment_id, $ctx, $msg) - }}; -} +/// If Ok, this is `(remaining, attachment_index, T)` +/// If Err, this is `(error, failing_arg)` +pub(crate) type PopArgumentResult<'a, T> = + Result<(&'a str, usize, T), (Box, Option)>; /// Parse a value out of a string by popping off the front of the string. Discord message context /// is available for parsing, and IO may be done as part of the parsing. /// /// Implementors should assume that a string never starts with whitespace, and fail to parse if it -/// does. This is for consistency's -/// sake and also because it keeps open the possibility of parsing whitespace. +/// does. This is for consistency's sake and also because it keeps open the possibility of parsing whitespace. /// /// Similar in spirit to [`std::str::FromStr`]. #[async_trait::async_trait] pub trait PopArgument<'a>: Sized { - /// Parse [`Self`] from the front of the given string and return a tuple of the remaining string - /// and [`Self`]. If parsing failed, an error is returned and, if applicable, the string on - /// which parsing failed. - /// - /// If parsing fails because the string is empty, use the `TooFewArguments` type as the error. + /// Pops an argument from the `args` string. /// - /// Don't call this method directly! Use [`crate::pop_prefix_argument!`] - async fn pop_from( - args: &'a str, - attachment_index: usize, - ctx: &serenity::Context, - msg: &serenity::Message, - ) -> Result<(&'a str, usize, Self), (Box, Option)>; -} - -#[doc(hidden)] -#[async_trait::async_trait] -pub trait PopArgumentHack<'a, T>: Sized { - async fn pop_from( - self, - args: &'a str, - attachment_index: usize, - ctx: &serenity::Context, - msg: &serenity::Message, - ) -> Result<(&'a str, usize, T), (Box, Option)>; -} - -#[async_trait::async_trait] -impl<'a, T: serenity::ArgumentConvert + Send> PopArgumentHack<'a, T> for PhantomData -where - T::Err: std::error::Error + Send + Sync + 'static, -{ + /// See the documentation of [`PopArgumentResult`] for the return type. async fn pop_from( - self, args: &'a str, attachment_index: usize, ctx: &serenity::Context, msg: &serenity::Message, - ) -> Result<(&'a str, usize, T), (Box, Option)> - { - let (args, string) = - pop_string(args).map_err(|_| (TooFewArguments::default().into(), None))?; - let object = T::convert(ctx, msg.guild_id, Some(msg.channel_id), &string) - .await - .map_err(|e| (e.into(), Some(string)))?; - - Ok((args.trim_start(), attachment_index, object)) - } + ) -> PopArgumentResult<'a, Self>; } #[async_trait::async_trait] -impl<'a, T: PopArgument<'a> + Send + Sync> PopArgumentHack<'a, T> for &PhantomData { +impl<'a> PopArgument<'a> for String { async fn pop_from( - self, args: &'a str, attachment_index: usize, ctx: &serenity::Context, msg: &serenity::Message, - ) -> Result<(&'a str, usize, T), (Box, Option)> - { - T::pop_from(args, attachment_index, ctx, msg).await + ) -> PopArgumentResult<'a, Self> { + match pop_string(args) { + Ok((args, string)) => Ok((args, attachment_index, string)), + Err(err) => Err((Box::new(err), Some(args.into()))), + } } } #[async_trait::async_trait] -impl<'a> PopArgumentHack<'a, bool> for &PhantomData { +impl<'a> PopArgument<'a> for bool { async fn pop_from( - self, args: &'a str, attachment_index: usize, ctx: &serenity::Context, msg: &serenity::Message, - ) -> Result<(&'a str, usize, bool), (Box, Option)> - { + ) -> PopArgumentResult<'a, Self> { let (args, string) = pop_string(args).map_err(|_| (TooFewArguments::default().into(), None))?; @@ -115,17 +69,13 @@ impl<'a> PopArgumentHack<'a, bool> for &PhantomData { } #[async_trait::async_trait] -impl<'a> PopArgumentHack<'a, serenity::Attachment> for &PhantomData { +impl<'a> PopArgument<'a> for serenity::Attachment { async fn pop_from( - self, args: &'a str, attachment_index: usize, ctx: &serenity::Context, msg: &serenity::Message, - ) -> Result< - (&'a str, usize, serenity::Attachment), - (Box, Option), - > { + ) -> PopArgumentResult<'a, Self> { let attachment = msg .attachments .get(attachment_index) @@ -136,56 +86,52 @@ impl<'a> PopArgumentHack<'a, serenity::Attachment> for &PhantomData { - /// Error thrown when the user enters a string that cannot be parsed correctly. - #[derive(Default, Debug)] - pub struct $error_type { - #[doc(hidden)] - pub __non_exhaustive: (), - } +/// Pops an argument from the message via serenity's ArgumentConvert trait +async fn pop_from_via_argumentconvert<'a, T>( + args: &'a str, + attachment_index: usize, + ctx: &serenity::Context, + msg: &serenity::Message, +) -> PopArgumentResult<'a, T> +where + T: serenity::ArgumentConvert + Send, + T::Err: std::error::Error + Send + Sync + 'static, +{ + let (args, string) = pop_string(args).map_err(|_| (TooFewArguments::default().into(), None))?; + let object = T::convert(ctx, msg.guild_id, Some(msg.channel_id), &string) + .await + .map_err(|e| (e.into(), Some(string)))?; - impl std::error::Error for $error_type {} - impl std::fmt::Display for $error_type { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(concat!( - "Enter a valid ", - stringify!($error_type), - " ID or a mention." - )) - } - } + Ok((args.trim_start(), attachment_index, object)) +} +/// Implements PopArgument for many types via `[pop_from_via_argumentconvert`]. +macro_rules! impl_popargument_via_argumentconvert { + ($($type:ty),*) => {$( #[async_trait::async_trait] - impl<'a> PopArgumentHack<'a, $type> for &PhantomData<$type> { + impl<'a> PopArgument<'a> for $type { async fn pop_from( - self, args: &'a str, attachment_index: usize, ctx: &serenity::Context, msg: &serenity::Message, - ) -> Result< - (&'a str, usize, $type), - (Box, Option), - > { - let (args, string) = - pop_string(args).map_err(|_| (TooFewArguments::default().into(), None))?; - - if let Some(parsed_id) = string - .parse() - .ok() - .or_else(|| serenity::utils::$parse_fn(&string)) - { - Ok((args.trim_start(), attachment_index, parsed_id)) - } else { - Err(($error_type::default().into(), Some(string))) - } + ) -> PopArgumentResult<'a, Self> { + pop_from_via_argumentconvert(args, attachment_index, ctx, msg).await } } - }; + )*}; } -snowflake_pop_argument!(serenity::UserId, parse_user_mention, InvalidUserId); -snowflake_pop_argument!(serenity::ChannelId, parse_channel_mention, InvalidChannelId); -snowflake_pop_argument!(serenity::RoleId, parse_role_mention, InvalidRoleId); +#[rustfmt::skip] +impl_popargument_via_argumentconvert!( + f32, f64, + u8, u16, u32, u64, + i8, i16, i32, i64, + serenity::UserId, serenity::User, + serenity::MessageId, serenity::Message, + serenity::ChannelId, serenity::Channel, serenity::GuildChannel, + serenity::EmojiId, serenity::Emoji, + serenity::RoleId, serenity::Role +); +#[cfg(feature = "cache")] +impl_popargument_via_argumentconvert!(serenity::GuildId, serenity::Guild); diff --git a/src/prefix_argument/code_block.rs b/src/prefix_argument/code_block.rs index 24ae8175144..53bf4d7ebaf 100644 --- a/src/prefix_argument/code_block.rs +++ b/src/prefix_argument/code_block.rs @@ -119,8 +119,7 @@ impl<'a> PopArgument<'a> for CodeBlock { attachment_index: usize, _: &serenity::Context, _: &serenity::Message, - ) -> Result<(&'a str, usize, Self), (Box, Option)> - { + ) -> PopArgumentResult<'a, Self> { let (a, b) = pop_from(args).map_err(|e| (e.into(), None))?; Ok((a, attachment_index, b)) diff --git a/src/prefix_argument/key_value_args.rs b/src/prefix_argument/key_value_args.rs index 7bcef20db7d..7069098fe76 100644 --- a/src/prefix_argument/key_value_args.rs +++ b/src/prefix_argument/key_value_args.rs @@ -78,8 +78,7 @@ impl<'a> PopArgument<'a> for KeyValueArgs { attachment_index: usize, _: &serenity::Context, _: &serenity::Message, - ) -> Result<(&'a str, usize, Self), (Box, Option)> - { + ) -> PopArgumentResult<'a, Self> { let (a, b) = Self::pop_from(args); Ok((a, attachment_index, b)) diff --git a/src/prefix_argument/macros.rs b/src/prefix_argument/macros.rs index 3a86d3884c4..371fa568c7c 100644 --- a/src/prefix_argument/macros.rs +++ b/src/prefix_argument/macros.rs @@ -17,7 +17,7 @@ macro_rules! _parse_prefix { $( $rest:tt )* ) => { // Try parse the next argument - match $crate::pop_prefix_argument!($type, &$args, $attachment_index, $ctx, $msg).await { + match <$type as $crate::PopArgument>::pop_from(&$args, $attachment_index, $ctx, $msg).await { // On success, we get a new `$args` which contains only the rest of the args Ok(($args, $attachment_index, token)) => { // On success, store `Some(token)` for the parsed argument @@ -41,7 +41,7 @@ macro_rules! _parse_prefix { ) => { let token: Option<$type> = None; $crate::_parse_prefix!($ctx $msg $args $attachment_index => [ $error $($preamble)* token ] $($rest)* ); - match $crate::pop_prefix_argument!($type, &$args, $attachment_index, $ctx, $msg).await { + match <$type as $crate::PopArgument>::pop_from(&$args, $attachment_index, $ctx, $msg).await { Ok(($args, $attachment_index, token)) => { let token: Option<$type> = Some(token); $crate::_parse_prefix!($ctx $msg $args $attachment_index => [ $error $($preamble)* token ] $($rest)* ); @@ -85,7 +85,7 @@ macro_rules! _parse_prefix { let mut attachment = $attachment_index; loop { - match $crate::pop_prefix_argument!($type, &running_args, attachment, $ctx, $msg).await { + match <$type as $crate::PopArgument>::pop_from(&running_args, attachment, $ctx, $msg).await { Ok((popped_args, new_attachment, token)) => { tokens.push(token); token_rest_args.push(popped_args.clone()); @@ -138,7 +138,7 @@ macro_rules! _parse_prefix { (#[flag] $name:literal) $( $rest:tt )* ) => { - match $crate::pop_prefix_argument!(String, &$args, $attachment_index, $ctx, $msg).await { + match ::pop_from(&$args, $attachment_index, $ctx, $msg).await { Ok(($args, $attachment_index, token)) if token.eq_ignore_ascii_case($name) => { $crate::_parse_prefix!($ctx $msg $args $attachment_index => [ $error $($preamble)* true ] $($rest)* ); }, @@ -156,7 +156,7 @@ macro_rules! _parse_prefix { ($type:ty) $( $rest:tt )* ) => { - match $crate::pop_prefix_argument!($type, &$args, $attachment_index, $ctx, $msg).await { + match <$type as $crate::PopArgument>::pop_from(&$args, $attachment_index, $ctx, $msg).await { Ok(($args, $attachment_index, token)) => { $crate::_parse_prefix!($ctx $msg $args $attachment_index => [ $error $($preamble)* token ] $($rest)* ); }, From d4f4977a7c3531796fee6708b257559fa41f4ba8 Mon Sep 17 00:00:00 2001 From: GnomedDev Date: Fri, 1 Nov 2024 01:32:41 +0000 Subject: [PATCH 3/4] Fix up docs --- src/prefix_argument/argument_trait.rs | 6 +++--- src/slash_argument/slash_trait.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/prefix_argument/argument_trait.rs b/src/prefix_argument/argument_trait.rs index 76ec739c25d..826d360d02e 100644 --- a/src/prefix_argument/argument_trait.rs +++ b/src/prefix_argument/argument_trait.rs @@ -1,6 +1,6 @@ -//! Trait implemented for all types usable as prefix command parameters. This file also includes -//! the auto-deref specialization emulation code to e.g. support more strings for bool parameters -//! instead of the `FromStr` ones +//! Trait implemented for all types usable as prefix command parameters. +//! +//! Many of these implementations defer to [`serenity::ArgumentConvert`]. use super::{pop_string, InvalidBool, MissingAttachment, TooFewArguments}; use crate::serenity_prelude as serenity; diff --git a/src/slash_argument/slash_trait.rs b/src/slash_argument/slash_trait.rs index b11aa2e0db7..5eb60fda5e3 100644 --- a/src/slash_argument/slash_trait.rs +++ b/src/slash_argument/slash_trait.rs @@ -1,4 +1,4 @@ -//! Traits for slash command parameters and a macro to wrap the auto-deref specialization hack +//! Traits for slash command parameters. use super::SlashArgError; use std::{borrow::Cow, convert::TryInto as _}; From ba14a48183df31ed4100778e9602b488f2fd3997 Mon Sep 17 00:00:00 2001 From: GnomedDev Date: Fri, 1 Nov 2024 02:01:09 +0000 Subject: [PATCH 4/4] Add poise::StrArg for FromStr arguments --- macros/src/lib.rs | 2 +- src/argument.rs | 61 +++++++++++++++++++++++++++ src/lib.rs | 11 ++--- src/prefix_argument/argument_trait.rs | 2 +- 4 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 src/argument.rs diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 76158deb785..8e37f5d506d 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -88,7 +88,7 @@ for example for command-specific help (i.e. `~help command_name`). Escape newlin SlashContext, which contain a variety of context data each. Context provides some utility methods to access data present in both PrefixContext and SlashContext, like `author()` or `created_at()`. -All following parameters are inputs to the command. You can use all types that implement `poise::PopArgument`, `serenity::ArgumentConvert` or `std::str::FromStr`. +All following parameters are inputs to the command. You can use all types that implement `PopArgument` (for prefix) or `SlashArgument` (for slash). You can also wrap types in `Option` or `Vec` to make them optional or variadic. In addition, there are multiple attributes you can use on parameters: diff --git a/src/argument.rs b/src/argument.rs new file mode 100644 index 00000000000..443286bacb1 --- /dev/null +++ b/src/argument.rs @@ -0,0 +1,61 @@ +use std::str::FromStr; + +use crate::{ + serenity_prelude as serenity, PopArgument, PopArgumentResult, SlashArgError, SlashArgument, +}; + +/// A wrapper for `T` to implement [`SlashArgument`] and [`PopArgument`] via [`FromStr`]. +/// +/// This is useful if you need to take an argument via a string, but immediately convert it via [`FromStr`]. +pub struct StrArg(pub T); + +#[async_trait::async_trait] +impl SlashArgument for StrArg +where + T: FromStr, + T::Err: std::error::Error + Send + Sync + 'static, +{ + fn create(builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption { + builder.kind(serenity::CommandOptionType::String) + } + + async fn extract( + _: &serenity::Context, + _: &serenity::CommandInteraction, + value: &serenity::ResolvedValue<'_>, + ) -> Result { + let serenity::ResolvedValue::String(value) = value else { + return Err(SlashArgError::new_command_structure_mismatch( + "expected a String", + )); + }; + + match T::from_str(value) { + Ok(value) => Ok(Self(value)), + Err(err) => Err(SlashArgError::Parse { + error: err.into(), + input: String::from(*value), + }), + } + } +} + +#[async_trait::async_trait] +impl<'a, T> PopArgument<'a> for StrArg +where + T: FromStr, + T::Err: std::error::Error + Send + Sync + 'static, +{ + async fn pop_from( + args: &'a str, + attachment_index: usize, + ctx: &serenity::Context, + msg: &serenity::Message, + ) -> PopArgumentResult<'a, Self> { + let (args, attach_idx, value) = String::pop_from(args, attachment_index, ctx, msg).await?; + match T::from_str(&value) { + Ok(value) => Ok((args, attach_idx, Self(value))), + Err(err) => Err((Box::new(err), Some(value))), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 032a9ef40ca..c7d4de4b595 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,7 +173,7 @@ For another example of subcommands, see `examples/feature_showcase/subcommands.r Also see the [`command`] macro docs ```rust -use poise::serenity_prelude as serenity; +use poise::{StrArg, serenity_prelude as serenity}; type Data = (); type Error = Box; type Context<'a> = poise::Context<'a, Data, Error>; @@ -190,9 +190,9 @@ type Context<'a> = poise::Context<'a, Data, Error>; )] async fn my_huge_ass_command( ctx: Context<'_>, - #[description = "Consectetur"] ip_addr: std::net::IpAddr, // implements FromStr - #[description = "Amet"] user: serenity::Member, // implements ArgumentConvert - #[description = "Sit"] code_block: poise::CodeBlock, // implements PopArgument + #[description = "Amet"] user: serenity::Member, + #[description = "Consectetur"] #[rename = "ip_addr"] StrArg(ip_addr): StrArg, + #[description = "Sit"] code_block: poise::CodeBlock, #[description = "Dolor"] #[flag] my_flag: bool, #[description = "Ipsum"] #[lazy] always_none: Option, #[description = "Lorem"] #[rest] rest: String, @@ -381,6 +381,7 @@ underlying this framework, so that's what I chose. Also, poise is a stat in Dark Souls */ +mod argument; pub mod builtins; pub mod choice_parameter; pub mod cooldown; @@ -400,7 +401,7 @@ pub mod macros { #[doc(no_inline)] pub use { - choice_parameter::*, cooldown::*, dispatch::*, framework::*, macros::*, modal::*, + argument::*, choice_parameter::*, cooldown::*, dispatch::*, framework::*, macros::*, modal::*, prefix_argument::*, reply::*, slash_argument::*, structs::*, track_edits::*, }; diff --git a/src/prefix_argument/argument_trait.rs b/src/prefix_argument/argument_trait.rs index 826d360d02e..01fa47458a4 100644 --- a/src/prefix_argument/argument_trait.rs +++ b/src/prefix_argument/argument_trait.rs @@ -127,7 +127,7 @@ impl_popargument_via_argumentconvert!( f32, f64, u8, u16, u32, u64, i8, i16, i32, i64, - serenity::UserId, serenity::User, + serenity::UserId, serenity::User, serenity::Member, serenity::MessageId, serenity::Message, serenity::ChannelId, serenity::Channel, serenity::GuildChannel, serenity::EmojiId, serenity::Emoji,