mirror of
https://github.com/teloxide/teloxide.git
synced 2025-03-24 15:49:27 +01:00
added typed commands for unnamed variants of enum
This commit is contained in:
parent
fdf68463d9
commit
d27e741539
5 changed files with 162 additions and 77 deletions
|
@ -7,6 +7,8 @@ pub enum BotCommandAttribute {
|
||||||
Prefix,
|
Prefix,
|
||||||
Description,
|
Description,
|
||||||
RenameRule,
|
RenameRule,
|
||||||
|
CustomParser,
|
||||||
|
Separator,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for BotCommandAttribute {
|
impl Parse for BotCommandAttribute {
|
||||||
|
@ -16,6 +18,8 @@ impl Parse for BotCommandAttribute {
|
||||||
"prefix" => Ok(BotCommandAttribute::Prefix),
|
"prefix" => Ok(BotCommandAttribute::Prefix),
|
||||||
"description" => Ok(BotCommandAttribute::Description),
|
"description" => Ok(BotCommandAttribute::Description),
|
||||||
"rename" => Ok(BotCommandAttribute::RenameRule),
|
"rename" => Ok(BotCommandAttribute::RenameRule),
|
||||||
|
"parse_with" => Ok(BotCommandAttribute::CustomParser),
|
||||||
|
"separator" => Ok(BotCommandAttribute::Separator),
|
||||||
_ => Err(syn::Error::new(name_arg.span(), "unexpected argument")),
|
_ => Err(syn::Error::new(name_arg.span(), "unexpected argument")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
|
use crate::enum_attributes::CommandEnum;
|
||||||
|
use crate::fields_parse::ParserType;
|
||||||
use crate::{
|
use crate::{
|
||||||
attr::{Attr, BotCommandAttribute},
|
attr::{Attr, BotCommandAttribute},
|
||||||
rename_rules::rename_by_rule,
|
rename_rules::rename_by_rule,
|
||||||
};
|
};
|
||||||
use crate::enum_attributes::CommandEnum;
|
|
||||||
|
|
||||||
pub struct Command {
|
pub struct Command {
|
||||||
pub prefix: Option<String>,
|
pub prefix: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
pub parser: Option<ParserType>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub renamed: bool,
|
pub renamed: bool,
|
||||||
}
|
}
|
||||||
|
@ -20,6 +22,7 @@ impl Command {
|
||||||
let prefix = attrs.prefix;
|
let prefix = attrs.prefix;
|
||||||
let description = attrs.description;
|
let description = attrs.description;
|
||||||
let rename = attrs.rename;
|
let rename = attrs.rename;
|
||||||
|
let parser = attrs.parser;
|
||||||
if let Some(rename_rule) = rename {
|
if let Some(rename_rule) = rename {
|
||||||
new_name = rename_by_rule(name, &rename_rule);
|
new_name = rename_by_rule(name, &rename_rule);
|
||||||
renamed = true;
|
renamed = true;
|
||||||
|
@ -27,6 +30,7 @@ impl Command {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
prefix,
|
prefix,
|
||||||
description,
|
description,
|
||||||
|
parser,
|
||||||
name: new_name,
|
name: new_name,
|
||||||
renamed,
|
renamed,
|
||||||
})
|
})
|
||||||
|
@ -44,24 +48,28 @@ impl Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CommandAttrs {
|
pub struct CommandAttrs {
|
||||||
prefix: Option<String>,
|
pub(crate) prefix: Option<String>,
|
||||||
description: Option<String>,
|
pub(crate) description: Option<String>,
|
||||||
rename: Option<String>,
|
pub(crate) rename: Option<String>,
|
||||||
|
pub(crate) parser: Option<ParserType>,
|
||||||
|
pub(crate) separator: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_attrs(attrs: &[Attr]) -> Result<CommandAttrs, String> {
|
pub fn parse_attrs(attrs: &[Attr]) -> Result<CommandAttrs, String> {
|
||||||
let mut prefix = None;
|
let mut prefix = None;
|
||||||
let mut description = None;
|
let mut description = None;
|
||||||
let mut rename_rule = None;
|
let mut rename_rule = None;
|
||||||
|
let mut parser = None;
|
||||||
|
let mut separator = None;
|
||||||
|
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
match attr.name() {
|
match attr.name() {
|
||||||
BotCommandAttribute::Prefix => prefix = Some(attr.value()),
|
BotCommandAttribute::Prefix => prefix = Some(attr.value()),
|
||||||
BotCommandAttribute::Description => description = Some(attr.value()),
|
BotCommandAttribute::Description => description = Some(attr.value()),
|
||||||
BotCommandAttribute::RenameRule => rename_rule = Some(attr.value()),
|
BotCommandAttribute::RenameRule => rename_rule = Some(attr.value()),
|
||||||
#[allow(unreachable_patterns)]
|
BotCommandAttribute::CustomParser => parser = Some(ParserType::parse(&attr.value())),
|
||||||
_ => return Err("unexpected attribute".to_owned()),
|
BotCommandAttribute::Separator => separator = Some(attr.value()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,5 +77,7 @@ fn parse_attrs(attrs: &[Attr]) -> Result<CommandAttrs, String> {
|
||||||
prefix,
|
prefix,
|
||||||
description,
|
description,
|
||||||
rename: rename_rule,
|
rename: rename_rule,
|
||||||
|
parser,
|
||||||
|
separator,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
use crate::attr::{Attr, BotCommandAttribute};
|
use crate::attr::Attr;
|
||||||
|
use crate::command::parse_attrs;
|
||||||
|
use crate::fields_parse::ParserType;
|
||||||
|
|
||||||
pub struct CommandEnum {
|
pub struct CommandEnum {
|
||||||
pub prefix: Option<String>,
|
pub prefix: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub rename_rule: Option<String>,
|
pub rename_rule: Option<String>,
|
||||||
|
pub parser_type: ParserType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandEnum {
|
impl CommandEnum {
|
||||||
|
@ -13,6 +16,12 @@ impl CommandEnum {
|
||||||
let prefix = attrs.prefix;
|
let prefix = attrs.prefix;
|
||||||
let description = attrs.description;
|
let description = attrs.description;
|
||||||
let rename = attrs.rename;
|
let rename = attrs.rename;
|
||||||
|
let separator = attrs.separator;
|
||||||
|
let mut parser = attrs.parser.unwrap_or(ParserType::Default);
|
||||||
|
match (&mut parser, &separator) {
|
||||||
|
(ParserType::Split { separator }, Some(s)) => *separator = Some(s.clone()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
if let Some(rename_rule) = &rename {
|
if let Some(rename_rule) = &rename {
|
||||||
match rename_rule.as_str() {
|
match rename_rule.as_str() {
|
||||||
"lowercase" => {}
|
"lowercase" => {}
|
||||||
|
@ -23,34 +32,7 @@ impl CommandEnum {
|
||||||
prefix,
|
prefix,
|
||||||
description,
|
description,
|
||||||
rename_rule: rename,
|
rename_rule: rename,
|
||||||
|
parser_type: parser,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CommandAttrs {
|
|
||||||
prefix: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
rename: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_attrs(attrs: &[Attr]) -> Result<CommandAttrs, String> {
|
|
||||||
let mut prefix = None;
|
|
||||||
let mut description = None;
|
|
||||||
let mut rename_rule = None;
|
|
||||||
|
|
||||||
for attr in attrs {
|
|
||||||
match attr.name() {
|
|
||||||
BotCommandAttribute::Prefix => prefix = Some(attr.value()),
|
|
||||||
BotCommandAttribute::Description => description = Some(attr.value()),
|
|
||||||
BotCommandAttribute::RenameRule => rename_rule = Some(attr.value()),
|
|
||||||
#[allow(unreachable_patterns)]
|
|
||||||
_ => return Err("unexpected attribute".to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(CommandAttrs {
|
|
||||||
prefix,
|
|
||||||
description,
|
|
||||||
rename: rename_rule,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
77
src/fields_parse.rs
Normal file
77
src/fields_parse.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
extern crate quote;
|
||||||
|
|
||||||
|
use quote::__private::Span;
|
||||||
|
use quote::{quote, ToTokens};
|
||||||
|
use syn::FieldsUnnamed;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ParserType {
|
||||||
|
Default,
|
||||||
|
Split { separator: Option<String> },
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParserType {
|
||||||
|
pub fn parse(data: &str) -> Self {
|
||||||
|
match data {
|
||||||
|
"default" => ParserType::Default,
|
||||||
|
"split" => ParserType::Split { separator: None },
|
||||||
|
s => ParserType::Custom(s.to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn impl_parse_args_unnamed(
|
||||||
|
data: &FieldsUnnamed,
|
||||||
|
variant: impl ToTokens,
|
||||||
|
parser_type: &ParserType,
|
||||||
|
) -> quote::__private::TokenStream {
|
||||||
|
let function_to_parse = match parser_type {
|
||||||
|
ParserType::Default => {
|
||||||
|
match data.unnamed.len() {
|
||||||
|
1 => {
|
||||||
|
quote! { (|s: String| Ok((FromStr::from_str(&s).map_err(|_|ParseError::UncorrectFormat)?,)) ) }
|
||||||
|
}
|
||||||
|
_ => quote! { compile_error!("Expected 1 argument") },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParserType::Split { separator } => parser_with_separator(
|
||||||
|
&separator.clone().unwrap_or(" ".to_owned()),
|
||||||
|
data.unnamed.len(),
|
||||||
|
),
|
||||||
|
ParserType::Custom(s) => {
|
||||||
|
let ident = syn::Ident::new(&s, Span::call_site());
|
||||||
|
quote! { #ident }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let get_arguments = quote! { let arguments = #function_to_parse(args)?; };
|
||||||
|
let iter = 0..data.unnamed.len();
|
||||||
|
let mut initialization = quote! {};
|
||||||
|
for i in iter {
|
||||||
|
initialization.extend(quote! { arguments.#i, })
|
||||||
|
}
|
||||||
|
let res = quote! {
|
||||||
|
{
|
||||||
|
#get_arguments
|
||||||
|
#variant(#initialization)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parser_with_separator(separator: &str, count_args: usize) -> quote::__private::TokenStream {
|
||||||
|
let inner = quote! { let splited = s.split(#separator).collect::<Vec<_>>(); };
|
||||||
|
let mut inner2 = quote! {};
|
||||||
|
for i in 0..count_args {
|
||||||
|
inner2.extend(
|
||||||
|
quote! { FromStr::from_str(splited[#i]).map_err(|_|ParseError::UncorrectFormat)?, },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let res = quote! {
|
||||||
|
(|s: String| {
|
||||||
|
#inner
|
||||||
|
Ok((#inner2))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
res
|
||||||
|
}
|
92
src/lib.rs
92
src/lib.rs
|
@ -1,10 +1,13 @@
|
||||||
mod attr;
|
mod attr;
|
||||||
mod command;
|
mod command;
|
||||||
mod enum_attributes;
|
mod enum_attributes;
|
||||||
|
mod fields_parse;
|
||||||
mod rename_rules;
|
mod rename_rules;
|
||||||
|
|
||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
|
extern crate quote;
|
||||||
extern crate syn;
|
extern crate syn;
|
||||||
|
use crate::fields_parse::impl_parse_args_unnamed;
|
||||||
use crate::{
|
use crate::{
|
||||||
attr::{Attr, VecAttrs},
|
attr::{Attr, VecAttrs},
|
||||||
command::Command,
|
command::Command,
|
||||||
|
@ -12,7 +15,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::{quote, ToTokens};
|
use quote::{quote, ToTokens};
|
||||||
use syn::{parse_macro_input, DeriveInput, Variant};
|
use syn::{parse_macro_input, DeriveInput, Fields};
|
||||||
|
|
||||||
macro_rules! get_or_return {
|
macro_rules! get_or_return {
|
||||||
($($some:tt)*) => {
|
($($some:tt)*) => {
|
||||||
|
@ -36,7 +39,7 @@ pub fn derive_telegram_command_enum(tokens: TokenStream) -> TokenStream {
|
||||||
Err(e) => return compile_error(e),
|
Err(e) => return compile_error(e),
|
||||||
};
|
};
|
||||||
|
|
||||||
let variants: Vec<&syn::Variant> = data_enum.variants.iter().map(|attr| attr).collect();
|
let variants: Vec<&syn::Variant> = data_enum.variants.iter().map(|variant| variant).collect();
|
||||||
|
|
||||||
let mut variant_infos = vec![];
|
let mut variant_infos = vec![];
|
||||||
for variant in variants.iter() {
|
for variant in variants.iter() {
|
||||||
|
@ -57,15 +60,29 @@ pub fn derive_telegram_command_enum(tokens: TokenStream) -> TokenStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut vec_impl_create = vec![];
|
||||||
|
for (variant, info) in variants.iter().zip(variant_infos.iter()) {
|
||||||
|
let var = &variant.ident;
|
||||||
|
let variantt = quote! { Self::#var };
|
||||||
|
match &variant.fields {
|
||||||
|
Fields::Unnamed(fields) => {
|
||||||
|
let parser = info.parser.as_ref().unwrap_or(&command_enum.parser_type);
|
||||||
|
vec_impl_create.push(impl_parse_args_unnamed(fields, variantt, parser));
|
||||||
|
}
|
||||||
|
Fields::Unit => {
|
||||||
|
vec_impl_create.push(variantt);
|
||||||
|
}
|
||||||
|
_ => panic!("only unnamed fields"), // TODO: named fields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ident = &input.ident;
|
let ident = &input.ident;
|
||||||
|
|
||||||
let fn_try_from = impl_try_parse_command(&variants, &variant_infos, &command_enum);
|
|
||||||
let fn_descriptions = impl_descriptions(&variant_infos, &command_enum);
|
let fn_descriptions = impl_descriptions(&variant_infos, &command_enum);
|
||||||
let fn_parse = impl_parse();
|
let fn_parse = impl_parse(&variant_infos, &command_enum, &vec_impl_create);
|
||||||
|
|
||||||
let trait_impl = quote! {
|
let trait_impl = quote! {
|
||||||
impl BotCommand for #ident {
|
impl BotCommand for #ident {
|
||||||
#fn_try_from
|
|
||||||
#fn_descriptions
|
#fn_descriptions
|
||||||
#fn_parse
|
#fn_parse
|
||||||
}
|
}
|
||||||
|
@ -74,23 +91,7 @@ pub fn derive_telegram_command_enum(tokens: TokenStream) -> TokenStream {
|
||||||
TokenStream::from(trait_impl)
|
TokenStream::from(trait_impl)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn impl_try_parse_command(variants: &[&Variant], infos: &[Command], global: &CommandEnum) -> impl ToTokens {
|
fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> quote::__private::TokenStream {
|
||||||
let matching_values = infos.iter().map(|c| c.get_matched_value(global));
|
|
||||||
let variant_ident = variants.iter().map(|variant| &variant.ident);
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
fn try_from(value: &str) -> Option<Self> {
|
|
||||||
match value {
|
|
||||||
#(
|
|
||||||
#matching_values => Some(Self::#variant_ident),
|
|
||||||
)*
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> impl ToTokens {
|
|
||||||
let global_description = if let Some(s) = &global.description {
|
let global_description = if let Some(s) = &global.description {
|
||||||
quote! { #s, "\n", }
|
quote! { #s, "\n", }
|
||||||
} else {
|
} else {
|
||||||
|
@ -105,31 +106,42 @@ fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> impl ToTokens {
|
||||||
});
|
});
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
fn descriptions() -> &'static str {
|
fn descriptions() -> String {
|
||||||
std::concat!(#global_description #(#command, #description, '\n'),*)
|
std::concat!(#global_description #(#command, #description, '\n'),*).to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn impl_parse() -> impl ToTokens {
|
fn impl_parse(
|
||||||
|
infos: &[Command],
|
||||||
|
global: &CommandEnum,
|
||||||
|
variants_initialization: &[quote::__private::TokenStream],
|
||||||
|
) -> quote::__private::TokenStream {
|
||||||
|
let matching_values = infos.iter().map(|c| c.get_matched_value(global));
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
fn parse<N>(s: &str, bot_name: N) -> Option<(Self, Vec<&str>)>
|
fn parse<N>(s: &str, bot_name: N) -> Result<Self, ParseError>
|
||||||
where
|
where
|
||||||
N: Into<String>
|
N: Into<String>
|
||||||
{
|
{
|
||||||
let mut words = s.split_whitespace();
|
let mut words = s.splitn(2, ' ');
|
||||||
let mut splited = words.next()?.split('@');
|
let mut splited = words.next().ok_or(ParseError::UncorrectFormat)?.split('@');
|
||||||
let command_raw = splited.next()?;
|
let command_raw = splited.next().ok_or(ParseError::UncorrectFormat)?;
|
||||||
let bot = splited.next();
|
let bot = splited.next();
|
||||||
let bot_name = bot_name.into();
|
let bot_name = bot_name.into();
|
||||||
match bot {
|
match bot {
|
||||||
Some(name) if name == bot_name => {}
|
Some(name) if name == bot_name => {}
|
||||||
None => {}
|
None => {}
|
||||||
_ => return None,
|
Some(n) => return Err(ParseError::WrongBotName(n.to_string())),
|
||||||
}
|
}
|
||||||
let command = Self::try_from(command_raw)?;
|
let mut args = words.next().unwrap_or("").to_string();
|
||||||
Some((command, words.collect()))
|
match command_raw {
|
||||||
}
|
#(
|
||||||
|
#matching_values => Ok(#variants_initialization),
|
||||||
|
)*
|
||||||
|
_ => Err(ParseError::UncorrectCommand(command_raw.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue