From b695d70732c85dba9f2fd86efb7a4569a9a39533 Mon Sep 17 00:00:00 2001 From: Michael Krasnitski Date: Wed, 9 Jul 2025 10:01:26 -0400 Subject: [PATCH 1/3] Remove autoref specialization for `PopArgument` Replaces the specialization of blanket impls with macro-generated impls for types that explicitly implement `serenity::ArgumentConvert`, and introduce a new `#[string]` attribute to signal that a type's `FromStr` implementation should be used. --- macros/src/command/mod.rs | 1 + macros/src/command/prefix.rs | 13 +- macros/src/lib.rs | 8 +- src/choice_parameter.rs | 7 +- src/lib.rs | 2 +- src/prefix_argument/argument_trait.rs | 182 +++++++++++++------------- src/prefix_argument/macros.rs | 28 +++- src/prefix_argument/mod.rs | 1 + 8 files changed, 135 insertions(+), 107 deletions(-) diff --git a/macros/src/command/mod.rs b/macros/src/command/mod.rs index 3fdb8ecdff7..3c9d82feb1f 100644 --- a/macros/src/command/mod.rs +++ b/macros/src/command/mod.rs @@ -79,6 +79,7 @@ struct ParamArgs { max: Option, min_length: Option, max_length: Option, + string: bool, lazy: bool, flag: bool, rest: bool, diff --git a/macros/src/command/prefix.rs b/macros/src/command/prefix.rs index 9a3bc6f16b5..d4750ae1216 100644 --- a/macros/src/command/prefix.rs +++ b/macros/src/command/prefix.rs @@ -8,12 +8,14 @@ fn quote_parameter(p: &super::CommandParameter) -> Result Modifier::None, - (true, false, false) => Modifier::Lazy, - (false, true, false) => Modifier::Rest, - (false, false, true) => Modifier::Flag, + let modifier = match (p.args.lazy, p.args.rest, p.args.flag, p.args.string) { + (false, false, false, false) => Modifier::None, + (true, false, false, false) => Modifier::Lazy, + (false, true, false, false) => Modifier::Rest, + (false, false, true, false) => Modifier::Flag, + (false, false, false, true) => Modifier::String, _ => { let message = "modifiers like #[lazy] or #[rest] currently cannot be used together"; return Err(syn::Error::new(p.span, message)); @@ -33,6 +35,7 @@ fn quote_parameter(p: &super::CommandParameter) -> Result quote::quote! { #[lazy] (#type_) }, Modifier::Rest => quote::quote! { #[rest] (#type_) }, + Modifier::String => quote::quote! { #[string] (#type_) }, Modifier::None => quote::quote! { (#type_) }, }) } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 76158deb785..802be75e9b2 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -88,9 +88,10 @@ 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`. -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: +All following parameters are inputs to the command. You can use all types that implement +`PopArgument` (for prefix commands) or `SlashArgument` (for slash commands). 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: ## Meta properties @@ -109,6 +110,7 @@ are multiple attributes you can use on parameters: - `#[max_length = 1]`: Maximum length for this string parameter (slash-only) ## Parser settings (prefix only) +- `#[string]`: Indicates that a type implements `FromStr` and should be parsed from a string argument. - `#[rest]`: Use the entire rest of the message for this parameter (prefix-only) - `#[lazy]`: Can be used on Option and Vec parameters and is equivalent to regular expressions' laziness (prefix-only) - `#[flag]`: Can be used on a bool parameter to set the bool to true if the user typed the parameter name literally (prefix-only) diff --git a/src/choice_parameter.rs b/src/choice_parameter.rs index 32f0b168cac..4bdd7096057 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?; + String::pop_from(args, attachment_index, ctx, msg).await?; Ok(( args, diff --git a/src/lib.rs b/src/lib.rs index 032a9ef40ca..371400bbac1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,7 +190,7 @@ 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 = "Consectetur"] #[string] ip_addr: std::net::IpAddr, // implements FromStr #[description = "Amet"] user: serenity::Member, // implements ArgumentConvert #[description = "Sit"] code_block: poise::CodeBlock, // implements PopArgument #[description = "Dolor"] #[flag] my_flag: bool, diff --git a/src/prefix_argument/argument_trait.rs b/src/prefix_argument/argument_trait.rs index 8a6bf6fee7a..93dc0dd3bfd 100644 --- a/src/prefix_argument/argument_trait.rs +++ b/src/prefix_argument/argument_trait.rs @@ -1,106 +1,43 @@ -//! 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; -use std::marker::PhantomData; -/// Full version of [`crate::PopArgument::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) - }}; -} +/// The result of [`PopArgument::pop_from`]. +/// - 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. - /// - /// Don't call this method directly! Use [`crate::pop_prefix_argument!`] + /// Pops an argument from the `args` string, and parses it as `Self`. async fn pop_from( args: &'a str, attachment_index: usize, ctx: &serenity::Context, msg: &serenity::Message, - ) -> Result<(&'a str, usize, Self), (Box, Option)>; + ) -> PopArgumentResult<'a, Self>; } -#[doc(hidden)] #[async_trait::async_trait] -pub trait PopArgumentHack<'a, T>: Sized { +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, 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, -{ - 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)) - } -} - -#[async_trait::async_trait] -impl<'a, T: PopArgument<'a> + Send + Sync> PopArgumentHack<'a, T> for &PhantomData { - 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 - } -} - -#[async_trait::async_trait] -impl<'a> PopArgumentHack<'a, bool> for &PhantomData { - 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 +52,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,6 +69,81 @@ impl<'a> PopArgumentHack<'a, serenity::Attachment> for &PhantomData PopArgument<'a> for String { + async fn pop_from( + args: &'a str, + attachment_index: usize, + ctx: &serenity::Context, + msg: &serenity::Message, + ) -> PopArgumentResult<'a, Self> { + match pop_string(args) { + Ok((args, string)) => Ok((args, attachment_index, string)), + Err(err) => Err((err.into(), Some(args.into()))), + } + } +} + +async fn pop_from_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)))?; + + Ok((args.trim_start(), attachment_index, object)) +} + +macro_rules! argumentconvert_pop_argument { + ( $( + $( #[cfg(feature = $feature:literal)] )? + $type:ty, + )* ) => { + $( + $( #[cfg(feature = $feature)] )? + #[async_trait::async_trait] + impl<'a> PopArgument<'a> for $type { + async fn pop_from( + args: &'a str, + attachment_index: usize, + ctx: &serenity::Context, + msg: &serenity::Message, + ) -> PopArgumentResult<'a, Self> { + pop_from_argumentconvert(args, attachment_index, ctx, msg).await + } + } + )* + } +} + +argumentconvert_pop_argument! { + // Via blanket impl of `ArgumentConvert` for `T: FromStr` + f32, f64, + u8, u16, u32, u64, + i8, i16, i32, i64, + serenity::Mention, + + // Via explicit `ArgumentConvert` impls + serenity::User, serenity::Member, + serenity::Message, + serenity::Channel, serenity::GuildChannel, + serenity::EmojiId, serenity::Emoji, + serenity::Role, + + #[cfg(feature = "cache")] + serenity::GuildId, + #[cfg(feature = "cache")] + serenity::Guild, +} + /// Macro to allow for using mentions in snowflake types macro_rules! snowflake_pop_argument { ($type:ty, $parse_fn:ident, $error_type:ident) => { @@ -158,17 +166,13 @@ macro_rules! snowflake_pop_argument { } #[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), - > { + ) -> PopArgumentResult<'a, Self> { let (args, string) = pop_string(args).map_err(|_| (TooFewArguments::default().into(), None))?; diff --git a/src/prefix_argument/macros.rs b/src/prefix_argument/macros.rs index 3a86d3884c4..163417e769d 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)* ); }, @@ -151,12 +151,30 @@ macro_rules! _parse_prefix { } }; + // Consume #[string] TYPE + ( $ctx:ident $msg:ident $args:ident $attachment_index:ident => [ $error:ident $($preamble:tt)* ] + (#[string] $type:ty $(,)?) + $( $rest:tt )* + ) => { + match ::pop_from(&$args, $attachment_index, $ctx, $msg).await { + Ok(($args, $attachment_index, token)) => { + match <$type as ::std::str::FromStr>::from_str(&token) { + Ok(token) => { + $crate::_parse_prefix!($ctx $msg $args $attachment_index => [ $error $($preamble)* token ] $($rest)* ); + }, + Err(e) => $error = (e.into(), Some(token)), + } + }, + Err(e) => $error = e, + } + }; + // Consume T ( $ctx:ident $msg:ident $args:ident $attachment_index:ident => [ $error:ident $($preamble:tt)* ] ($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)* ); }, diff --git a/src/prefix_argument/mod.rs b/src/prefix_argument/mod.rs index fdfa6e53471..e5de88df6c1 100644 --- a/src/prefix_argument/mod.rs +++ b/src/prefix_argument/mod.rs @@ -140,6 +140,7 @@ fn test_pop_string() { (r#"\"AA BB\""#, r#""AA"#), (r#"\"AA\ BB\""#, r#""AA BB""#), (r#""\"AA BB\"""#, r#""AA BB""#), + (r#" AA BB"#, r#"AA"#), ] { assert_eq!(pop_string(string).unwrap().1, arg); } From 11e9052472662303f35b71695dff688c27a49c5a Mon Sep 17 00:00:00 2001 From: Michael Krasnitski Date: Wed, 9 Jul 2025 14:08:34 -0400 Subject: [PATCH 2/3] Remove autoref specialization for `SlashArgument` Replaces hacky blanket impls with macro-generated impls for specific types, including some types that didn't have impls before, to line up with the list of types that implement `PopArgument`. --- macros/src/command/slash.rs | 4 +- src/slash_argument/slash_macro.rs | 4 +- src/slash_argument/slash_trait.rs | 173 +++++++++++------------------- 3 files changed, 63 insertions(+), 118 deletions(-) diff --git a/macros/src/command/slash.rs b/macros/src/command/slash.rs index d2be02a430f..bb316e8c9a1 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..8a9142305e6 100644 --- a/src/slash_argument/slash_macro.rs +++ b/src/slash_argument/slash_macro.rs @@ -130,7 +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) + Some(<$type as $crate::SlashArgument>::extract($ctx, $interaction, &arg.value) .await?) } else { None @@ -187,8 +187,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..46f5e085ccd 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 +//! Traits for slash command parameters. use super::SlashArgError; use std::convert::TryInto as _; -use std::marker::PhantomData; #[allow(unused_imports)] // import is required if serenity simdjson feature is enabled use crate::serenity::json::*; @@ -12,9 +11,7 @@ use crate::{serenity_prelude as serenity, CowVec}; #[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!`] + /// [`serenity::ResolvedValue`]. async fn extract( ctx: &serenity::Context, interaction: &serenity::CommandInteraction, @@ -25,110 +22,79 @@ 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() - } -} - -/// 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 +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(), - }) - } + T::convert( + ctx, + interaction.guild_id, + Some(interaction.channel_id), + string, + ) + .await + .map_err(|e| SlashArgError::Parse { + error: e.into(), + input: string.into(), + }) +} + +macro_rules! argumentconvert_slash_argument { + ( $( + $( #[cfg(feature = $feature:literal)] )? + $type:ty, + ) *) => { + $( + $( #[cfg(feature = $feature)] )? + #[async_trait::async_trait] + impl SlashArgument for $type { + async fn extract( + ctx: &serenity::Context, + interaction: &serenity::CommandInteraction, + value: &serenity::ResolvedValue<'_>, + ) -> Result { + extract_via_argumentconvert(ctx, interaction, value).await + } - 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) + } + } + )* } } +argumentconvert_slash_argument! { + serenity::Message, + serenity::EmojiId, serenity::Emoji, + #[cfg(feature = "cache")] + serenity::GuildId, + #[cfg(feature = "cache")] + serenity::Guild, +} + /// Implements slash argument trait for integer types macro_rules! impl_for_integer { ($($t:ty)*) => { $( @@ -162,27 +128,7 @@ 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() - } -} - -/// 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 +156,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 46904709b4f773861abdbfa5f207f393f0ba0655 Mon Sep 17 00:00:00 2001 From: Michael Krasnitski Date: Mon, 14 Jul 2025 15:01:34 -0400 Subject: [PATCH 3/3] Add docs --- src/prefix_argument/argument_trait.rs | 2 ++ src/slash_argument/slash_trait.rs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/prefix_argument/argument_trait.rs b/src/prefix_argument/argument_trait.rs index 93dc0dd3bfd..913a1efcf29 100644 --- a/src/prefix_argument/argument_trait.rs +++ b/src/prefix_argument/argument_trait.rs @@ -84,6 +84,7 @@ impl<'a> PopArgument<'a> for String { } } +/// Pops a string and then converts it to `T` using its `ArgumentConvert` implementation. async fn pop_from_argumentconvert<'a, T>( args: &'a str, attachment_index: usize, @@ -102,6 +103,7 @@ where Ok((args.trim_start(), attachment_index, object)) } +/// Auto-impls `PopArgument` for a type by deferring to [`pop_from_argumentconvert`]. macro_rules! argumentconvert_pop_argument { ( $( $( #[cfg(feature = $feature:literal)] )? diff --git a/src/slash_argument/slash_trait.rs b/src/slash_argument/slash_trait.rs index 46f5e085ccd..d112a146845 100644 --- a/src/slash_argument/slash_trait.rs +++ b/src/slash_argument/slash_trait.rs @@ -30,6 +30,8 @@ pub trait SlashArgument: Sized { } } +/// Extracts a string argument and then converts it to `T` using its `ArgumentConvert` +/// implementation. async fn extract_via_argumentconvert( ctx: &serenity::Context, interaction: &serenity::CommandInteraction, @@ -61,6 +63,7 @@ where }) } +/// Auto-impls `SlashArgument` for a type by deferring to [`extract_via_argumentconvert`]. macro_rules! argumentconvert_slash_argument { ( $( $( #[cfg(feature = $feature:literal)] )?