diff --git a/src/attr.rs b/src/attr.rs index f57c1783..949e1659 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -7,6 +7,8 @@ pub enum BotCommandAttribute { Prefix, Description, RenameRule, + CustomParser, + Separator, } impl Parse for BotCommandAttribute { @@ -16,6 +18,8 @@ impl Parse for BotCommandAttribute { "prefix" => Ok(BotCommandAttribute::Prefix), "description" => Ok(BotCommandAttribute::Description), "rename" => Ok(BotCommandAttribute::RenameRule), + "parse_with" => Ok(BotCommandAttribute::CustomParser), + "separator" => Ok(BotCommandAttribute::Separator), _ => Err(syn::Error::new(name_arg.span(), "unexpected argument")), } } diff --git a/src/command.rs b/src/command.rs index 36496f26..0aa11fb9 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,12 +1,14 @@ +use crate::enum_attributes::CommandEnum; +use crate::fields_parse::ParserType; use crate::{ attr::{Attr, BotCommandAttribute}, rename_rules::rename_by_rule, }; -use crate::enum_attributes::CommandEnum; pub struct Command { pub prefix: Option, pub description: Option, + pub parser: Option, pub name: String, pub renamed: bool, } @@ -20,6 +22,7 @@ impl Command { let prefix = attrs.prefix; let description = attrs.description; let rename = attrs.rename; + let parser = attrs.parser; if let Some(rename_rule) = rename { new_name = rename_by_rule(name, &rename_rule); renamed = true; @@ -27,6 +30,7 @@ impl Command { Ok(Self { prefix, description, + parser, name: new_name, renamed, }) @@ -44,24 +48,28 @@ impl Command { } } -struct CommandAttrs { - prefix: Option, - description: Option, - rename: Option, +pub struct CommandAttrs { + pub(crate) prefix: Option, + pub(crate) description: Option, + pub(crate) rename: Option, + pub(crate) parser: Option, + pub(crate) separator: Option, } -fn parse_attrs(attrs: &[Attr]) -> Result { +pub fn parse_attrs(attrs: &[Attr]) -> Result { let mut prefix = None; let mut description = None; let mut rename_rule = None; + let mut parser = None; + let mut separator = 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()), + BotCommandAttribute::CustomParser => parser = Some(ParserType::parse(&attr.value())), + BotCommandAttribute::Separator => separator = Some(attr.value()), } } @@ -69,5 +77,7 @@ fn parse_attrs(attrs: &[Attr]) -> Result { prefix, description, rename: rename_rule, + parser, + separator, }) } diff --git a/src/enum_attributes.rs b/src/enum_attributes.rs index 728b80eb..92086683 100644 --- a/src/enum_attributes.rs +++ b/src/enum_attributes.rs @@ -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 prefix: Option, pub description: Option, pub rename_rule: Option, + pub parser_type: ParserType, } impl CommandEnum { @@ -13,6 +16,12 @@ impl CommandEnum { let prefix = attrs.prefix; let description = attrs.description; 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 { match rename_rule.as_str() { "lowercase" => {} @@ -23,34 +32,7 @@ impl CommandEnum { prefix, description, rename_rule: rename, + parser_type: parser, }) } } - -struct CommandAttrs { - prefix: Option, - description: Option, - rename: Option, -} - -fn parse_attrs(attrs: &[Attr]) -> Result { - 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, - }) -} diff --git a/src/fields_parse.rs b/src/fields_parse.rs new file mode 100644 index 00000000..d641d657 --- /dev/null +++ b/src/fields_parse.rs @@ -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 }, + 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::>(); }; + 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 +} diff --git a/src/lib.rs b/src/lib.rs index 4e445232..dc6c1b69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,13 @@ mod attr; mod command; mod enum_attributes; +mod fields_parse; mod rename_rules; extern crate proc_macro; +extern crate quote; extern crate syn; +use crate::fields_parse::impl_parse_args_unnamed; use crate::{ attr::{Attr, VecAttrs}, command::Command, @@ -12,7 +15,7 @@ use crate::{ }; use proc_macro::TokenStream; use quote::{quote, ToTokens}; -use syn::{parse_macro_input, DeriveInput, Variant}; +use syn::{parse_macro_input, DeriveInput, Fields}; macro_rules! get_or_return { ($($some:tt)*) => { @@ -36,7 +39,7 @@ pub fn derive_telegram_command_enum(tokens: TokenStream) -> TokenStream { 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![]; 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 fn_try_from = impl_try_parse_command(&variants, &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! { impl BotCommand for #ident { - #fn_try_from #fn_descriptions #fn_parse } @@ -74,23 +91,7 @@ pub fn derive_telegram_command_enum(tokens: TokenStream) -> TokenStream { TokenStream::from(trait_impl) } -fn impl_try_parse_command(variants: &[&Variant], infos: &[Command], global: &CommandEnum) -> impl ToTokens { - 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 { - match value { - #( - #matching_values => Some(Self::#variant_ident), - )* - _ => None - } - } - } -} - -fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> impl ToTokens { +fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> quote::__private::TokenStream { let global_description = if let Some(s) = &global.description { quote! { #s, "\n", } } else { @@ -105,31 +106,42 @@ fn impl_descriptions(infos: &[Command], global: &CommandEnum) -> impl ToTokens { }); quote! { - fn descriptions() -> &'static str { - std::concat!(#global_description #(#command, #description, '\n'),*) + fn descriptions() -> String { + 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! { - fn parse(s: &str, bot_name: N) -> Option<(Self, Vec<&str>)> + fn parse(s: &str, bot_name: N) -> Result where - N: Into + N: Into { - let mut words = s.split_whitespace(); - let mut splited = words.next()?.split('@'); - let command_raw = splited.next()?; - let bot = splited.next(); - let bot_name = bot_name.into(); - match bot { - Some(name) if name == bot_name => {} - None => {} - _ => return None, - } - let command = Self::try_from(command_raw)?; - Some((command, words.collect())) - } + let mut words = s.splitn(2, ' '); + let mut splited = words.next().ok_or(ParseError::UncorrectFormat)?.split('@'); + let command_raw = splited.next().ok_or(ParseError::UncorrectFormat)?; + let bot = splited.next(); + let bot_name = bot_name.into(); + match bot { + Some(name) if name == bot_name => {} + None => {} + Some(n) => return Err(ParseError::WrongBotName(n.to_string())), + } + let mut args = words.next().unwrap_or("").to_string(); + match command_raw { + #( + #matching_values => Ok(#variants_initialization), + )* + _ => Err(ParseError::UncorrectCommand(command_raw.to_string())), + } + } } }