Actually tests that macros work

This commit is contained in:
Maybe Waffle 2022-09-29 14:08:27 +04:00
parent 822300eb89
commit 66e7ebd7f1
4 changed files with 523 additions and 2 deletions

View file

@ -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
View 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
View 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
View 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()
);
}