From 2f17c1153cf4c22f976f4950cc75cfa718fe3202 Mon Sep 17 00:00:00 2001 From: CircuitSacul Date: Sun, 9 Jul 2023 10:18:17 -0400 Subject: [PATCH] feat!: require fallbacks in ` desc_localizations` implementations for localized commands (#31) --- CHANGELOG.md | 7 + .../src/command/description.rs | 36 ++++ .../src/command/mod.rs | 1 + .../src/command/model/create_command.rs | 36 ++-- .../src/command/subcommand/create_command.rs | 19 +- .../src/command/localizations.rs | 41 +++++ twilight-interactions/src/command/mod.rs | 10 +- twilight-interactions/tests/localizations.rs | 169 ++++++++++++++++++ twilight-interactions/tests/subcommand.rs | 11 +- 9 files changed, 297 insertions(+), 33 deletions(-) create mode 100644 twilight-interactions-derive/src/command/description.rs create mode 100644 twilight-interactions/src/command/localizations.rs create mode 100644 twilight-interactions/tests/localizations.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 69ef77b..fe97e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - A basic example bot implementation has been added in the `examples` directory of the repository. +- `DescriptionLocalizations` and `NameLocalizations` structs + +### Changed +- `desc` and `desc_localizations` are now mutually exclusive +- `desc_localizations` and `name_localizations` must return the + `DescriptionLocalizations` and `NameLocalizations` structs, + respectively. ## [0.15.2] - 2023-06-23 ### Added diff --git a/twilight-interactions-derive/src/command/description.rs b/twilight-interactions-derive/src/command/description.rs new file mode 100644 index 0000000..df2e861 --- /dev/null +++ b/twilight-interactions-derive/src/command/description.rs @@ -0,0 +1,36 @@ +use quote::quote; + +use crate::parse::parse_doc; + +pub fn get_description( + desc_localizations: &Option, + desc: &Option, + span: proc_macro2::Span, + attrs: &[syn::Attribute], +) -> syn::Result { + if desc.is_some() && desc_localizations.is_some() { + return Err(syn::Error::new( + span, + "You can't specify `desc` and `desc_localizations`.", + )); + } + + let desc = match desc_localizations { + Some(path) => quote! { + { + let desc = #path(); + (desc.fallback, ::std::option::Option::Some(desc.localizations)) + } + }, + None => { + let desc = match desc { + Some(desc) => desc.clone(), + None => parse_doc(attrs, span)?, + }; + + quote! { (::std::convert::From::from(#desc), None) } + } + }; + + Ok(desc) +} diff --git a/twilight-interactions-derive/src/command/mod.rs b/twilight-interactions-derive/src/command/mod.rs index 514d95a..1ec5468 100644 --- a/twilight-interactions-derive/src/command/mod.rs +++ b/twilight-interactions-derive/src/command/mod.rs @@ -2,6 +2,7 @@ mod impls; +mod description; mod model; mod subcommand; diff --git a/twilight-interactions-derive/src/command/model/create_command.rs b/twilight-interactions-derive/src/command/model/create_command.rs index 8756c24..07aaba3 100644 --- a/twilight-interactions-derive/src/command/model/create_command.rs +++ b/twilight-interactions-derive/src/command/model/create_command.rs @@ -3,7 +3,7 @@ use quote::{quote, quote_spanned}; use syn::{spanned::Spanned, DeriveInput, Error, FieldsNamed, Result}; use super::parse::{channel_type, command_option_value, optional, StructField, TypeAttribute}; -use crate::parse::{find_attr, parse_doc}; +use crate::{command::description::get_description, parse::find_attr}; /// Implementation of `CreateCommand` derive macro pub fn impl_create_command(input: DeriveInput, fields: Option) -> Result { @@ -36,16 +36,18 @@ pub fn impl_create_command(input: DeriveInput, fields: Option) -> R )); } + let desc = get_description( + &attributes.desc_localizations, + &attributes.desc, + span, + &input.attrs, + )?; + let name = match &attributes.name { Some(name) => name, None => return Err(Error::new(attr_span, "missing required attribute `name`")), }; let name_localizations = localization_field(&attributes.name_localizations); - let description = match &attributes.desc { - Some(desc) => desc.clone(), - None => parse_doc(&input.attrs, span)?, - }; - let description_localizations = localization_field(&attributes.desc_localizations); let default_permissions = match &attributes.default_permissions { Some(path) => quote! { ::std::option::Option::Some(#path())}, None => quote! { ::std::option::Option::None }, @@ -73,11 +75,12 @@ pub fn impl_create_command(input: DeriveInput, fields: Option) -> R #(#field_options)* + let desc = #desc; ::twilight_interactions::command::ApplicationCommandData { name: ::std::convert::From::from(#name), name_localizations: #name_localizations, - description: ::std::convert::From::from(#description), - description_localizations: #description_localizations, + description: desc.0, + description_localizations: desc.1, options: command_options, default_member_permissions: #default_permissions, dm_permission: #dm_permission, @@ -94,13 +97,15 @@ fn field_option(field: &StructField) -> Result { let ty = &field.ty; let span = field.span; + let desc = get_description( + &field.attributes.desc_localizations, + &field.attributes.desc, + field.span, + &field.raw_attrs, + )?; + let name = field.attributes.name_default(field.ident.to_string()); let name_localizations = localization_field(&field.attributes.name_localizations); - let description = match &field.attributes.desc { - Some(desc) => desc.clone(), - None => parse_doc(&field.raw_attrs, field.span)?, - }; - let description_localizations = localization_field(&field.attributes.desc_localizations); let required = field.kind.required(); let autocomplete = field.attributes.autocomplete; let max_value = command_option_value(field.attributes.max_value); @@ -116,12 +121,13 @@ fn field_option(field: &StructField) -> Result { }; Ok(quote_spanned! {span=> + let desc = #desc; command_options.push(<#ty as ::twilight_interactions::command::CreateOption>::create_option( ::twilight_interactions::command::internal::CreateOptionData { name: ::std::convert::From::from(#name), name_localizations: #name_localizations, - description: ::std::convert::From::from(#description), - description_localizations: #description_localizations, + description: desc.0, + description_localizations: desc.1, required: ::std::option::Option::Some(#required), autocomplete: #autocomplete, data: ::twilight_interactions::command::internal::CommandOptionData { diff --git a/twilight-interactions-derive/src/command/subcommand/create_command.rs b/twilight-interactions-derive/src/command/subcommand/create_command.rs index f5f4db5..1cc6fc4 100644 --- a/twilight-interactions-derive/src/command/subcommand/create_command.rs +++ b/twilight-interactions-derive/src/command/subcommand/create_command.rs @@ -3,7 +3,7 @@ use quote::{quote, quote_spanned}; use syn::{spanned::Spanned, DeriveInput, Error, Result, Variant}; use super::parse::{ParsedVariant, TypeAttribute}; -use crate::parse::{find_attr, parse_doc}; +use crate::{command::description::get_description, parse::find_attr}; /// Implementation of `CreateCommand` derive macro pub fn impl_create_command( @@ -26,14 +26,16 @@ pub fn impl_create_command( } }; + let desc = get_description( + &attribute.desc_localizations, + &attribute.desc, + span, + &input.attrs, + )?; + let capacity = variants.len(); let name = &attribute.name; let name_localizations = localization_field(&attribute.name_localizations); - let description_localizations = localization_field(&attribute.desc_localizations); - let description = match attribute.desc { - Some(desc) => desc, - None => parse_doc(&input.attrs, span)?, - }; let default_permissions = match &attribute.default_permissions { Some(path) => quote! { ::std::option::Option::Some(#path())}, None => quote! { ::std::option::Option::None }, @@ -54,6 +56,7 @@ pub fn impl_create_command( const NAME: &'static str = #name; fn create_command() -> ::twilight_interactions::command::ApplicationCommandData { + let desc = #desc; let mut command_options = ::std::vec::Vec::with_capacity(#capacity); #(#variant_options)* @@ -61,8 +64,8 @@ pub fn impl_create_command( ::twilight_interactions::command::ApplicationCommandData { name: ::std::convert::From::from(#name), name_localizations: #name_localizations, - description: ::std::convert::From::from(#description), - description_localizations: #description_localizations, + description: desc.0, + description_localizations: desc.1, options: command_options, default_member_permissions: #default_permissions, dm_permission: #dm_permission, diff --git a/twilight-interactions/src/command/localizations.rs b/twilight-interactions/src/command/localizations.rs new file mode 100644 index 0000000..9bffb06 --- /dev/null +++ b/twilight-interactions/src/command/localizations.rs @@ -0,0 +1,41 @@ +use std::collections::HashMap; + +pub struct DescriptionLocalizations { + pub fallback: String, + pub localizations: HashMap, +} + +impl DescriptionLocalizations { + pub fn new(fallback: impl ToString, localizations: I) -> Self + where + I: IntoIterator, + K: ToString, + V: ToString, + { + Self { + fallback: fallback.to_string(), + localizations: localizations + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + } + } +} + +pub struct NameLocalizations(pub HashMap); + +impl NameLocalizations { + pub fn new(localizations: I) -> Self + where + I: IntoIterator, + K: ToString, + V: ToString, + { + Self( + localizations + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + ) + } +} diff --git a/twilight-interactions/src/command/mod.rs b/twilight-interactions/src/command/mod.rs index ca0849e..1875be0 100644 --- a/twilight-interactions/src/command/mod.rs +++ b/twilight-interactions/src/command/mod.rs @@ -39,14 +39,14 @@ //! and the second tuple element is the localized value. //! //! ``` -//! use twilight_interactions::command::{CommandModel, CreateCommand, ResolvedUser}; +//! use twilight_interactions::command::{CommandModel, CreateCommand, ResolvedUser, DescriptionLocalizations}; //! //! #[derive(CommandModel, CreateCommand)] -//! #[command(name = "hello", desc = "Say hello", desc_localizations = "hello_desc")] +//! #[command(name = "hello", desc_localizations = "hello_desc")] //! struct HelloCommand; //! -//! pub fn hello_desc() -> [(&'static str, &'static str); 2] { -//! [("fr", "Dis bonjour"), ("de", "Sag Hallo")] +//! pub fn hello_desc() -> DescriptionLocalizations { +//! DescriptionLocalizations::new("Say hello", [("fr", "Dis bonjour"), ("de", "Sag Hallo")]) //! } //! ``` //! @@ -84,6 +84,7 @@ mod command_model; mod create_command; +mod localizations; #[doc(hidden)] pub mod internal; @@ -93,6 +94,7 @@ pub use command_model::{ ResolvedUser, }; pub use create_command::{ApplicationCommandData, CreateCommand, CreateOption}; +pub use localizations::{DescriptionLocalizations, NameLocalizations}; #[cfg(feature = "derive")] #[cfg_attr(docsrs, doc(cfg(feature = "derive")))] pub use twilight_interactions_derive::{CommandModel, CommandOption, CreateCommand, CreateOption}; diff --git a/twilight-interactions/tests/localizations.rs b/twilight-interactions/tests/localizations.rs new file mode 100644 index 0000000..e7f879e --- /dev/null +++ b/twilight-interactions/tests/localizations.rs @@ -0,0 +1,169 @@ +use std::collections::HashMap; + +use twilight_interactions::command::{ + ApplicationCommandData, CommandModel, CreateCommand, DescriptionLocalizations, +}; +use twilight_model::application::command::{CommandOption, CommandOptionType}; + +fn localize() -> DescriptionLocalizations { + DescriptionLocalizations::new("fallback", [("en", "english"), ("fr", "french")]) +} + +#[derive(CommandModel, CreateCommand, Debug, PartialEq)] +#[command(name = "command-desc", desc = "desc")] +struct CommandDesc { + #[command(desc = "desc")] + option_one: i64, + #[command(desc_localizations = "localize")] + option_two: i64, +} + +#[derive(CommandModel, CreateCommand, Debug, PartialEq)] +#[command(name = "command-locale", desc_localizations = "localize")] +struct CommandLocale { + #[command(desc = "desc")] + option_one: i64, + #[command(desc_localizations = "localize")] + option_two: i64, +} + +#[derive(CommandModel, CreateCommand, Debug, PartialEq)] +#[command(name = "command-group-desc", desc = "desc")] +enum CommandGroupDesc { + #[command(name = "command-desc")] + CommandDesc(CommandDesc), + #[command(name = "command-locale")] + CommandLocale(CommandLocale), +} + +#[derive(CommandModel, CreateCommand, Debug, PartialEq)] +#[command(name = "command-group-locale", desc_localizations = "localize")] +enum CommandGroupLocale { + #[command(name = "command-desc")] + CommandDesc(CommandDesc), + #[command(name = "command-locale")] + CommandLocale(CommandLocale), +} + +fn option( + name: impl ToString, + desc: impl ToString, + locales: Option>, +) -> CommandOption { + CommandOption { + autocomplete: Some(false), + channel_types: None, + choices: None, + description: desc.to_string(), + description_localizations: locales, + kind: CommandOptionType::Integer, + min_length: None, + max_length: None, + max_value: None, + min_value: None, + options: None, + name: name.to_string(), + name_localizations: None, + required: Some(true), + } +} + +fn sub_command( + name: impl ToString, + desc: impl ToString, + locales: Option>, + options: Vec, +) -> CommandOption { + CommandOption { + autocomplete: Some(false), + channel_types: None, + choices: None, + description: desc.to_string(), + description_localizations: locales, + kind: CommandOptionType::SubCommand, + min_length: None, + max_length: None, + max_value: None, + min_value: None, + options: Some(options), + name: name.to_string(), + name_localizations: None, + required: None, + } +} + +fn command( + name: impl ToString, + desc: impl ToString, + locales: Option>, + options: Vec, + group: bool, +) -> ApplicationCommandData { + ApplicationCommandData { + name: name.to_string(), + name_localizations: None, + description: desc.to_string(), + description_localizations: locales, + options, + dm_permission: None, + default_member_permissions: None, + group, + nsfw: None, + } +} + +#[test] +fn test_top_level_commands() { + let options = vec![ + option("option_one", "desc", None), + option("option_two", "fallback", Some(localize().localizations)), + ]; + + let command_desc = command("command-desc", "desc", None, options.clone(), false); + let command_locale = command( + "command-locale", + "fallback", + Some(localize().localizations), + options, + false, + ); + + assert_eq!(CommandDesc::create_command(), command_desc); + assert_eq!(CommandLocale::create_command(), command_locale); +} + +#[test] +fn test_group_commands() { + let sub_options = vec![ + option("option_one", "desc", None), + option("option_two", "fallback", Some(localize().localizations)), + ]; + + let sub_commands = vec![ + sub_command("command-desc", "desc", None, sub_options.clone()), + sub_command( + "command-locale", + "fallback", + Some(localize().localizations), + sub_options, + ), + ]; + + let command_group_desc = command( + "command-group-desc", + "desc", + None, + sub_commands.clone(), + true, + ); + let command_group_locale = command( + "command-group-locale", + "fallback", + Some(localize().localizations), + sub_commands, + true, + ); + + assert_eq!(CommandGroupDesc::create_command(), command_group_desc); + assert_eq!(CommandGroupLocale::create_command(), command_group_locale); +} diff --git a/twilight-interactions/tests/subcommand.rs b/twilight-interactions/tests/subcommand.rs index d72bdaf..809240a 100644 --- a/twilight-interactions/tests/subcommand.rs +++ b/twilight-interactions/tests/subcommand.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use twilight_interactions::command::{ - ApplicationCommandData, CommandInputData, CommandModel, CreateCommand, + ApplicationCommandData, CommandInputData, CommandModel, CreateCommand, DescriptionLocalizations, }; use twilight_model::{ application::{ @@ -44,7 +44,6 @@ enum SubCommandGroup { #[derive(CommandModel, CreateCommand, Debug, PartialEq, Eq)] #[command( name = "command", - desc = "Command", desc_localizations = "subcommand_desc", default_permissions = "subcommand_permissions" )] @@ -55,8 +54,8 @@ enum SubCommand { Group(Box), } -fn subcommand_desc() -> [(&'static str, &'static str); 1] { - [("en", "Command")] +fn subcommand_desc() -> DescriptionLocalizations { + DescriptionLocalizations::new("fallback", [("en", "en description")]) } fn subcommand_permissions() -> Permissions { @@ -214,8 +213,8 @@ fn test_create_subcommand() { let expected = ApplicationCommandData { name: "command".into(), name_localizations: None, - description: "Command".into(), - description_localizations: Some(HashMap::from([("en".into(), "Command".into())])), + description: "fallback".into(), + description_localizations: Some(HashMap::from([("en".into(), "en description".into())])), options: subcommand, default_member_permissions: Some(Permissions::empty()), dm_permission: None,