Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get rid of autoref-specialisation #320

Open
wants to merge 4 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions macros/src/command/slash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub fn generate_parameters(inv: &Invocation) -> Result<Vec<proc_macro2::TokenStr
quote::quote! { Some(|o| o.kind(::poise::serenity_prelude::CommandOptionType::Integer)) }
} else {
quote::quote! { Some(|o| {
poise::create_slash_argument!(#type_, o)
<#type_ as poise::SlashArgument>::create(o)
#min_value_setter #max_value_setter
#min_length_setter #max_length_setter
}) }
Expand All @@ -101,7 +101,7 @@ pub fn generate_parameters(inv: &Invocation) -> Result<Vec<proc_macro2::TokenStr
__non_exhaustive: (),
} ),*]) }
} else {
quote::quote! { poise::slash_argument_choices!(#type_) }
quote::quote! { <#type_ as ::poise::SlashArgument>::choices() }
}
} else {
quote::quote! { Cow::Borrowed(&[]) }
Expand Down
2 changes: 1 addition & 1 deletion macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
61 changes: 61 additions & 0 deletions src/argument.rs
Original file line number Diff line number Diff line change
@@ -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<T>(pub T);

#[async_trait::async_trait]
impl<T> SlashArgument for StrArg<T>
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<Self, SlashArgError> {
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<T>
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))),
}
}
}
7 changes: 3 additions & 4 deletions src/choice_parameter.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<dyn std::error::Error + Send + Sync>, Option<String>)>
{
) -> PopArgumentResult<'a, Self> {
let (args, attachment_index, s) =
crate::pop_prefix_argument!(String, args, attachment_index, ctx, msg).await?;
<String as crate::PopArgument<'a>>::pop_from(args, attachment_index, ctx, msg).await?;

Ok((
args,
Expand Down
11 changes: 6 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
Expand All @@ -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<std::net::IpAddr>,
#[description = "Sit"] code_block: poise::CodeBlock,
#[description = "Dolor"] #[flag] my_flag: bool,
#[description = "Ipsum"] #[lazy] always_none: Option<String>,
#[description = "Lorem"] #[rest] rest: String,
Expand Down Expand Up @@ -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;
Expand All @@ -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::*,
};

Expand Down
172 changes: 59 additions & 113 deletions src/prefix_argument/argument_trait.rs
Original file line number Diff line number Diff line change
@@ -1,106 +1,60 @@
//! 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`].
/// The result of `<T as 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)
}};
}
/// 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<dyn std::error::Error + Send + Sync>, Option<String>)>;

/// 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<dyn std::error::Error + Send + Sync>, Option<String>)>;
}

#[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<dyn std::error::Error + Send + Sync>, Option<String>)>;
}

#[async_trait::async_trait]
impl<'a, T: serenity::ArgumentConvert + Send> PopArgumentHack<'a, T> for PhantomData<T>
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<dyn std::error::Error + Send + Sync>, Option<String>)>
{
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<T> {
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<dyn std::error::Error + Send + Sync>, Option<String>)>
{
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<bool> {
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<dyn std::error::Error + Send + Sync>, Option<String>)>
{
) -> PopArgumentResult<'a, Self> {
let (args, string) =
pop_string(args).map_err(|_| (TooFewArguments::default().into(), None))?;

Expand All @@ -115,17 +69,13 @@ impl<'a> PopArgumentHack<'a, bool> for &PhantomData<bool> {
}

#[async_trait::async_trait]
impl<'a> PopArgumentHack<'a, serenity::Attachment> for &PhantomData<serenity::Attachment> {
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<dyn std::error::Error + Send + Sync>, Option<String>),
> {
) -> PopArgumentResult<'a, Self> {
let attachment = msg
.attachments
.get(attachment_index)
Expand All @@ -136,56 +86,52 @@ impl<'a> PopArgumentHack<'a, serenity::Attachment> for &PhantomData<serenity::At
}
}

/// Macro to allow for using mentions in snowflake types
macro_rules! snowflake_pop_argument {
($type:ty, $parse_fn:ident, $error_type:ident) => {
/// 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<dyn std::error::Error + Send + Sync>, Option<String>),
> {
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::Member,
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);
Loading
Loading