diff --git a/crates/teloxide-macros/CHANGELOG.md b/crates/teloxide-macros/CHANGELOG.md index d9a191ef..57b65163 100644 --- a/crates/teloxide-macros/CHANGELOG.md +++ b/crates/teloxide-macros/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +### Added + +- Now you can use `#[command(command_separator="sep")]` (default is a whitespace character) to set the separator between command and its arguments ([issue #897](https://github.com/teloxide/teloxide/issues/897)) + ### Fixed - Fix `split` parser for tuple variants with len < 2 ([issue #834](https://github.com/teloxide/teloxide/issues/834)) diff --git a/crates/teloxide-macros/src/bot_commands.rs b/crates/teloxide-macros/src/bot_commands.rs index e081d680..6cebfdeb 100644 --- a/crates/teloxide-macros/src/bot_commands.rs +++ b/crates/teloxide-macros/src/bot_commands.rs @@ -28,7 +28,7 @@ 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, &var_init); + let fn_parse = impl_parse(&var_info, &var_init, &command_enum.command_separator); let fn_commands = impl_commands(&var_info); let trait_impl = quote! { @@ -99,6 +99,7 @@ fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> proc_macro2::To fn impl_parse( infos: &[Command], variants_initialization: &[proc_macro2::TokenStream], + command_separator: &str, ) -> proc_macro2::TokenStream { let matching_values = infos.iter().map(|c| c.get_prefixed_command()); @@ -110,7 +111,7 @@ fn impl_parse( // 2 is used to only split once (=> in two parts), // we only need to split the command and the rest of arguments. - let mut words = s.splitn(2, ' '); + let mut words = s.splitn(2, #command_separator); // Unwrap: split iterators always have at least one item let mut full_command = words.next().unwrap().split('@'); diff --git a/crates/teloxide-macros/src/command.rs b/crates/teloxide-macros/src/command.rs index a36f91eb..794d42d8 100644 --- a/crates/teloxide-macros/src/command.rs +++ b/crates/teloxide-macros/src/command.rs @@ -34,6 +34,8 @@ impl Command { parser, // FIXME: error on/do not ignore separator separator: _, + // FIXME: error on/do not ignore command separator + command_separator: _, hide, } = attrs; diff --git a/crates/teloxide-macros/src/command_attr.rs b/crates/teloxide-macros/src/command_attr.rs index b9ce1727..9d1e9550 100644 --- a/crates/teloxide-macros/src/command_attr.rs +++ b/crates/teloxide-macros/src/command_attr.rs @@ -22,6 +22,7 @@ pub(crate) struct CommandAttrs { pub rename: Option<(String, Span)>, pub parser: Option<(ParserType, Span)>, pub separator: Option<(String, Span)>, + pub command_separator: Option<(String, Span)>, pub hide: Option<((), Span)>, } @@ -48,6 +49,7 @@ enum CommandAttrKind { Rename(String), ParseWith(ParserType), Separator(String), + CommandSeparator(String), Hide, } @@ -66,6 +68,7 @@ impl CommandAttrs { rename: None, parser: None, separator: None, + command_separator: None, hide: None, }, |mut this, attr| { @@ -110,6 +113,7 @@ impl CommandAttrs { 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), + CommandSeparator(s) => insert(&mut this.command_separator, s, attr.sp), Hide => insert(&mut this.hide, (), attr.sp), }?; @@ -165,6 +169,7 @@ impl CommandAttr { "rename" => Rename(value.expect_string()?), "parse_with" => ParseWith(ParserType::parse(value)?), "separator" => Separator(value.expect_string()?), + "command_separator" => CommandSeparator(value.expect_string()?), "hide" => value.expect_none("hide").map(|_| Hide)?, _ => { return Err(compile_error_at( diff --git a/crates/teloxide-macros/src/command_enum.rs b/crates/teloxide-macros/src/command_enum.rs index d17247cc..ff2a4960 100644 --- a/crates/teloxide-macros/src/command_enum.rs +++ b/crates/teloxide-macros/src/command_enum.rs @@ -7,6 +7,7 @@ pub(crate) struct CommandEnum { pub prefix: String, /// The bool is true if the description contains a doc comment pub description: Option<(String, bool)>, + pub command_separator: String, pub rename_rule: RenameRule, pub parser_type: ParserType, } @@ -14,8 +15,16 @@ pub(crate) struct CommandEnum { impl CommandEnum { pub fn from_attributes(attributes: &[syn::Attribute]) -> Result { let attrs = CommandAttrs::from_attributes(attributes)?; - let CommandAttrs { prefix, description, rename_rule, rename, parser, separator, hide } = - attrs; + let CommandAttrs { + prefix, + description, + rename_rule, + rename, + parser, + separator, + command_separator, + hide, + } = attrs; if let Some((_rename, sp)) = rename { return Err(compile_error_at( @@ -39,6 +48,9 @@ impl CommandEnum { Ok(Self { prefix: prefix.map(|(p, _)| p).unwrap_or_else(|| "/".to_owned()), description: description.map(|(d, is_doc, _)| (d, is_doc)), + command_separator: command_separator + .map(|(s, _)| s) + .unwrap_or_else(|| String::from(" ")), rename_rule: rename_rule.map(|(rr, _)| rr).unwrap_or(RenameRule::Identity), parser_type: parser, }) diff --git a/crates/teloxide/src/utils/command.rs b/crates/teloxide/src/utils/command.rs index d3413e9d..79c9dc7a 100644 --- a/crates/teloxide/src/utils/command.rs +++ b/crates/teloxide/src/utils/command.rs @@ -155,6 +155,30 @@ pub use teloxide_macros::BotCommands; /// # } /// ``` /// +/// 6. `#[command(command_separator = "sep")]` +/// Specify separator between command and args. Default is a space character. +/// +/// ## Example +/// ``` +/// # #[cfg(feature = "macros")] { +/// use teloxide::utils::command::BotCommands; +/// +/// #[derive(BotCommands, PartialEq, Debug)] +/// #[command( +/// rename_rule = "lowercase", +/// parse_with = "split", +/// separator = "_", +/// command_separator = "_" +/// )] +/// enum Command { +/// Nums(u8, u16, i32), +/// } +/// +/// let command = Command::parse("/nums_1_32_5", "").unwrap(); +/// assert_eq!(command, Command::Nums(1, 32, 5)); +/// # } +/// ``` +/// /// # Variant attributes /// All variant attributes override the corresponding `enum` attributes. /// diff --git a/crates/teloxide/tests/command.rs b/crates/teloxide/tests/command.rs index 765cd969..ce6ccf75 100644 --- a/crates/teloxide/tests/command.rs +++ b/crates/teloxide/tests/command.rs @@ -166,6 +166,67 @@ fn parse_with_split4() { assert_eq!(DefaultCommands::Start(), DefaultCommands::parse("/start", "").unwrap(),); } +#[test] +#[cfg(feature = "macros")] +fn parse_with_command_separator1() { + #[derive(BotCommands, Debug, PartialEq)] + #[command(rename_rule = "lowercase")] + #[command(parse_with = "split", separator = "|", command_separator = "_")] + enum DefaultCommands { + Start(u8, String), + Help, + } + + assert_eq!( + DefaultCommands::Start(10, "hello".to_string()), + DefaultCommands::parse("/start_10|hello", "").unwrap() + ); +} + +#[test] +#[cfg(feature = "macros")] +fn parse_with_command_separator2() { + #[derive(BotCommands, Debug, PartialEq)] + #[command(rename_rule = "lowercase")] + #[command(parse_with = "split", separator = "_", command_separator = "_")] + enum DefaultCommands { + Start(u8, String), + Help, + } + + assert_eq!( + DefaultCommands::Start(10, "hello".to_string()), + DefaultCommands::parse("/start_10_hello", "").unwrap() + ); +} + +#[test] +#[cfg(feature = "macros")] +fn parse_with_command_separator3() { + #[derive(BotCommands, Debug, PartialEq)] + #[command(rename_rule = "lowercase")] + #[command(parse_with = "split", command_separator = ":")] + enum DefaultCommands { + Help, + } + + assert_eq!(DefaultCommands::Help, DefaultCommands::parse("/help", "").unwrap()); +} + +#[test] +#[cfg(feature = "macros")] +fn parse_with_command_separator4() { + #[derive(BotCommands, Debug, PartialEq)] + #[command(rename_rule = "lowercase")] + #[command(parse_with = "split", command_separator = ":")] + enum DefaultCommands { + Start(u8), + Help, + } + + assert_eq!(DefaultCommands::Start(10), DefaultCommands::parse("/start:10", "").unwrap()); +} + #[test] #[cfg(feature = "macros")] fn parse_custom_parser() {