Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
182 changes: 93 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,81 @@ 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()))),
}
}
}

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) => {
Expand All @@ -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<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