diff --git a/CHANGELOG.md b/CHANGELOG.md index a0916ad5..9c997856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- `#[command(rename = "a_name_that_is_not_a_case_name")]` doesn't work anymore +- `#[command(rename = "...")]` now always renames to `"..."`, to rename multiple commands using the same pattern, use `#[command(rename_rule = "snake_case")]` and the like. ## 0.6.3 - 2022-07-19 diff --git a/src/bot_commands.rs b/src/bot_commands.rs index 46d77672..89ee82ce 100644 --- a/src/bot_commands.rs +++ b/src/bot_commands.rs @@ -1,6 +1,6 @@ use crate::{ - command::Command, command_attr::CommandAttrs, command_enum::CommandEnum, - compile_error, fields_parse::impl_parse_args, unzip::Unzip, Result, + command::Command, command_enum::CommandEnum, compile_error, + fields_parse::impl_parse_args, unzip::Unzip, Result, }; use proc_macro2::TokenStream; @@ -9,22 +9,23 @@ use syn::DeriveInput; pub(crate) fn bot_commands_impl(input: DeriveInput) -> Result { let data_enum = get_enum_data(&input)?; - let enum_attrs = CommandAttrs::from_attributes(&input.attrs)?; - let command_enum = CommandEnum::try_from(enum_attrs)?; + let command_enum = CommandEnum::from_attributes(&input.attrs)?; let Unzip(var_init, var_info) = data_enum .variants .iter() .map(|variant| { - let attrs = CommandAttrs::from_attributes(&variant.attrs)?; - let command = Command::try_from(attrs, &variant.ident.to_string())?; + let command = Command::new( + &variant.ident.to_string(), + &variant.attrs, + &command_enum, + )?; let variant_name = &variant.ident; let self_variant = quote! { Self::#variant_name }; - let parser = - command.parser.as_ref().unwrap_or(&command_enum.parser_type); - let parse = impl_parse_args(&variant.fields, self_variant, parser); + let parse = + impl_parse_args(&variant.fields, self_variant, &command.parser); Ok((parse, command)) }) @@ -32,8 +33,8 @@ pub(crate) fn bot_commands_impl(input: DeriveInput) -> Result { let type_name = &input.ident; let fn_descriptions = impl_descriptions(&var_info, &command_enum); - let fn_parse = impl_parse(&var_info, &command_enum, &var_init); - let fn_commands = impl_commands(&var_info, &command_enum); + let fn_parse = impl_parse(&var_info, &var_init); + let fn_commands = impl_commands(&var_info); let trait_impl = quote! { impl teloxide::utils::command::BotCommands for #type_name { @@ -46,15 +47,12 @@ pub(crate) fn bot_commands_impl(input: DeriveInput) -> Result { Ok(trait_impl) } -fn impl_commands( - infos: &[Command], - global: &CommandEnum, -) -> proc_macro2::TokenStream { +fn impl_commands(infos: &[Command]) -> proc_macro2::TokenStream { let commands = infos .iter() .filter(|command| command.description_is_enabled()) .map(|command| { - let c = command.get_matched_value(global); + let c = command.get_prefixed_command(); let d = command.description.as_deref().unwrap_or_default(); quote! { BotCommand::new(#c,#d) } }); @@ -74,10 +72,9 @@ fn impl_descriptions( let command_descriptions = infos .iter() .filter(|command| command.description_is_enabled()) - .map(|c| { - let (prefix, command) = c.get_matched_value2(global); - let description = c.description.clone().unwrap_or_default(); - quote! { CommandDescription { prefix: #prefix, command: #command, description: #description } } + .map(|Command { prefix, name, description, ..}| { + let description = description.clone().unwrap_or_default(); + quote! { CommandDescription { prefix: #prefix, command: #name, description: #description } } }); let global_description = match global.description.as_deref() { @@ -100,10 +97,9 @@ fn impl_descriptions( fn impl_parse( infos: &[Command], - global: &CommandEnum, variants_initialization: &[proc_macro2::TokenStream], ) -> proc_macro2::TokenStream { - let matching_values = infos.iter().map(|c| c.get_matched_value(global)); + let matching_values = infos.iter().map(|c| c.get_prefixed_command()); quote! { fn parse(s: &str, bot_name: N) -> Result diff --git a/src/command.rs b/src/command.rs index 769c83cc..72b05f3e 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,55 +1,62 @@ use crate::{ command_attr::CommandAttrs, command_enum::CommandEnum, - fields_parse::ParserType, rename_rules::RenameRule, Result, + error::compile_error_at, fields_parse::ParserType, Result, }; pub(crate) struct Command { - pub prefix: Option, + /// Prefix of this command, for example "/". + pub prefix: String, + /// Description for the command. pub description: Option, - pub parser: Option, + /// Name of the command, with all renames already applied. pub name: String, + /// Parser for arguments of this command. + pub parser: ParserType, } impl Command { - pub fn try_from(attrs: CommandAttrs, name: &str) -> Result { + pub fn new( + name: &str, + attributes: &[syn::Attribute], + global_options: &CommandEnum, + ) -> Result { + let attrs = CommandAttrs::from_attributes(attributes)?; let CommandAttrs { prefix, description, rename_rule, + rename, parser, + // FIXME: error on/do not ignore separator separator: _, } = attrs; - let name = rename_rule.unwrap_or(RenameRule::Identity).apply(name); + let name = match (rename, rename_rule) { + (Some((rename, _)), None) => rename, + (Some(_), Some((_, sp))) => { + return Err(compile_error_at( + "`rename_rule` can't be applied to `rename`-d variant", + sp, + )) + } + (None, Some((rule, _))) => rule.apply(name), + (None, None) => global_options.rename_rule.apply(name), + }; + + let prefix = prefix + .map(|(p, _)| p) + .unwrap_or_else(|| global_options.prefix.clone()); + let description = description.map(|(d, _)| d); + let parser = parser + .map(|(p, _)| p) + .unwrap_or_else(|| global_options.parser_type.clone()); Ok(Self { prefix, description, parser, name }) } - pub fn get_matched_value(&self, global_parameters: &CommandEnum) -> String { - let prefix = if let Some(prefix) = &self.prefix { - prefix - } else if let Some(prefix) = &global_parameters.prefix { - prefix - } else { - "/" - }; - - String::from(prefix) + &global_parameters.rename_rule.apply(&self.name) - } - - pub fn get_matched_value2( - &self, - global_parameters: &CommandEnum, - ) -> (String, String) { - let prefix = if let Some(prefix) = &self.prefix { - prefix - } else if let Some(prefix) = &global_parameters.prefix { - prefix - } else { - "/" - }; - - (String::from(prefix), global_parameters.rename_rule.apply(&self.name)) + pub fn get_prefixed_command(&self) -> String { + let Self { prefix, name, .. } = self; + format!("{prefix}{name}") } pub(crate) fn description_is_enabled(&self) -> bool { diff --git a/src/command_attr.rs b/src/command_attr.rs index 4550f679..3aeb8e63 100644 --- a/src/command_attr.rs +++ b/src/command_attr.rs @@ -9,25 +9,36 @@ use crate::{ use proc_macro2::Span; use syn::Attribute; -/// Attributes for `BotCommands` derive macro. +/// All attributes that can be used for `derive(BotCommands)` pub(crate) struct CommandAttrs { - pub prefix: Option, - pub description: Option, - pub rename_rule: Option, - pub parser: Option, - pub separator: Option, + pub prefix: Option<(String, Span)>, + pub description: Option<(String, Span)>, + pub rename_rule: Option<(RenameRule, Span)>, + pub rename: Option<(String, Span)>, + pub parser: Option<(ParserType, Span)>, + pub separator: Option<(String, Span)>, } -/// An attribute for `BotCommands` derive macro. -pub(crate) struct CommandAttr { +/// A single k/v attribute for `BotCommands` derive macro. +/// +/// For example: +/// ```text +/// #[command(prefix = "!", rename_rule = "snake_case")] +/// /^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^---- CommandAttr { kind: RenameRule(SnakeCase) } +/// | +/// CommandAttr { kind: Prefix("!") } +/// ``` +struct CommandAttr { kind: CommandAttrKind, sp: Span, } -pub(crate) enum CommandAttrKind { +/// Kind of [`CommandAttr`]. +enum CommandAttrKind { Prefix(String), Description(String), - Rename(RenameRule), + RenameRule(RenameRule), + Rename(String), ParseWith(ParserType), Separator(String), } @@ -44,18 +55,19 @@ impl CommandAttrs { prefix: None, description: None, rename_rule: None, + rename: None, parser: None, separator: None, }, |mut this, attr| { fn insert( - opt: &mut Option, + opt: &mut Option<(T, Span)>, x: T, sp: Span, ) -> Result<()> { match opt { slot @ None => { - *slot = Some(x); + *slot = Some((x, sp)); Ok(()) } Some(_) => { @@ -67,7 +79,8 @@ impl CommandAttrs { match attr.kind { Prefix(p) => insert(&mut this.prefix, p, attr.sp), Description(d) => insert(&mut this.description, d, attr.sp), - Rename(r) => insert(&mut this.rename_rule, r, attr.sp), + RenameRule(r) => insert(&mut this.rename_rule, r, attr.sp), + Rename(r) => insert(&mut this.rename, r, attr.sp), ParseWith(p) => insert(&mut this.parser, p, attr.sp), Separator(s) => insert(&mut this.separator, s, attr.sp), }?; @@ -87,9 +100,12 @@ impl CommandAttr { let kind = match &*key.to_string() { "prefix" => Prefix(value.expect_string()?), "description" => Description(value.expect_string()?), - "rename" => Rename( - value.expect_string().and_then(|r| RenameRule::parse(&r))?, + "rename_rule" => RenameRule( + value + .expect_string() + .and_then(|r| self::RenameRule::parse(&r))?, ), + "rename" => Rename(value.expect_string()?), "parse_with" => { ParseWith(value.expect_string().map(|p| ParserType::parse(&p))?) } diff --git a/src/command_enum.rs b/src/command_enum.rs index ad3959a0..2a882e17 100644 --- a/src/command_enum.rs +++ b/src/command_enum.rs @@ -1,37 +1,50 @@ use crate::{ - command_attr::CommandAttrs, fields_parse::ParserType, - rename_rules::RenameRule, Result, + command_attr::CommandAttrs, error::compile_error_at, + fields_parse::ParserType, rename_rules::RenameRule, Result, }; #[derive(Debug)] pub(crate) struct CommandEnum { - pub prefix: Option, + pub prefix: String, pub description: Option, pub rename_rule: RenameRule, pub parser_type: ParserType, } impl CommandEnum { - pub fn try_from(attrs: CommandAttrs) -> Result { + pub fn from_attributes(attributes: &[syn::Attribute]) -> Result { + let attrs = CommandAttrs::from_attributes(attributes)?; let CommandAttrs { prefix, description, rename_rule, + rename, parser, separator, } = attrs; - let mut parser = parser.unwrap_or(ParserType::Default); + + if let Some((_rename, sp)) = rename { + return Err(compile_error_at( + "`rename` attribute can only be applied to enums *variants*", + sp, + )); + } + + let mut parser = parser.map(|(p, _)| p).unwrap_or(ParserType::Default); // FIXME: Error on unused separator - if let (ParserType::Split { separator }, Some(s)) = + if let (ParserType::Split { separator }, Some((s, _))) = (&mut parser, &separator) { *separator = Some(s.clone()) } + Ok(Self { - prefix, - description, - rename_rule: rename_rule.unwrap_or(RenameRule::Identity), + prefix: prefix.map(|(p, _)| p).unwrap_or_else(|| "/".to_owned()), + description: description.map(|(d, _)| d), + rename_rule: rename_rule + .map(|(rr, _)| rr) + .unwrap_or(RenameRule::Identity), parser_type: parser, }) } diff --git a/src/fields_parse.rs b/src/fields_parse.rs index a9a2d0b2..6dc2baeb 100644 --- a/src/fields_parse.rs +++ b/src/fields_parse.rs @@ -1,7 +1,7 @@ use quote::quote; use syn::{Fields, FieldsNamed, FieldsUnnamed, Type}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) enum ParserType { Default, Split { separator: Option }, diff --git a/tests/command.rs b/tests/command.rs index 135610cf..f7297c8d 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -9,7 +9,7 @@ use teloxide::utils::command::BotCommands as _; #[test] fn parse_command_with_args() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { Start(String), Help, @@ -24,7 +24,7 @@ fn parse_command_with_args() { #[test] fn parse_command_with_non_string_arg() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { Start(i32), Help, @@ -39,7 +39,7 @@ fn parse_command_with_non_string_arg() { #[test] fn attribute_prefix() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { #[command(prefix = "!")] Start(String), @@ -55,7 +55,7 @@ fn attribute_prefix() { #[test] fn many_attributes() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { #[command(prefix = "!", description = "desc")] Start, @@ -75,7 +75,11 @@ fn many_attributes() { #[test] fn global_attributes() { #[derive(BotCommands, Debug, PartialEq)] - #[command(prefix = "!", rename = "lowercase", description = "Bot commands")] + #[command( + prefix = "!", + rename_rule = "lowercase", + description = "Bot commands" + )] enum DefaultCommands { #[command(prefix = "/")] Start, @@ -99,7 +103,7 @@ fn global_attributes() { #[test] fn parse_command_with_bot_name() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { #[command(prefix = "/")] Start, @@ -115,7 +119,7 @@ fn parse_command_with_bot_name() { #[test] fn parse_with_split() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] #[command(parse_with = "split")] enum DefaultCommands { Start(u8, String), @@ -131,7 +135,7 @@ fn parse_with_split() { #[test] fn parse_with_split2() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] #[command(parse_with = "split", separator = "|")] enum DefaultCommands { Start(u8, String), @@ -174,7 +178,7 @@ fn parse_custom_parser() { use parser::custom_parse_function; #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { #[command(parse_with = "custom_parse_function")] Start(u8, String), @@ -199,7 +203,7 @@ fn parse_custom_parser() { #[test] fn parse_named_fields() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] #[command(parse_with = "split")] enum DefaultCommands { Start { num: u8, data: String }, @@ -215,7 +219,7 @@ fn parse_named_fields() { #[test] fn descriptions_off() { #[derive(BotCommands, Debug, PartialEq)] - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] enum DefaultCommands { #[command(description = "off")] Start, @@ -229,24 +233,24 @@ fn descriptions_off() { fn rename_rules() { #[derive(BotCommands, Debug, PartialEq)] enum DefaultCommands { - #[command(rename = "lowercase")] + #[command(rename_rule = "lowercase")] AaaAaa, - #[command(rename = "UPPERCASE")] + #[command(rename_rule = "UPPERCASE")] BbbBbb, - #[command(rename = "PascalCase")] + #[command(rename_rule = "PascalCase")] CccCcc, - #[command(rename = "camelCase")] + #[command(rename_rule = "camelCase")] DddDdd, - #[command(rename = "snake_case")] + #[command(rename_rule = "snake_case")] EeeEee, - #[command(rename = "SCREAMING_SNAKE_CASE")] + #[command(rename_rule = "SCREAMING_SNAKE_CASE")] FffFff, - #[command(rename = "kebab-case")] + #[command(rename_rule = "kebab-case")] GggGgg, - #[command(rename = "SCREAMING-KEBAB-CASE")] + #[command(rename_rule = "SCREAMING-KEBAB-CASE")] HhhHhh, - //#[command(rename = "Bar")] - //Foo, + #[command(rename = "Bar")] + Foo, } assert_eq!( @@ -281,16 +285,14 @@ fn rename_rules() { DefaultCommands::HhhHhh, DefaultCommands::parse("/HHH-HHH", "").unwrap() ); - //assert_eq!(DefaultCommands::Foo, DefaultCommands::parse("/Bar", - // "").unwrap()); + assert_eq!( + DefaultCommands::Foo, + DefaultCommands::parse("/Bar", "").unwrap() + ); - // assert_eq!( - // "/aaaaaa\n/BBBBBB\n/CccCcc\n/dddDdd\n/eee_eee\n/FFF_FFF\n/ggg-ggg\n/ - // HHH-HHH\n/Bar", DefaultCommands::descriptions().to_string() - // ); assert_eq!( "/aaaaaa\n/BBBBBB\n/CccCcc\n/dddDdd\n/eee_eee\n/FFF_FFF\n/ggg-ggg\n/\ - HHH-HHH", + HHH-HHH\n/Bar", DefaultCommands::descriptions().to_string() ); }