Skip to content
Merged
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
1 change: 1 addition & 0 deletions macros/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ struct ParamArgs {
max: Option<syn::Lit>,
min_length: Option<syn::Lit>,
max_length: Option<syn::Lit>,
string: bool,
lazy: bool,
flag: bool,
rest: bool,
Expand Down
13 changes: 8 additions & 5 deletions macros/src/command/prefix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ fn quote_parameter(p: &super::CommandParameter) -> Result<proc_macro2::TokenStre
Lazy,
Flag,
Rest,
String,
}
let modifier = match (p.args.lazy, p.args.rest, p.args.flag) {
(false, false, false) => 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));
Expand All @@ -33,6 +35,7 @@ fn quote_parameter(p: &super::CommandParameter) -> Result<proc_macro2::TokenStre
}
Modifier::Lazy => quote::quote! { #[lazy] (#type_) },
Modifier::Rest => quote::quote! { #[rest] (#type_) },
Modifier::String => quote::quote! { #[string] (#type_) },
Modifier::None => quote::quote! { (#type_) },
})
}
Expand Down
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
8 changes: 5 additions & 3 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
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::pop_from(args, attachment_index, ctx, msg).await?;

Ok((
args,
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
184 changes: 95 additions & 89 deletions src/prefix_argument/argument_trait.rs
Original file line number Diff line number Diff line change
@@ -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<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.
///
/// 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<dyn std::error::Error + Send + Sync>, Option<String>)>;
) -> 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<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,
{
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))
}
}

#[async_trait::async_trait]
impl<'a, T: PopArgument<'a> + Send + Sync> PopArgumentHack<'a, T> for &PhantomData<T> {
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
}
}

#[async_trait::async_trait]
impl<'a> PopArgumentHack<'a, bool> for &PhantomData<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 +52,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,6 +69,83 @@ impl<'a> PopArgumentHack<'a, serenity::Attachment> for &PhantomData<serenity::At
}
}

#[async_trait::async_trait]
impl<'a> 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()))),
}
}
}

/// 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,
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))
}

/// Auto-impls `PopArgument` for a type by deferring to [`pop_from_argumentconvert`].
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) => {
Expand All @@ -158,17 +168,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<dyn std::error::Error + Send + Sync>, Option<String>),
> {
) -> PopArgumentResult<'a, Self> {
let (args, string) =
pop_string(args).map_err(|_| (TooFewArguments::default().into(), None))?;

Expand Down
Loading
Loading