diff --git a/macros/src/command/slash.rs b/macros/src/command/slash.rs index d2be02a430f..eccf4bd672a 100644 --- a/macros/src/command/slash.rs +++ b/macros/src/command/slash.rs @@ -1,8 +1,9 @@ -use super::Invocation; +use super::{Invocation, ParamArgs}; use crate::util::{ - extract_type_parameter, iter_tuple_2_to_vec_map, tuple_2_iter_deref, wrap_option_to_string, + extract_type_parameter, iter_tuple_2_to_vec_map, tuple_2_iter_deref, wrap_option, + wrap_option_to_string, }; -use quote::format_ident; +use quote::{format_ident, quote}; use syn::spanned::Spanned as _; fn lit_to_string(lit: &syn::Lit) -> Result { @@ -20,6 +21,40 @@ fn lit_to_string(lit: &syn::Lit) -> Result { } } +fn generate_value_limits( + args: &ParamArgs, + span: proc_macro2::Span, +) -> syn::Result> { + let limits = match (&args.min, &args.max, &args.min_length, &args.max_length) { + (None, None, None, Some(max)) => { + quote!( ::poise::ValueLimits::Length { min: None, max: Some(#max) } ) + } + (None, None, Some(min), None) => { + quote!( ::poise::ValueLimits::Length { min: Some(#min), max: None } ) + } + (None, None, Some(min), Some(max)) => { + quote!( ::poise::ValueLimits::Length { min: Some(#min), max: Some(#max) } ) + } + (None, Some(max), None, None) => { + quote!( ::poise::ValueLimits::Value { min: None, max: Some((#max) as f64) } ) + } + (Some(min), None, None, None) => { + quote!( ::poise::ValueLimits::Value { min: Some((#min) as f64), max: None } ) + } + (Some(min), Some(max), None, None) => { + quote!( ::poise::ValueLimits::Value { min: Some((#min) as f64), max: Some((#max) as f64) } ) + } + + (None, None, None, None) => return Ok(None), + _ => { + let err = "Cannot set both a `min_length/max_length` and a `min/max`"; + return Err(syn::Error::new(span, err)); + } + }; + + Ok(Some(limits)) +} + pub fn generate_parameters(inv: &Invocation) -> Result, syn::Error> { let mut parameter_structs = Vec::new(); for param in &inv.parameters { @@ -47,47 +82,27 @@ pub fn generate_parameters(inv: &Invocation) -> Result { - quote::quote! { Some(| + quote! { Some(| ctx: poise::ApplicationContext<'_, _, _>, partial: &str, | Box::pin(#autocomplete_fn(ctx.into(), partial))) } } - None => quote::quote! { None }, + None => quote! { None }, }; - // We can just cast to f64 here because Discord only uses f64 precision anyways - // TODO: move this to poise::CommandParameter::{min, max} fields - let min_value_setter = match ¶m.args.min { - Some(x) => quote::quote! { .min_number_value(#x as f64) }, - None => quote::quote! {}, - }; - let max_value_setter = match ¶m.args.max { - Some(x) => quote::quote! { .max_number_value(#x as f64) }, - None => quote::quote! {}, - }; - // TODO: move this to poise::CommandParameter::{min_length, max_length} fields - let min_length_setter = match ¶m.args.min_length { - Some(x) => quote::quote! { .min_length(#x) }, - None => quote::quote! {}, - }; - let max_length_setter = match ¶m.args.max_length { - Some(x) => quote::quote! { .max_length(#x) }, - None => quote::quote! {}, - }; + let value_limits = wrap_option(generate_value_limits(¶m.args, param.span)?); + let type_setter = match inv.args.slash_command { true => { if let Some(_choices) = ¶m.args.choices { - quote::quote! { Some(|o| o.kind(::poise::serenity_prelude::CommandOptionType::Integer)) } + quote! { Some(|o| o.kind(::poise::serenity_prelude::CommandOptionType::Integer)) } } else { - quote::quote! { Some(|o| { - poise::create_slash_argument!(#type_, o) - #min_value_setter #max_value_setter - #min_length_setter #max_length_setter - }) } + quote! { Some(|o| poise::create_slash_argument!(#type_, o)) } } } - false => quote::quote! { None }, + false => quote! { None }, }; + // TODO: theoretically a problem that we don't store choices for non slash commands // TODO: move this to poise::CommandParameter::choices (is there a reason not to?) let choices = if inv.args.slash_command { @@ -95,27 +110,27 @@ pub fn generate_parameters(inv: &Invocation) -> Result = choices_iter.map(lit_to_string).collect::>()?; - quote::quote! { Cow::Borrowed(&[#( ::poise::CommandParameterChoice { + quote! { Cow::Borrowed(&[#( ::poise::CommandParameterChoice { name: Cow::Borrowed(#choices), localizations: Cow::Borrowed(&[]), __non_exhaustive: (), } ),*]) } } else { - quote::quote! { poise::slash_argument_choices!(#type_) } + quote! { poise::slash_argument_choices!(#type_) } } } else { - quote::quote! { Cow::Borrowed(&[]) } + quote! { Cow::Borrowed(&[]) } }; let channel_types = match ¶m.args.channel_types { - Some(crate::util::List(channel_types)) => quote::quote! { Some( + Some(crate::util::List(channel_types)) => quote! { Some( Cow::Borrowed(&[ #( poise::serenity_prelude::ChannelType::#channel_types ),* ]) ) }, - None => quote::quote! { None }, + None => quote! { None }, }; parameter_structs.push(( - quote::quote! { + quote! { ::poise::CommandParameter { name: ::std::borrow::Cow::Borrowed(#param_name), name_localizations: #name_localizations, @@ -123,6 +138,7 @@ pub fn generate_parameters(inv: &Invocation) -> Result Result>(); - Ok(quote::quote! { + Ok(quote! { |ctx| Box::pin(async move { // idk why this can't be put in the macro itself (where the lint is triggered) and // why clippy doesn't turn off this lint inside macros in the first place @@ -216,7 +232,7 @@ pub fn generate_context_menu_action( } }; - Ok(quote::quote! { + Ok(quote! { <#param_type as ::poise::ContextMenuParameter<_, _>>::to_action(|ctx, value| { Box::pin(async move { let is_framework_cooldown = !ctx.command.manual_cooldowns diff --git a/src/structs/slash.rs b/src/structs/slash.rs index e5abbcbe1e7..ca9082a46ea 100644 --- a/src/structs/slash.rs +++ b/src/structs/slash.rs @@ -109,6 +109,55 @@ impl Clone for ContextMenuCommandAction { } } +/// An enum to hold the different limits a CommandParameter may have. +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum ValueLimits { + /// Used if the CommandParameter is a string. + Length { + /// See [`serenity::CreateCommandOption::min_length`] + min: Option, + /// See [`serenity::CreateCommandOption::max_length`] + max: Option, + }, + /// Used if the CommandParameter is an integer or number. + Value { + /// See [`serenity::CreateCommandOption::min_number_value`] + min: Option, + /// See [`serenity::CreateCommandOption::max_number_value`] + max: Option, + }, +} + +impl ValueLimits { + /// Applies the limits to a [`serenity::CreateCommandOption`]. + pub fn apply_to_slash_command_option( + self, + mut builder: serenity::CreateCommandOption, + ) -> serenity::CreateCommandOption { + match self { + ValueLimits::Length { min, max } => { + if let Some(min) = min { + builder = builder.min_length(min); + } + if let Some(max) = max { + builder = builder.max_length(max); + } + } + ValueLimits::Value { min, max } => { + if let Some(min) = min { + builder = builder.min_number_value(min); + } + if let Some(max) = max { + builder = builder.max_number_value(max); + } + } + } + + builder + } +} + /// A single drop-down choice in a slash command choice parameter #[derive(Debug, Clone)] pub struct CommandParameterChoice { @@ -140,13 +189,15 @@ pub struct CommandParameter { pub channel_types: Option>, /// If this parameter is a choice parameter, this is the fixed list of options pub choices: CowVec, + /// For String or Number argument types, this contains the limits. + pub value_limits: Option, /// Closure that sets this parameter's type and min/max value in the given builder /// /// For example a u32 [`CommandParameter`] would store this as the [`Self::type_setter`]: /// ```rust /// # use poise::serenity_prelude as serenity; /// # let _: fn(serenity::CreateCommandOption) -> serenity::CreateCommandOption = - /// |b| b.kind(serenity::CommandOptionType::Integer).min_int_value(0).max_int_value(u64::MAX) + /// |b| b.kind(serenity::CommandOptionType::Integer) /// # ; /// ``` #[derivative(Debug = "ignore")] @@ -203,6 +254,9 @@ impl CommandParameter { .map(|(name, description)| (name.as_ref(), description.as_ref())), ); } + if let Some(value_limits) = self.value_limits { + builder = value_limits.apply_to_slash_command_option(builder) + } Some((self.type_setter?)(builder)) }