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
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
quote = "1.0.7"
|
||||
proc-macro2 = "1.0.19"
|
||||
syn = { version = "1.0.13", features = ["full"] }
|
||||
heck = "0.4.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
[dev-dependencies]
|
||||
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