diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 306faec8fc03..1e33f4c4a6d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: include: - name: MSRV - toolchain: 1.74.0 + toolchain: 1.80.0 # don't do doctests because they rely on new features for brevity # copy known Cargo.lock to avoid random dependency MSRV bumps to mess up our test command: cp .github/Cargo-msrv.lock Cargo.lock && cargo test --all-features --lib --tests diff --git a/Cargo.toml b/Cargo.toml index 6f23c262fb7f..65b26c8df06d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ authors = ["kangalio "] edition = "2021" name = "poise" version = "0.6.1" -rust-version = "1.74.0" +rust-version = "1.80.0" description = "A Discord bot framework for serenity" license = "MIT" repository = "https://github.com/serenity-rs/poise/" diff --git a/README.md b/README.md index 628d83fb3e13..f65dd020256d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Docs](https://img.shields.io/badge/docs-online-informational)](https://docs.rs/poise/) [![Docs (git)](https://img.shields.io/badge/docs%20%28git%29-online-informational)](https://serenity-rs.github.io/poise/) [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Rust: 1.74+](https://img.shields.io/badge/rust-1.74+-93450a)](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html) +[![Rust: 1.80+](https://img.shields.io/badge/rust-1.80+-93450a)](https://blog.rust-lang.org/2024/07/25/Rust-1.80.0.html) # Poise Poise is an opinionated Discord bot framework with a few distinctive features: diff --git a/examples/fluent_localization/main.rs b/examples/fluent_localization/main.rs index f215a3a99584..db82185bf342 100644 --- a/examples/fluent_localization/main.rs +++ b/examples/fluent_localization/main.rs @@ -4,7 +4,7 @@ use poise::serenity_prelude as serenity; use translation::tr; pub struct Data { - translations: translation::Translations, + translations: &'static translation::Translations, } type Error = Box; @@ -62,7 +62,12 @@ async fn main() { let mut commands = vec![welcome(), info(), register()]; let translations = translation::read_ftl().expect("failed to read translation files"); - translation::apply_translations(&translations, &mut commands); + + // We leak the translations so we can easily copy around `&'static str`s, to the downside + // that the OS will reclaim the memory at the end of `main` instead of the Drop implementation. + let translations: &'static translation::Translations = Box::leak(Box::new(translations)); + + translation::apply_translations(translations, &mut commands); let token = std::env::var("TOKEN").unwrap(); let intents = serenity::GatewayIntents::non_privileged(); diff --git a/examples/fluent_localization/translation.rs b/examples/fluent_localization/translation.rs index 250c20e40650..2961550000c6 100644 --- a/examples/fluent_localization/translation.rs +++ b/examples/fluent_localization/translation.rs @@ -1,5 +1,7 @@ //! Wraps the fluent API and provides easy to use functions and macros for translation +use std::borrow::Cow; + use crate::{Context, Data, Error}; type FluentBundle = fluent::bundle::FluentBundle< @@ -30,27 +32,27 @@ pub(crate) use tr; /// Given a language file and message identifier, returns the translation pub fn format( - bundle: &FluentBundle, + bundle: &'static FluentBundle, id: &str, attr: Option<&str>, args: Option<&fluent::FluentArgs<'_>>, -) -> Option { +) -> Option> { let message = bundle.get_message(id)?; let pattern = match attr { Some(attribute) => message.get_attribute(attribute)?.value(), None => message.value()?, }; let formatted = bundle.format_pattern(pattern, args, &mut vec![]); - Some(formatted.into_owned()) + Some(formatted) } /// Retrieves the appropriate language file depending on user locale and calls [`format`] pub fn get( ctx: Context, - id: &str, + id: &'static str, attr: Option<&str>, args: Option<&fluent::FluentArgs<'_>>, -) -> String { +) -> Cow<'static, str> { let translations = &ctx.data().translations; ctx.locale() // Try to get the language-specific translation @@ -60,7 +62,7 @@ pub fn get( // If this message ID is not present in any translation files whatsoever .unwrap_or_else(|| { tracing::warn!("unknown fluent message identifier `{}`", id); - id.to_string() + Cow::Borrowed(id) }) } @@ -97,7 +99,7 @@ pub fn read_ftl() -> Result { /// Given a set of language files, fills in command strings and their localizations accordingly pub fn apply_translations( - translations: &Translations, + translations: &'static Translations, commands: &mut [poise::Command], ) { for command in &mut *commands { @@ -108,21 +110,24 @@ pub fn apply_translations( Some(x) => x, None => continue, // no localization entry => skip localization }; - command - .name_localizations - .insert(locale.clone(), localized_command_name); - command.description_localizations.insert( + + let locale = Cow::Borrowed(locale.as_str()); + let name_localizations = command.name_localizations.to_mut(); + let description_localizations = command.description_localizations.to_mut(); + + name_localizations.push((locale.clone(), localized_command_name)); + description_localizations.push(( locale.clone(), format(bundle, &command.name, Some("description"), None).unwrap(), - ); + )); for parameter in &mut command.parameters { // Insert localized parameter name and description - parameter.name_localizations.insert( + parameter.name_localizations.to_mut().push(( locale.clone(), format(bundle, &command.name, Some(¶meter.name), None).unwrap(), - ); - parameter.description_localizations.insert( + )); + parameter.description_localizations.to_mut().push(( locale.clone(), format( bundle, @@ -131,14 +136,14 @@ pub fn apply_translations( None, ) .unwrap(), - ); + )); // If this is a choice parameter, insert its localized variants - for choice in &mut parameter.choices { - choice.localizations.insert( + for choice in parameter.choices.to_mut().iter_mut() { + choice.localizations.to_mut().push(( locale.clone(), format(bundle, &choice.name, None, None).unwrap(), - ); + )); } } } @@ -170,7 +175,7 @@ pub fn apply_translations( ); // If this is a choice parameter, set the choice names to en-US - for choice in &mut parameter.choices { + for choice in parameter.choices.to_mut().iter_mut() { choice.name = format(bundle, &choice.name, None, None).unwrap(); } } diff --git a/macros/src/choice_parameter.rs b/macros/src/choice_parameter.rs index e84005371af1..0e66eea39e1f 100644 --- a/macros/src/choice_parameter.rs +++ b/macros/src/choice_parameter.rs @@ -67,14 +67,16 @@ pub fn choice_parameter(input: syn::DeriveInput) -> Result Vec { - vec![ #( poise::CommandParameterChoice { + fn list() -> std::borrow::Cow<'static, [poise::CommandParameterChoice]> { + use ::std::borrow::Cow; + + Cow::Borrowed(&[ #( poise::CommandParameterChoice { __non_exhaustive: (), - name: #names.to_string(), - localizations: std::collections::HashMap::from([ - #( (#locales.to_string(), #localized_names.to_string()) ),* + name: Cow::Borrowed(#names), + localizations: Cow::Borrowed(&[ + #( (Cow::Borrowed(#locales), Cow::Borrowed(#localized_names)) ),* ]), - }, )* ] + }, )* ]) } fn from_index(index: usize) -> Option { diff --git a/macros/src/command/mod.rs b/macros/src/command/mod.rs index 9369ff39a253..3fdb8ecdff71 100644 --- a/macros/src/command/mod.rs +++ b/macros/src/command/mod.rs @@ -2,7 +2,7 @@ mod prefix; mod slash; use crate::util::{ - iter_tuple_2_to_hash_map, wrap_option, wrap_option_and_map, wrap_option_to_string, + iter_tuple_2_to_vec_map, wrap_option, wrap_option_and_map, wrap_option_to_string, }; use proc_macro::TokenStream; use syn::spanned::Spanned as _; @@ -343,9 +343,9 @@ fn generate_command(mut inv: Invocation) -> Result quote::quote! { Box::new(()) }, }; - let name_localizations = iter_tuple_2_to_hash_map(inv.args.name_localized.into_iter()); + let name_localizations = iter_tuple_2_to_vec_map(inv.args.name_localized.into_iter()); let description_localizations = - iter_tuple_2_to_hash_map(inv.args.description_localized.into_iter()); + iter_tuple_2_to_vec_map(inv.args.description_localized.into_iter()); let function_ident = std::mem::replace(&mut inv.function.sig.ident, syn::parse_quote! { inner }); @@ -359,6 +359,8 @@ fn generate_command(mut inv: Invocation) -> Result::U, <#ctx_type_with_static as poise::_GetGenerics>::E, > { + use ::std::borrow::Cow; + #function ::poise::Command { @@ -368,11 +370,11 @@ fn generate_command(mut inv: Invocation) -> Result Result Result { @@ -91,29 +90,26 @@ pub fn generate_parameters(inv: &Invocation) -> Result { - if let Some(List(choices)) = ¶m.args.choices { - let choices = choices - .iter() - .map(lit_to_string) - .collect::, _>>()?; - - quote::quote! { vec![#( ::poise::CommandParameterChoice { - name: String::from(#choices), - localizations: Default::default(), - __non_exhaustive: (), - } ),*] } - } else { - quote::quote! { poise::slash_argument_choices!(#type_) } - } + let choices = if inv.args.slash_command { + if let Some(choices) = ¶m.args.choices { + let choices_iter = choices.0.iter(); + let choices: Vec<_> = choices_iter.map(lit_to_string).collect::>()?; + + quote::quote! { Cow::Borrowed(&[#( ::poise::CommandParameterChoice { + name: Cow::Borrowed(#choices), + localizations: Cow::Borrowed(&[]), + __non_exhaustive: (), + } ),*]) } + } else { + quote::quote! { poise::slash_argument_choices!(#type_) } } - false => quote::quote! { vec![] }, + } else { + quote::quote! { Cow::Borrowed(&[]) } }; let channel_types = match ¶m.args.channel_types { Some(crate::util::List(channel_types)) => quote::quote! { Some( - vec![ #( poise::serenity_prelude::ChannelType::#channel_types ),* ] + Cow::Borrowed(&[ #( poise::serenity_prelude::ChannelType::#channel_types ),* ]) ) }, None => quote::quote! { None }, }; @@ -121,7 +117,7 @@ pub fn generate_parameters(inv: &Invocation) -> Result( } pub fn wrap_option_to_string(literal: Option) -> syn::Expr { - let to_string_path = quote::quote!(::std::string::ToString::to_string); - wrap_option_and_map(literal, to_string_path) + let cowstr_path = quote::quote!(Cow::Borrowed); + wrap_option_and_map(literal, cowstr_path) } /// Syn Fold to make all lifetimes 'static. Used to access trait items of a type without having its @@ -99,13 +99,13 @@ where .map(|Tuple2(t, v)| Tuple2(t.deref(), v.deref())) } -pub fn iter_tuple_2_to_hash_map(v: I) -> proc_macro2::TokenStream +pub fn iter_tuple_2_to_vec_map(v: I) -> proc_macro2::TokenStream where I: ExactSizeIterator>, T: quote::ToTokens, { if v.len() == 0 { - return quote::quote!(std::collections::HashMap::new()); + return quote::quote!(Cow::Borrowed(&[])); } let (keys, values) = v @@ -114,8 +114,8 @@ where .unzip::<_, _, Vec<_>, Vec<_>>(); quote::quote! { - std::collections::HashMap::from([ - #( (#keys.to_string(), #values.to_string()) ),* + Cow::Borrowed(&[ + #( (Cow::Borrowed(#keys), Cow::Borrowed(#values)) ),* ]) } } diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index 88b66dc44524..80c801eda93f 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -235,7 +235,7 @@ pub async fn autocomplete_command<'a, U, E>( .take(25); let choices = filtered_commands - .map(|cmd| serenity::AutocompleteChoice::from(&cmd.name)) + .map(|cmd| serenity::AutocompleteChoice::from(cmd.name.as_ref())) .collect(); serenity::CreateAutocompleteResponse::new().set_choices(choices) diff --git a/src/choice_parameter.rs b/src/choice_parameter.rs index 94b7753494a6..32f0b168cac2 100644 --- a/src/choice_parameter.rs +++ b/src/choice_parameter.rs @@ -1,13 +1,13 @@ //! Contains the [`ChoiceParameter`] trait and the blanket [`crate::SlashArgument`] and //! [`crate::PopArgument`] impl -use crate::serenity_prelude as serenity; +use crate::{serenity_prelude as serenity, CowVec}; /// This trait is implemented by [`crate::macros::ChoiceParameter`]. See its docs for more /// information pub trait ChoiceParameter: Sized { /// Returns all possible choices for this parameter, in the order they will appear in Discord. - fn list() -> Vec; + fn list() -> CowVec; /// Returns an instance of [`Self`] corresponding to the given index into [`Self::list()`] fn from_index(index: usize) -> Option; @@ -50,7 +50,7 @@ impl crate::SlashArgument for T { builder.kind(serenity::CommandOptionType::Integer) } - fn choices() -> Vec { + fn choices() -> CowVec { Self::list() } } diff --git a/src/framework/mod.rs b/src/framework/mod.rs index 2b0b9cee1c9d..6c6161b19d94 100644 --- a/src/framework/mod.rs +++ b/src/framework/mod.rs @@ -1,6 +1,6 @@ //! The central Framework struct that ties everything together. -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; pub use builder::*; @@ -218,7 +218,7 @@ pub fn set_qualified_names(commands: &mut [crate::Command]) { /// Fills in `qualified_name` fields by appending command name to the parent command name fn set_subcommand_qualified_names(parents: &str, commands: &mut [crate::Command]) { for cmd in commands { - cmd.qualified_name = format!("{} {}", parents, cmd.name); + cmd.qualified_name = Cow::Owned(format!("{} {}", parents, cmd.name)); set_subcommand_qualified_names(&cmd.qualified_name, &mut cmd.subcommands); } } diff --git a/src/slash_argument/slash_trait.rs b/src/slash_argument/slash_trait.rs index df52687a0d32..fda0001552b6 100644 --- a/src/slash_argument/slash_trait.rs +++ b/src/slash_argument/slash_trait.rs @@ -6,7 +6,7 @@ use std::marker::PhantomData; #[allow(unused_imports)] // import is required if serenity simdjson feature is enabled use crate::serenity::json::*; -use crate::serenity_prelude as serenity; +use crate::{serenity_prelude as serenity, CowVec}; /// Implement this trait on types that you want to use as a slash command parameter. #[async_trait::async_trait] @@ -32,8 +32,8 @@ pub trait SlashArgument: Sized { /// If this is a choice parameter, returns the choices /// /// Don't call this method directly! Use [`crate::slash_argument_choices!`] - fn choices() -> Vec { - Vec::new() + fn choices() -> CowVec { + CowVec::default() } } @@ -53,8 +53,8 @@ pub trait SlashArgumentHack: Sized { fn create(self, builder: serenity::CreateCommandOption) -> serenity::CreateCommandOption; - fn choices(self) -> Vec { - Vec::new() + fn choices(self) -> CowVec { + CowVec::default() } } @@ -177,7 +177,7 @@ impl SlashArgumentHack for &PhantomData { ::create(builder) } - fn choices(self) -> Vec { + fn choices(self) -> CowVec { ::choices() } } diff --git a/src/structs/command.rs b/src/structs/command.rs index b9d9829f174e..27cfb452e492 100644 --- a/src/structs/command.rs +++ b/src/structs/command.rs @@ -2,6 +2,8 @@ use crate::{serenity_prelude as serenity, BoxFuture}; +use super::{CowStr, CowVec}; + /// Type returned from `#[poise::command]` annotated functions, which contains all of the generated /// prefix and application commands #[derive(derivative::Derivative)] @@ -33,31 +35,31 @@ pub struct Command { /// Require a subcommand to be invoked pub subcommand_required: bool, /// Main name of the command. Aliases (prefix-only) can be set in [`Self::aliases`]. - pub name: String, + pub name: CowStr, /// Localized names with locale string as the key (slash-only) - pub name_localizations: std::collections::HashMap, + pub name_localizations: CowVec<(CowStr, CowStr)>, /// Full name including parent command names. /// /// Initially set to just [`Self::name`] and properly populated when the framework is started. - pub qualified_name: String, + pub qualified_name: CowStr, /// A string to identify this particular command within a list of commands. /// /// Can be configured via the [`crate::command`] macro (though it's probably not needed for most /// bots). If not explicitly configured, it falls back to the command function name. - pub identifying_name: String, + pub identifying_name: CowStr, /// The name of the `#[poise::command]`-annotated function - pub source_code_name: String, + pub source_code_name: CowStr, /// Identifier for the category that this command will be displayed in for help commands. - pub category: Option, + pub category: Option, /// Whether to hide this command in help menus. pub hide_in_help: bool, /// Short description of the command. Displayed inline in help menus and similar. - pub description: Option, + pub description: Option, /// Localized descriptions with locale string as the key (slash-only) - pub description_localizations: std::collections::HashMap, + pub description_localizations: CowVec<(CowStr, CowStr)>, /// Multiline description with detailed usage instructions. Displayed in the command specific /// help: `~help command_name` - pub help_text: Option, + pub help_text: Option, /// if `true`, disables automatic cooldown handling before this commands invocation. /// /// Will override [`crate::FrameworkOptions::manual_cooldowns`] allowing manual cooldowns @@ -114,7 +116,7 @@ pub struct Command { // ============= Prefix-specific data /// Alternative triggers for the command (prefix-only) - pub aliases: Vec, + pub aliases: CowVec, /// Whether to rerun the command if an existing invocation message is edited (prefix-only) pub invoke_on_edit: bool, /// Whether to delete the bot response if an existing invocation message is deleted (prefix-only) @@ -124,7 +126,7 @@ pub struct Command { // ============= Application-specific data /// Context menu specific name for this command, displayed in Discord's context menu - pub context_menu_name: Option, + pub context_menu_name: Option, /// Whether responses to this command should be ephemeral by default (application-only) pub ephemeral: bool, /// List of installation contexts for this command (application-only) @@ -159,11 +161,11 @@ impl Command { let description = self.description.as_deref().unwrap_or("A slash command"); let mut builder = serenity::CreateCommandOption::new(kind, self.name.clone(), description); - for (locale, name) in &self.name_localizations { - builder = builder.name_localized(locale, name); + for (locale, name) in self.name_localizations.iter() { + builder = builder.name_localized(locale.as_ref(), name.as_ref()); } - for (locale, description) in &self.description_localizations { - builder = builder.description_localized(locale, description); + for (locale, description) in self.description_localizations.iter() { + builder = builder.description_localized(locale.as_ref(), description.as_ref()); } if self.subcommands.is_empty() { @@ -191,11 +193,11 @@ impl Command { let mut builder = serenity::CreateCommand::new(self.name.clone()) .description(self.description.as_deref().unwrap_or("A slash command")); - for (locale, name) in &self.name_localizations { - builder = builder.name_localized(locale, name); + for (locale, name) in self.name_localizations.iter() { + builder = builder.name_localized(locale.as_ref(), name.as_ref()); } - for (locale, description) in &self.description_localizations { - builder = builder.description_localized(locale, description); + for (locale, description) in self.description_localizations.iter() { + builder = builder.description_localized(locale.as_ref(), description.as_ref()); } // This is_empty check is needed because Discord special cases empty diff --git a/src/structs/mod.rs b/src/structs/mod.rs index c0b36a21ec62..9f51390324a0 100644 --- a/src/structs/mod.rs +++ b/src/structs/mod.rs @@ -1,5 +1,7 @@ //! Plain data structs that define the framework configuration. +use std::borrow::Cow; + mod context; pub use context::*; @@ -17,3 +19,9 @@ pub use slash::*; mod framework_error; pub use framework_error::*; + +/// A type alias for `&'static str` or `String` +pub(crate) type CowStr = Cow<'static, str>; + +/// A type alias for `&'static [T]` or `Vec` +pub(crate) type CowVec = Cow<'static, [T]>; diff --git a/src/structs/slash.rs b/src/structs/slash.rs index ebbe2a7a9b7f..e5abbcbe1e73 100644 --- a/src/structs/slash.rs +++ b/src/structs/slash.rs @@ -2,6 +2,8 @@ use crate::{serenity_prelude as serenity, BoxFuture}; +use super::{CowStr, CowVec}; + /// Specifies if the current invokation is from a Command or Autocomplete. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum CommandInteractionType { @@ -111,9 +113,9 @@ impl Clone for ContextMenuCommandAction { #[derive(Debug, Clone)] pub struct CommandParameterChoice { /// Label of this choice - pub name: String, + pub name: CowStr, /// Localized labels with locale string as the key (slash-only) - pub localizations: std::collections::HashMap, + pub localizations: CowVec<(CowStr, CowStr)>, #[doc(hidden)] pub __non_exhaustive: (), } @@ -123,21 +125,21 @@ pub struct CommandParameterChoice { #[derivative(Debug(bound = ""))] pub struct CommandParameter { /// Name of this command parameter - pub name: String, + pub name: CowStr, /// Localized names with locale string as the key (slash-only) - pub name_localizations: std::collections::HashMap, + pub name_localizations: CowVec<(CowStr, CowStr)>, /// Description of the command. Required for slash commands - pub description: Option, + pub description: Option, /// Localized descriptions with locale string as the key (slash-only) - pub description_localizations: std::collections::HashMap, + pub description_localizations: CowVec<(CowStr, CowStr)>, /// `true` is this parameter is required, `false` if it's optional or variadic pub required: bool, /// If this parameter is a channel, users can only enter these channel types in a slash command /// /// Prefix commands are currently unaffected by this - pub channel_types: Option>, + pub channel_types: Option>, /// If this parameter is a choice parameter, this is the fixed list of options - pub choices: Vec, + pub choices: CowVec, /// 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`]: @@ -182,18 +184,24 @@ impl CommandParameter { .required(self.required) .set_autocomplete(self.autocomplete_callback.is_some()); - for (locale, name) in &self.name_localizations { - builder = builder.name_localized(locale, name); + for (locale, name) in self.name_localizations.iter() { + builder = builder.name_localized(locale.as_ref(), name.as_ref()); } - for (locale, description) in &self.description_localizations { - builder = builder.description_localized(locale, description); + for (locale, description) in self.description_localizations.iter() { + builder = builder.description_localized(locale.as_ref(), description.as_ref()); } - if let Some(channel_types) = self.channel_types.clone() { - builder = builder.channel_types(channel_types); + if let Some(channel_types) = self.channel_types.as_deref() { + builder = builder.channel_types(channel_types.to_owned()); } for (i, choice) in self.choices.iter().enumerate() { - builder = - builder.add_int_choice_localized(&choice.name, i as _, choice.localizations.iter()); + builder = builder.add_int_choice_localized( + choice.name.as_ref(), + i as _, + choice + .localizations + .iter() + .map(|(name, description)| (name.as_ref(), description.as_ref())), + ); } Some((self.type_setter?)(builder))