mirror of
https://github.com/teloxide/teloxide.git
synced 2025-01-03 09:49:07 +01:00
Actually tests that macros work
This commit is contained in:
parent
822300eb89
commit
66e7ebd7f1
4 changed files with 523 additions and 2 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -7,11 +7,17 @@ edition = "2018"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
quote = "1.0.7"
|
quote = "1.0.7"
|
||||||
proc-macro2 = "1.0.19"
|
proc-macro2 = "1.0.19"
|
||||||
syn = { version = "1.0.13", features = ["full"] }
|
syn = { version = "1.0.13", features = ["full"] }
|
||||||
heck = "0.4.0"
|
heck = "0.4.0"
|
||||||
|
|
||||||
[lib]
|
[dev-dependencies]
|
||||||
proc-macro = true
|
teloxide = { path = "micro-teloxide", package = "micro-teloxide" }
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = [".", "micro-teloxide"]
|
||||||
|
|
11
micro-teloxide/Cargo.toml
Normal file
11
micro-teloxide/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "micro-teloxide"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
teloxide-macros = { path = ".." }
|
||||||
|
serde = { version = "1.0.145", features = ["derive"] }
|
||||||
|
serde_with_macros = "1.5.2"
|
212
micro-teloxide/src/lib.rs
Normal file
212
micro-teloxide/src/lib.rs
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
//! A minimal "mock" of teloxide crate that is needed to test macros. Changes to
|
||||||
|
//! `teloxide`[`-core`] should be kept in sync with this... somehow
|
||||||
|
//!
|
||||||
|
//! This is a price for placing all crates in separate repositories.
|
||||||
|
|
||||||
|
pub use teloxide_macros as macros;
|
||||||
|
|
||||||
|
pub mod utils {
|
||||||
|
pub mod command {
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fmt::{self, Display, Write},
|
||||||
|
marker::PhantomData,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use teloxide_macros::BotCommands;
|
||||||
|
|
||||||
|
use crate::types::{BotCommand, Me};
|
||||||
|
pub trait BotCommands: Sized {
|
||||||
|
fn parse<N>(s: &str, bot_username: N) -> Result<Self, ParseError>
|
||||||
|
where
|
||||||
|
N: Into<String>;
|
||||||
|
|
||||||
|
fn descriptions() -> CommandDescriptions<'static>;
|
||||||
|
|
||||||
|
fn bot_commands() -> Vec<BotCommand>;
|
||||||
|
|
||||||
|
fn ty() -> PhantomData<Self> {
|
||||||
|
PhantomData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PrefixedBotCommand = String;
|
||||||
|
pub type BotName = String;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ParseError {
|
||||||
|
TooFewArguments { expected: usize, found: usize, message: String },
|
||||||
|
TooManyArguments { expected: usize, found: usize, message: String },
|
||||||
|
|
||||||
|
IncorrectFormat(Box<dyn Error + Send + Sync + 'static>),
|
||||||
|
|
||||||
|
UnknownCommand(PrefixedBotCommand),
|
||||||
|
WrongBotName(BotName),
|
||||||
|
|
||||||
|
Custom(Box<dyn Error + Send + Sync + 'static>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct CommandDescriptions<'a> {
|
||||||
|
global_description: Option<&'a str>,
|
||||||
|
descriptions: &'a [CommandDescription<'a>],
|
||||||
|
bot_username: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CommandDescription<'a> {
|
||||||
|
pub prefix: &'a str,
|
||||||
|
pub command: &'a str,
|
||||||
|
pub description: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CommandDescriptions<'a> {
|
||||||
|
pub fn new(descriptions: &'a [CommandDescription<'a>]) -> Self {
|
||||||
|
Self {
|
||||||
|
global_description: None,
|
||||||
|
descriptions,
|
||||||
|
bot_username: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn global_description(
|
||||||
|
self,
|
||||||
|
global_description: &'a str,
|
||||||
|
) -> Self {
|
||||||
|
Self { global_description: Some(global_description), ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn username(self, bot_username: &'a str) -> Self {
|
||||||
|
Self { bot_username: Some(bot_username), ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn username_from_me(
|
||||||
|
self,
|
||||||
|
me: &'a Me,
|
||||||
|
) -> CommandDescriptions<'a> {
|
||||||
|
self.username(
|
||||||
|
me.user
|
||||||
|
.username
|
||||||
|
.as_deref()
|
||||||
|
.expect("Bots must have usernames"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CommandDescriptions<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
if let Some(global_description) = self.global_description {
|
||||||
|
f.write_str(global_description)?;
|
||||||
|
f.write_str("\n\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut write =
|
||||||
|
|&CommandDescription { prefix, command, description },
|
||||||
|
nls| {
|
||||||
|
if nls {
|
||||||
|
f.write_char('\n')?;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.write_str(prefix)?;
|
||||||
|
f.write_str(command)?;
|
||||||
|
|
||||||
|
if let Some(username) = self.bot_username {
|
||||||
|
f.write_char('@')?;
|
||||||
|
f.write_str(username)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !description.is_empty() {
|
||||||
|
f.write_str(" — ")?;
|
||||||
|
f.write_str(description)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt::Result::Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(descr) = self.descriptions.first() {
|
||||||
|
write(descr, false)?;
|
||||||
|
for descr in &self.descriptions[1..] {
|
||||||
|
write(descr, true)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod types {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Me {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub user: User,
|
||||||
|
pub can_join_groups: bool,
|
||||||
|
pub can_read_all_group_messages: bool,
|
||||||
|
pub supports_inline_queries: bool,
|
||||||
|
}
|
||||||
|
#[serde_with_macros::skip_serializing_none]
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: UserId,
|
||||||
|
pub is_bot: bool,
|
||||||
|
pub first_name: String,
|
||||||
|
pub last_name: Option<String>,
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub language_code: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
pub is_premium: bool,
|
||||||
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
pub added_to_attachment_menu: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
Hash,
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct UserId(pub u64);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct BotCommand {
|
||||||
|
pub command: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BotCommand {
|
||||||
|
pub fn new<S1, S2>(command: S1, description: S2) -> Self
|
||||||
|
where
|
||||||
|
S1: Into<String>,
|
||||||
|
S2: Into<String>,
|
||||||
|
{
|
||||||
|
Self { command: command.into(), description: description.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command<S>(mut self, val: S) -> Self
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.command = val.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn description<S>(mut self, val: S) -> Self
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.description = val.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
292
tests/command.rs
Normal file
292
tests/command.rs
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
//! Test for `teloxide-macros`
|
||||||
|
|
||||||
|
use teloxide::utils::command::BotCommands;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_command_with_args() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
Start(String),
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = "/start arg1 arg2";
|
||||||
|
let expected = DefaultCommands::Start("arg1 arg2".to_string());
|
||||||
|
let actual = DefaultCommands::parse(data, "").unwrap();
|
||||||
|
assert_eq!(actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_command_with_non_string_arg() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
Start(i32),
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = "/start -50";
|
||||||
|
let expected = DefaultCommands::Start("-50".parse().unwrap());
|
||||||
|
let actual = DefaultCommands::parse(data, "").unwrap();
|
||||||
|
assert_eq!(actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_prefix() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
#[command(prefix = "!")]
|
||||||
|
Start(String),
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = "!start arg1 arg2";
|
||||||
|
let expected = DefaultCommands::Start("arg1 arg2".to_string());
|
||||||
|
let actual = DefaultCommands::parse(data, "").unwrap();
|
||||||
|
assert_eq!(actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn many_attributes() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
#[command(prefix = "!", description = "desc")]
|
||||||
|
Start,
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::Start,
|
||||||
|
DefaultCommands::parse("!start", "").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::descriptions().to_string(),
|
||||||
|
"!start — desc\n/help"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn global_attributes() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(prefix = "!", rename = "lowercase", description = "Bot commands")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
#[command(prefix = "/")]
|
||||||
|
Start,
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::Start,
|
||||||
|
DefaultCommands::parse("/start", "MyNameBot").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::Help,
|
||||||
|
DefaultCommands::parse("!help", "MyNameBot").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::descriptions().to_string(),
|
||||||
|
"Bot commands\n\n/start\n!help"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_command_with_bot_name() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
#[command(prefix = "/")]
|
||||||
|
Start,
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::Start,
|
||||||
|
DefaultCommands::parse("/start@MyNameBot", "MyNameBot").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_split() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
#[command(parse_with = "split")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
Start(u8, String),
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::Start(10, "hello".to_string()),
|
||||||
|
DefaultCommands::parse("/start 10 hello", "").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_split2() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
#[command(parse_with = "split", separator = "|")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
Start(u8, String),
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::Start(10, "hello".to_string()),
|
||||||
|
DefaultCommands::parse("/start 10|hello", "").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_custom_parser() {
|
||||||
|
mod parser {
|
||||||
|
use teloxide::utils::command::ParseError;
|
||||||
|
|
||||||
|
pub fn custom_parse_function(
|
||||||
|
s: String,
|
||||||
|
) -> Result<(u8, String), ParseError> {
|
||||||
|
let vec = s.split_whitespace().collect::<Vec<_>>();
|
||||||
|
let (left, right) = match vec.as_slice() {
|
||||||
|
[l, r] => (l, r),
|
||||||
|
_ => {
|
||||||
|
return Err(ParseError::IncorrectFormat(
|
||||||
|
"might be 2 arguments!".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
left.parse::<u8>().map(|res| (res, (*right).to_string())).map_err(
|
||||||
|
|_| {
|
||||||
|
ParseError::Custom(
|
||||||
|
"First argument must be a integer!".to_owned().into(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use parser::custom_parse_function;
|
||||||
|
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
#[command(parse_with = "custom_parse_function")]
|
||||||
|
Start(u8, String),
|
||||||
|
|
||||||
|
// Test <https://github.com/teloxide/teloxide/issues/668>.
|
||||||
|
#[command(parse_with = "parser::custom_parse_function")]
|
||||||
|
TestPath(u8, String),
|
||||||
|
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::Start(10, "hello".to_string()),
|
||||||
|
DefaultCommands::parse("/start 10 hello", "").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::TestPath(10, "hello".to_string()),
|
||||||
|
DefaultCommands::parse("/testpath 10 hello", "").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_named_fields() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
#[command(parse_with = "split")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
Start { num: u8, data: String },
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::Start { num: 10, data: "hello".to_string() },
|
||||||
|
DefaultCommands::parse("/start 10 hello", "").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn descriptions_off() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
enum DefaultCommands {
|
||||||
|
#[command(description = "off")]
|
||||||
|
Start,
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(DefaultCommands::descriptions().to_string(), "/help".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rename_rules() {
|
||||||
|
#[derive(BotCommands, Debug, PartialEq)]
|
||||||
|
enum DefaultCommands {
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
AaaAaa,
|
||||||
|
#[command(rename = "UPPERCASE")]
|
||||||
|
BbbBbb,
|
||||||
|
#[command(rename = "PascalCase")]
|
||||||
|
CccCcc,
|
||||||
|
#[command(rename = "camelCase")]
|
||||||
|
DddDdd,
|
||||||
|
#[command(rename = "snake_case")]
|
||||||
|
EeeEee,
|
||||||
|
#[command(rename = "SCREAMING_SNAKE_CASE")]
|
||||||
|
FffFff,
|
||||||
|
#[command(rename = "kebab-case")]
|
||||||
|
GggGgg,
|
||||||
|
#[command(rename = "SCREAMING-KEBAB-CASE")]
|
||||||
|
HhhHhh,
|
||||||
|
//#[command(rename = "Bar")]
|
||||||
|
//Foo,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::AaaAaa,
|
||||||
|
DefaultCommands::parse("/aaaaaa", "").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::BbbBbb,
|
||||||
|
DefaultCommands::parse("/BBBBBB", "").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::CccCcc,
|
||||||
|
DefaultCommands::parse("/CccCcc", "").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::DddDdd,
|
||||||
|
DefaultCommands::parse("/dddDdd", "").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::EeeEee,
|
||||||
|
DefaultCommands::parse("/eee_eee", "").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::FffFff,
|
||||||
|
DefaultCommands::parse("/FFF_FFF", "").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::GggGgg,
|
||||||
|
DefaultCommands::parse("/ggg-ggg", "").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
DefaultCommands::HhhHhh,
|
||||||
|
DefaultCommands::parse("/HHH-HHH", "").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",
|
||||||
|
DefaultCommands::descriptions().to_string()
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in a new issue