Merge pull request #932 from kpp/command_separator

Implemend command_separator attr to split command and args
This commit is contained in:
Waffle Maybe 2023-09-26 18:16:18 +00:00 committed by GitHub
commit d591d24507
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 4 deletions

View file

@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## unreleased ## 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 ### Fixed
- Fix `split` parser for tuple variants with len < 2 ([issue #834](https://github.com/teloxide/teloxide/issues/834)) - Fix `split` parser for tuple variants with len < 2 ([issue #834](https://github.com/teloxide/teloxide/issues/834))

View file

@ -28,7 +28,7 @@ pub(crate) fn bot_commands_impl(input: DeriveInput) -> Result<TokenStream> {
let type_name = &input.ident; let type_name = &input.ident;
let fn_descriptions = impl_descriptions(&var_info, &command_enum); 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 fn_commands = impl_commands(&var_info);
let trait_impl = quote! { let trait_impl = quote! {
@ -99,6 +99,7 @@ fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> proc_macro2::To
fn impl_parse( fn impl_parse(
infos: &[Command], infos: &[Command],
variants_initialization: &[proc_macro2::TokenStream], variants_initialization: &[proc_macro2::TokenStream],
command_separator: &str,
) -> proc_macro2::TokenStream { ) -> proc_macro2::TokenStream {
let matching_values = infos.iter().map(|c| c.get_prefixed_command()); 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), // 2 is used to only split once (=> in two parts),
// we only need to split the command and the rest of arguments. // 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 // Unwrap: split iterators always have at least one item
let mut full_command = words.next().unwrap().split('@'); let mut full_command = words.next().unwrap().split('@');

View file

@ -34,6 +34,8 @@ impl Command {
parser, parser,
// FIXME: error on/do not ignore separator // FIXME: error on/do not ignore separator
separator: _, separator: _,
// FIXME: error on/do not ignore command separator
command_separator: _,
hide, hide,
} = attrs; } = attrs;

View file

@ -22,6 +22,7 @@ pub(crate) struct CommandAttrs {
pub rename: Option<(String, Span)>, pub rename: Option<(String, Span)>,
pub parser: Option<(ParserType, Span)>, pub parser: Option<(ParserType, Span)>,
pub separator: Option<(String, Span)>, pub separator: Option<(String, Span)>,
pub command_separator: Option<(String, Span)>,
pub hide: Option<((), Span)>, pub hide: Option<((), Span)>,
} }
@ -48,6 +49,7 @@ enum CommandAttrKind {
Rename(String), Rename(String),
ParseWith(ParserType), ParseWith(ParserType),
Separator(String), Separator(String),
CommandSeparator(String),
Hide, Hide,
} }
@ -66,6 +68,7 @@ impl CommandAttrs {
rename: None, rename: None,
parser: None, parser: None,
separator: None, separator: None,
command_separator: None,
hide: None, hide: None,
}, },
|mut this, attr| { |mut this, attr| {
@ -110,6 +113,7 @@ impl CommandAttrs {
Rename(r) => insert(&mut this.rename, r, attr.sp), Rename(r) => insert(&mut this.rename, r, attr.sp),
ParseWith(p) => insert(&mut this.parser, p, attr.sp), ParseWith(p) => insert(&mut this.parser, p, attr.sp),
Separator(s) => insert(&mut this.separator, s, 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), Hide => insert(&mut this.hide, (), attr.sp),
}?; }?;
@ -165,6 +169,7 @@ impl CommandAttr {
"rename" => Rename(value.expect_string()?), "rename" => Rename(value.expect_string()?),
"parse_with" => ParseWith(ParserType::parse(value)?), "parse_with" => ParseWith(ParserType::parse(value)?),
"separator" => Separator(value.expect_string()?), "separator" => Separator(value.expect_string()?),
"command_separator" => CommandSeparator(value.expect_string()?),
"hide" => value.expect_none("hide").map(|_| Hide)?, "hide" => value.expect_none("hide").map(|_| Hide)?,
_ => { _ => {
return Err(compile_error_at( return Err(compile_error_at(

View file

@ -7,6 +7,7 @@ pub(crate) struct CommandEnum {
pub prefix: String, pub prefix: String,
/// The bool is true if the description contains a doc comment /// The bool is true if the description contains a doc comment
pub description: Option<(String, bool)>, pub description: Option<(String, bool)>,
pub command_separator: String,
pub rename_rule: RenameRule, pub rename_rule: RenameRule,
pub parser_type: ParserType, pub parser_type: ParserType,
} }
@ -14,8 +15,16 @@ pub(crate) struct CommandEnum {
impl CommandEnum { impl CommandEnum {
pub fn from_attributes(attributes: &[syn::Attribute]) -> Result<Self> { pub fn from_attributes(attributes: &[syn::Attribute]) -> Result<Self> {
let attrs = CommandAttrs::from_attributes(attributes)?; let attrs = CommandAttrs::from_attributes(attributes)?;
let CommandAttrs { prefix, description, rename_rule, rename, parser, separator, hide } = let CommandAttrs {
attrs; prefix,
description,
rename_rule,
rename,
parser,
separator,
command_separator,
hide,
} = attrs;
if let Some((_rename, sp)) = rename { if let Some((_rename, sp)) = rename {
return Err(compile_error_at( return Err(compile_error_at(
@ -39,6 +48,9 @@ impl CommandEnum {
Ok(Self { Ok(Self {
prefix: prefix.map(|(p, _)| p).unwrap_or_else(|| "/".to_owned()), prefix: prefix.map(|(p, _)| p).unwrap_or_else(|| "/".to_owned()),
description: description.map(|(d, is_doc, _)| (d, is_doc)), 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), rename_rule: rename_rule.map(|(rr, _)| rr).unwrap_or(RenameRule::Identity),
parser_type: parser, parser_type: parser,
}) })

View file

@ -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 /// # Variant attributes
/// All variant attributes override the corresponding `enum` attributes. /// All variant attributes override the corresponding `enum` attributes.
/// ///

View file

@ -166,6 +166,67 @@ fn parse_with_split4() {
assert_eq!(DefaultCommands::Start(), DefaultCommands::parse("/start", "").unwrap(),); 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] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn parse_custom_parser() { fn parse_custom_parser() {