mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 06:25:10 +01:00
Added basic parse and stringify for new proc macro InlineButtons
This commit is contained in:
parent
94db1757dc
commit
8d61e7baff
13 changed files with 565 additions and 22 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -805,12 +805,6 @@ dependencies = [
|
|||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
|
@ -2085,7 +2079,7 @@ checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5"
|
|||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
"heck 0.5.0",
|
||||
"heck",
|
||||
"hex",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
|
@ -2281,7 +2275,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"sqlx",
|
||||
"teloxide-core",
|
||||
"teloxide-macros 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"teloxide-macros",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
|
@ -2333,19 +2327,7 @@ dependencies = [
|
|||
name = "teloxide-macros"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "teloxide-macros"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e2d33d809c3e7161a9ab18bedddf98821245014f0a78fa4d2c9430b2ec018c1"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
|
|
29
crates/teloxide-macros/src/button.rs
Normal file
29
crates/teloxide-macros/src/button.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use crate::{button_attr::ButtonAttrs, button_enum::ButtonEnum, fields_parse::ParserType, Result};
|
||||
|
||||
pub(crate) struct Button {
|
||||
/// Callback data name, with all renames already applied.
|
||||
pub data_name: String,
|
||||
pub parser: ParserType,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(
|
||||
data_name: &str,
|
||||
attributes: &[syn::Attribute],
|
||||
global_options: &ButtonEnum,
|
||||
) -> Result<Self> {
|
||||
let attrs = ButtonAttrs::from_attributes(attributes)?;
|
||||
let ButtonAttrs {
|
||||
rename,
|
||||
// FIXME: error on/do not ignore separator
|
||||
fields_separator: _,
|
||||
} = attrs;
|
||||
|
||||
let data_name = match rename {
|
||||
Some((rename, _)) => rename,
|
||||
None => String::from(data_name),
|
||||
};
|
||||
|
||||
Ok(Self { data_name, parser: global_options.parser_type.clone() })
|
||||
}
|
||||
}
|
113
crates/teloxide-macros/src/button_attr.rs
Normal file
113
crates/teloxide-macros/src/button_attr.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
use crate::{
|
||||
attr::{fold_attrs, Attr},
|
||||
error::compile_error_at,
|
||||
Result,
|
||||
};
|
||||
|
||||
use proc_macro2::Span;
|
||||
use syn::Attribute;
|
||||
|
||||
/// All attributes that can be used for `derive(InlineButtons)`
|
||||
pub(crate) struct ButtonAttrs {
|
||||
pub rename: Option<(String, Span)>,
|
||||
pub fields_separator: Option<(String, Span)>,
|
||||
}
|
||||
|
||||
/// A single k/v attribute for `InlineButtons` derive macro.
|
||||
///
|
||||
/// Similar to [`crate::command_attr::CommandAttr`]
|
||||
struct ButtonAttr {
|
||||
kind: ButtonAttrKind,
|
||||
sp: Span,
|
||||
}
|
||||
|
||||
/// Kind of [`ButtonAttr`].
|
||||
enum ButtonAttrKind {
|
||||
Rename(String),
|
||||
FieldsSeparator(String),
|
||||
}
|
||||
|
||||
impl ButtonAttrs {
|
||||
pub fn from_attributes(attributes: &[Attribute]) -> Result<Self> {
|
||||
use ButtonAttrKind::*;
|
||||
|
||||
fold_attrs(
|
||||
attributes,
|
||||
is_button_attribute,
|
||||
ButtonAttr::parse,
|
||||
Self { rename: None, fields_separator: None },
|
||||
|mut this, attr| {
|
||||
fn insert<T>(opt: &mut Option<(T, Span)>, x: T, sp: Span) -> Result<()> {
|
||||
match opt {
|
||||
slot @ None => {
|
||||
*slot = Some((x, sp));
|
||||
Ok(())
|
||||
}
|
||||
Some(_) => Err(compile_error_at("duplicate attribute", sp)),
|
||||
}
|
||||
}
|
||||
|
||||
match attr.kind {
|
||||
Rename(r) => insert(&mut this.rename, r, attr.sp),
|
||||
FieldsSeparator(s) => insert(&mut this.fields_separator, s, attr.sp),
|
||||
}?;
|
||||
|
||||
Ok(this)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonAttr {
|
||||
fn parse(attr: Attr) -> Result<Self> {
|
||||
use ButtonAttrKind::*;
|
||||
|
||||
let sp = attr.span();
|
||||
let Attr { mut key, value } = attr;
|
||||
|
||||
let outermost_key = key.pop().unwrap(); // `Attr`'s invariants ensure `key.len() > 0`
|
||||
|
||||
let kind = match &*outermost_key.to_string() {
|
||||
"button" => {
|
||||
let Some(attr) = key.pop() else {
|
||||
return Err(compile_error_at(
|
||||
"expected an attribute name",
|
||||
outermost_key.span(),
|
||||
));
|
||||
};
|
||||
|
||||
if let Some(unexpected_key) = key.last() {
|
||||
return Err(compile_error_at(
|
||||
&format!("{attr} can't have nested attributes"),
|
||||
unexpected_key.span(),
|
||||
));
|
||||
}
|
||||
|
||||
match &*attr.to_string() {
|
||||
"rename" => Rename(value.expect_string()?),
|
||||
"fields_separator" => FieldsSeparator(value.expect_string()?),
|
||||
_ => {
|
||||
return Err(compile_error_at(
|
||||
"unexpected attribute name (expected one of `rename` or \
|
||||
`fields_separator`",
|
||||
attr.span(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Err(compile_error_at(
|
||||
"unexpected attribute (expected `button`)",
|
||||
outermost_key.span(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self { kind, sp })
|
||||
}
|
||||
}
|
||||
|
||||
fn is_button_attribute(a: &Attribute) -> bool {
|
||||
matches!(a.path.get_ident(), Some(ident) if ident == "button")
|
||||
}
|
31
crates/teloxide-macros/src/button_enum.rs
Normal file
31
crates/teloxide-macros/src/button_enum.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use crate::{
|
||||
button_attr::ButtonAttrs, command_enum::variants_only_attr, error::compile_error_at,
|
||||
fields_parse::ParserType, Result,
|
||||
};
|
||||
|
||||
const DEFAULT_CALLBACK_DATA_SEPARATOR: &str = ";";
|
||||
|
||||
pub(crate) struct ButtonEnum {
|
||||
pub parser_type: ParserType,
|
||||
pub fields_separator: String,
|
||||
}
|
||||
|
||||
impl ButtonEnum {
|
||||
pub fn from_attributes(attributes: &[syn::Attribute]) -> Result<Self> {
|
||||
let attrs = ButtonAttrs::from_attributes(attributes)?;
|
||||
let ButtonAttrs { rename, fields_separator } = attrs;
|
||||
|
||||
variants_only_attr![rename];
|
||||
|
||||
let separator = fields_separator
|
||||
.map(|(s, _)| s)
|
||||
.unwrap_or_else(|| String::from(DEFAULT_CALLBACK_DATA_SEPARATOR));
|
||||
|
||||
// We can just always use a separator parser, since the user won't ever interact
|
||||
// with that
|
||||
Ok(Self {
|
||||
parser_type: ParserType::Split { separator: Some(separator.clone()) },
|
||||
fields_separator: separator,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -18,6 +18,8 @@ macro_rules! variants_only_attr {
|
|||
};
|
||||
}
|
||||
|
||||
pub(crate) use variants_only_attr;
|
||||
|
||||
pub(crate) struct CommandEnum {
|
||||
pub prefix: String,
|
||||
/// The bool is true if the description contains a doc comment
|
||||
|
|
82
crates/teloxide-macros/src/fields_stringify.rs
Normal file
82
crates/teloxide-macros/src/fields_stringify.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use quote::quote;
|
||||
use syn::{spanned::Spanned, Fields, FieldsNamed, FieldsUnnamed};
|
||||
|
||||
pub(crate) fn impl_stringify_args(
|
||||
fields: &Fields,
|
||||
self_variant: proc_macro2::TokenStream,
|
||||
self_string_name: String,
|
||||
self_string_variant: String,
|
||||
) -> proc_macro2::TokenStream {
|
||||
match fields {
|
||||
Fields::Unit => {
|
||||
quote! { #self_variant => ::std::result::Result::Ok(#self_string_variant.to_owned()), }
|
||||
}
|
||||
Fields::Unnamed(fields) => {
|
||||
impl_stringify_args_unnamed(fields, self_variant, self_string_name, self_string_variant)
|
||||
}
|
||||
Fields::Named(named) => {
|
||||
impl_stringify_args_named(named, self_variant, self_string_name, self_string_variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn impl_stringify_args_unnamed(
|
||||
data: &FieldsUnnamed,
|
||||
variant: proc_macro2::TokenStream,
|
||||
self_string_name: String,
|
||||
string_variant: String,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let names =
|
||||
(0..data.unnamed.len()).map(|i| syn::Ident::new(&format!("field_{}", i), variant.span()));
|
||||
let types = data.unnamed.iter().map(|f| &f.ty);
|
||||
let mut all_fields = quote! {};
|
||||
for ((name, ty), i) in names.clone().zip(types).zip(0..data.unnamed.len()) {
|
||||
all_fields.extend(quote! { {
|
||||
let stringified = #ty::to_string(&#name);
|
||||
if stringified.contains(fields_separator) {
|
||||
return ::std::result::Result::Err(StringifyError::SeparatorInUnnamedArgument {
|
||||
enum_variant: std::concat!(#self_string_name, "::", #string_variant).to_owned(),
|
||||
field: #i
|
||||
});
|
||||
}
|
||||
stringified
|
||||
}, })
|
||||
}
|
||||
let all_names = quote! { #(#names),* };
|
||||
let res = quote! {
|
||||
#variant(#all_names) => ::std::result::Result::Ok(
|
||||
::std::vec![#string_variant.to_owned(), #all_fields].join(fields_separator)
|
||||
),
|
||||
};
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn impl_stringify_args_named(
|
||||
data: &FieldsNamed,
|
||||
variant: proc_macro2::TokenStream,
|
||||
self_string_name: String,
|
||||
string_variant: String,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let names = data.named.iter().map(|f| f.ident.as_ref().unwrap());
|
||||
let types = data.named.iter().map(|f| &f.ty);
|
||||
let mut all_fields = quote! {};
|
||||
for (name, ty) in names.clone().zip(types) {
|
||||
all_fields.extend(quote! { {
|
||||
let stringified = #ty::to_string(&#name);
|
||||
if stringified.contains(fields_separator) {
|
||||
return ::std::result::Result::Err(StringifyError::SeparatorInNamedArgument {
|
||||
enum_variant: ::std::concat!(#self_string_name, "::", #string_variant).to_owned(),
|
||||
argument: ::std::stringify!(#name).to_string()
|
||||
});
|
||||
}
|
||||
stringified
|
||||
}, })
|
||||
}
|
||||
let all_names = quote! { #(#names),* };
|
||||
let res = quote! {
|
||||
#variant { #all_names } => ::std::result::Result::Ok(
|
||||
::std::vec![#string_variant.to_owned(), #all_fields].join(fields_separator)
|
||||
),
|
||||
};
|
||||
res
|
||||
}
|
116
crates/teloxide-macros/src/inline_buttons.rs
Normal file
116
crates/teloxide-macros/src/inline_buttons.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{spanned::Spanned, DeriveInput};
|
||||
|
||||
use crate::{
|
||||
button::Button, button_enum::ButtonEnum, compile_error, error::compile_error_at,
|
||||
fields_parse::impl_parse_args, fields_stringify::impl_stringify_args, unzip::Unzip3, Result,
|
||||
};
|
||||
|
||||
pub(crate) fn inline_buttons_impl(input: DeriveInput) -> Result<TokenStream> {
|
||||
let data_enum = get_enum_data(&input)?;
|
||||
let button_enum = ButtonEnum::from_attributes(&input.attrs)?;
|
||||
let type_name = &input.ident;
|
||||
|
||||
let Unzip3(var_init, var_info, var_stringify) = data_enum
|
||||
.variants
|
||||
.iter()
|
||||
.map(|variant| {
|
||||
let button = Button::new(&variant.ident.to_string(), &variant.attrs, &button_enum)?;
|
||||
|
||||
if button.data_name.len() > 64 {
|
||||
return Err(compile_error_at(
|
||||
// Limit of the TBA
|
||||
//
|
||||
// "64 chars" and not "64 bytes" because enum variants usually don't contain
|
||||
// non-ascii characters, and it's more understandable.
|
||||
"Enum variant is too long (64 chars max), please consider using \
|
||||
`#[button(rename=\"...\")` to reduce the size",
|
||||
variant.span(),
|
||||
));
|
||||
}
|
||||
|
||||
let variant_name = &variant.ident;
|
||||
let self_variant = quote! { Self::#variant_name };
|
||||
let button_data_name = button.data_name.clone();
|
||||
let self_string_variant = button_data_name.to_owned();
|
||||
let self_string_name = type_name.to_string();
|
||||
|
||||
let parse = impl_parse_args(&variant.fields, self_variant.clone(), &button.parser);
|
||||
let stringify = impl_stringify_args(
|
||||
&variant.fields,
|
||||
self_variant,
|
||||
self_string_name,
|
||||
self_string_variant,
|
||||
);
|
||||
|
||||
Ok((parse, button, stringify))
|
||||
})
|
||||
.collect::<Result<Unzip3<Vec<_>, Vec<_>, Vec<_>>>>()?;
|
||||
|
||||
let fn_parse = impl_parse(&var_info, &var_init, &button_enum.fields_separator);
|
||||
let fn_stringify = impl_stringify(&var_stringify, &button_enum.fields_separator);
|
||||
|
||||
let trait_impl = quote! {
|
||||
impl teloxide::utils::button::InlineButtons for #type_name {
|
||||
#fn_parse
|
||||
#fn_stringify
|
||||
}
|
||||
};
|
||||
|
||||
Ok(trait_impl)
|
||||
}
|
||||
|
||||
fn impl_parse(
|
||||
infos: &[Button],
|
||||
variants_initialization: &[proc_macro2::TokenStream],
|
||||
fields_separator: &str,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let matching_values = infos.iter().map(|c| c.data_name.clone());
|
||||
|
||||
quote! {
|
||||
fn parse(s: &str) -> ::std::result::Result<Self, teloxide::utils::command::ParseError> {
|
||||
use std::str::FromStr;
|
||||
use teloxide::utils::command::ParseError;
|
||||
|
||||
// 2 is used to only split once (=> in two parts),
|
||||
// we only need to split the enum variant and the rest of arguments.
|
||||
let mut words = s.splitn(2, #fields_separator);
|
||||
|
||||
let enum_variant = words.next().unwrap();
|
||||
|
||||
let args = words.next().unwrap_or("").to_owned();
|
||||
match enum_variant {
|
||||
#(
|
||||
#matching_values => Ok(#variants_initialization),
|
||||
)*
|
||||
_ => ::std::result::Result::Err(ParseError::UnknownCommand(enum_variant.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_stringify(
|
||||
stringify_return: &[proc_macro2::TokenStream],
|
||||
fields_separator: &str,
|
||||
) -> proc_macro2::TokenStream {
|
||||
quote! {
|
||||
fn stringify(self) -> ::std::result::Result<::std::string::String, teloxide::utils::button::StringifyError> {
|
||||
use std::string::ToString;
|
||||
use teloxide::utils::button::StringifyError;
|
||||
|
||||
let fields_separator = #fields_separator;
|
||||
|
||||
match self {
|
||||
#(#stringify_return)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_enum_data(input: &DeriveInput) -> Result<&syn::DataEnum> {
|
||||
match &input.data {
|
||||
syn::Data::Enum(data) => Ok(data),
|
||||
_ => Err(compile_error("`InlineButtons` is only allowed for enums")),
|
||||
}
|
||||
}
|
|
@ -2,14 +2,20 @@ extern crate proc_macro;
|
|||
|
||||
mod attr;
|
||||
mod bot_commands;
|
||||
mod button;
|
||||
mod button_attr;
|
||||
mod button_enum;
|
||||
mod command;
|
||||
mod command_attr;
|
||||
mod command_enum;
|
||||
mod error;
|
||||
mod fields_parse;
|
||||
mod fields_stringify;
|
||||
mod inline_buttons;
|
||||
mod rename_rules;
|
||||
mod unzip;
|
||||
|
||||
use crate::inline_buttons::inline_buttons_impl;
|
||||
pub(crate) use error::{compile_error, Result};
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
|
@ -22,3 +28,10 @@ pub fn bot_commands_derive(tokens: TokenStream) -> TokenStream {
|
|||
|
||||
bot_commands_impl(input).unwrap_or_else(<_>::into).into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(InlineButtons, attributes(button))]
|
||||
pub fn callback_data_derive(tokens: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(tokens as DeriveInput);
|
||||
|
||||
inline_buttons_impl(input).unwrap_or_else(<_>::into).into()
|
||||
}
|
||||
|
|
|
@ -16,3 +16,24 @@ where
|
|||
Unzip(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Unzip3<A, B, C>(pub A, pub B, pub C);
|
||||
|
||||
impl<A, B, C, T, U, V> FromIterator<(T, U, V)> for Unzip3<A, B, C>
|
||||
where
|
||||
A: Default + Extend<T>,
|
||||
B: Default + Extend<U>,
|
||||
C: Default + Extend<V>,
|
||||
{
|
||||
fn from_iter<I: IntoIterator<Item = (T, U, V)>>(iter: I) -> Self {
|
||||
let (mut a, mut b, mut c): (A, B, C) = Default::default();
|
||||
|
||||
for (t, u, v) in iter {
|
||||
a.extend([t]);
|
||||
b.extend([u]);
|
||||
c.extend([v]);
|
||||
}
|
||||
|
||||
Unzip3(a, b, c)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,8 @@ full = [
|
|||
[dependencies]
|
||||
# replace me by the actual version when release, and return path when it's time to make 0-day fixes
|
||||
teloxide-core = { path = "../../crates/teloxide-core", default-features = false }
|
||||
teloxide-macros = { version = "0.8", optional = true }
|
||||
# me too
|
||||
teloxide-macros = { path = "../../crates/teloxide-macros", optional = true }
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! Some useful utilities.
|
||||
|
||||
pub mod button;
|
||||
pub mod command;
|
||||
pub mod html;
|
||||
pub mod markdown;
|
||||
|
|
22
crates/teloxide/src/utils/button.rs
Normal file
22
crates/teloxide/src/utils/button.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
//! Docs later
|
||||
use super::command::ParseError;
|
||||
#[cfg(feature = "macros")]
|
||||
pub use teloxide_macros::InlineButtons;
|
||||
|
||||
/// Docs later
|
||||
pub trait InlineButtons: Sized {
|
||||
/// Parses the callback data.
|
||||
fn parse(s: &str) -> Result<Self, ParseError>;
|
||||
|
||||
/// Stringifies the callback data.
|
||||
fn stringify(self) -> Result<String, StringifyError>;
|
||||
}
|
||||
|
||||
/// Errors returned from [`InlineButtons::stringify`].
|
||||
///
|
||||
/// [`InlineButtons::stringify`]: InlineButtons::stringify
|
||||
#[derive(Debug)]
|
||||
pub enum StringifyError {
|
||||
SeparatorInNamedArgument { enum_variant: String, argument: String },
|
||||
SeparatorInUnnamedArgument { enum_variant: String, field: usize },
|
||||
}
|
130
crates/teloxide/tests/callback_data.rs
Normal file
130
crates/teloxide/tests/callback_data.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
#[cfg(feature = "macros")]
|
||||
use teloxide::utils::button::InlineButtons;
|
||||
|
||||
// We put tests here because macro expand in unit tests in module
|
||||
// teloxide::utils::command was a failure
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn parse_and_stringify_button_with_args() {
|
||||
#[derive(InlineButtons, Debug, PartialEq)]
|
||||
enum DefaultData {
|
||||
Fruit(String),
|
||||
Other,
|
||||
}
|
||||
|
||||
let data = "Fruit;apple";
|
||||
let expected = DefaultData::Fruit("apple".to_string());
|
||||
let actual = DefaultData::parse(data).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.stringify().unwrap(), data.to_owned())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn parse_and_stringify_button_with_non_string_arg() {
|
||||
#[derive(InlineButtons, Debug, PartialEq)]
|
||||
enum DefaultData {
|
||||
Fruit(i32),
|
||||
Other,
|
||||
}
|
||||
|
||||
let data = "Fruit;-50";
|
||||
let expected = DefaultData::Fruit("-50".parse().unwrap());
|
||||
let actual = DefaultData::parse(data).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.stringify().unwrap(), data.to_owned())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn stringify_button_error() {
|
||||
use teloxide::utils::button::StringifyError;
|
||||
|
||||
#[derive(InlineButtons, Debug, PartialEq)]
|
||||
#[button(fields_separator = ";")]
|
||||
enum DefaultData {
|
||||
Fruit(String),
|
||||
Other,
|
||||
}
|
||||
|
||||
let button = DefaultData::Fruit("test;test2".to_string());
|
||||
match button.stringify() {
|
||||
Err(StringifyError::SeparatorInUnnamedArgument { enum_variant, field }) => {
|
||||
assert_eq!(field, 0);
|
||||
assert_eq!(enum_variant, "DefaultData::Fruit")
|
||||
}
|
||||
_ => panic!("Expected an error!"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn parse_and_stringify_with_fields_separator1() {
|
||||
#[derive(InlineButtons, Debug, PartialEq)]
|
||||
#[button(fields_separator = ":")]
|
||||
enum DefaultData {
|
||||
Other,
|
||||
}
|
||||
|
||||
let data = "Other";
|
||||
let expected = DefaultData::Other;
|
||||
let actual = DefaultData::parse(data).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.stringify().unwrap(), data.to_owned())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn parse_and_stringify_with_fields_separator2() {
|
||||
#[derive(InlineButtons, Debug, PartialEq)]
|
||||
#[button(fields_separator = ":")]
|
||||
enum DefaultData {
|
||||
Fruit(u8),
|
||||
Other,
|
||||
}
|
||||
|
||||
let data = "Fruit:10";
|
||||
let expected = DefaultData::Fruit(10);
|
||||
let actual = DefaultData::parse("Fruit:10").unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.stringify().unwrap(), data.to_owned())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn parse_and_stringify_named_fields() {
|
||||
#[derive(InlineButtons, Debug, PartialEq)]
|
||||
enum DefaultData {
|
||||
Fruit { num: u8, data: String },
|
||||
Other,
|
||||
}
|
||||
|
||||
let data = "Fruit;10;hello";
|
||||
let expected = DefaultData::Fruit { num: 10, data: "hello".to_string() };
|
||||
let actual = DefaultData::parse(data).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.stringify().unwrap(), data.to_owned())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn stringify_button_named_fields_error() {
|
||||
use teloxide::utils::button::StringifyError;
|
||||
|
||||
#[derive(InlineButtons, Debug, PartialEq)]
|
||||
#[button(fields_separator = ";")]
|
||||
enum DefaultData {
|
||||
Fruit { num: u8, data: String },
|
||||
Other,
|
||||
}
|
||||
|
||||
let button = DefaultData::Fruit { num: 9, data: "test;test2".to_owned() };
|
||||
match button.stringify() {
|
||||
Err(StringifyError::SeparatorInNamedArgument { enum_variant, argument }) => {
|
||||
assert_eq!(argument, "data".to_owned());
|
||||
assert_eq!(enum_variant, "DefaultData::Fruit")
|
||||
}
|
||||
_ => panic!("Expected an error!"),
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue