mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 14:35:36 +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",
|
"hashbrown 0.14.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "heck"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -2085,7 +2079,7 @@ checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"either",
|
"either",
|
||||||
"heck 0.5.0",
|
"heck",
|
||||||
"hex",
|
"hex",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -2281,7 +2275,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"teloxide-core",
|
"teloxide-core",
|
||||||
"teloxide-macros 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"teloxide-macros",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
@ -2333,19 +2327,7 @@ dependencies = [
|
||||||
name = "teloxide-macros"
|
name = "teloxide-macros"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck",
|
||||||
"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",
|
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"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(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
|
||||||
|
|
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 attr;
|
||||||
mod bot_commands;
|
mod bot_commands;
|
||||||
|
mod button;
|
||||||
|
mod button_attr;
|
||||||
|
mod button_enum;
|
||||||
mod command;
|
mod command;
|
||||||
mod command_attr;
|
mod command_attr;
|
||||||
mod command_enum;
|
mod command_enum;
|
||||||
mod error;
|
mod error;
|
||||||
mod fields_parse;
|
mod fields_parse;
|
||||||
|
mod fields_stringify;
|
||||||
|
mod inline_buttons;
|
||||||
mod rename_rules;
|
mod rename_rules;
|
||||||
mod unzip;
|
mod unzip;
|
||||||
|
|
||||||
|
use crate::inline_buttons::inline_buttons_impl;
|
||||||
pub(crate) use error::{compile_error, Result};
|
pub(crate) use error::{compile_error, Result};
|
||||||
use syn::{parse_macro_input, DeriveInput};
|
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()
|
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)
|
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]
|
[dependencies]
|
||||||
# replace me by the actual version when release, and return path when it's time to make 0-day fixes
|
# 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-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_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//! Some useful utilities.
|
//! Some useful utilities.
|
||||||
|
|
||||||
|
pub mod button;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod html;
|
pub mod html;
|
||||||
pub mod markdown;
|
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