diff --git a/src/attr.rs b/src/attr.rs index 765cff7c..8f1945f1 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -1,10 +1,10 @@ +use crate::Result; + use syn::{ parse::{Parse, ParseBuffer, ParseStream}, Attribute, LitStr, Token, }; -use crate::Result; - pub(crate) enum CommandAttrName { Prefix, Description, diff --git a/src/bot_commands.rs b/src/bot_commands.rs new file mode 100644 index 00000000..e8d552c9 --- /dev/null +++ b/src/bot_commands.rs @@ -0,0 +1,148 @@ +use crate::{ + attr::CommandAttrs, command::Command, command_enum::CommandEnum, + compile_error, fields_parse::impl_parse_args, unzip::Unzip, Result, +}; + +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +pub(crate) fn bot_commands_impl(input: DeriveInput) -> Result { + let data_enum = get_enum_data(&input)?; + let enum_attrs = CommandAttrs::from_attributes(&input.attrs)?; + let command_enum = CommandEnum::try_from(enum_attrs)?; + + let Unzip(var_init, var_info) = data_enum + .variants + .iter() + .map(|variant| { + let attrs = CommandAttrs::from_attributes(&variant.attrs)?; + let command = Command::try_from(attrs, &variant.ident.to_string())?; + + let variant_name = &variant.ident; + let self_variant = quote! { Self::#variant_name }; + + let parser = + command.parser.as_ref().unwrap_or(&command_enum.parser_type); + let parse = impl_parse_args(&variant.fields, self_variant, parser); + + Ok((parse, command)) + }) + .collect::, Vec<_>>>>()?; + + let type_name = &input.ident; + let fn_descriptions = impl_descriptions(&var_info, &command_enum); + let fn_parse = impl_parse(&var_info, &command_enum, &var_init); + let fn_commands = impl_commands(&var_info, &command_enum); + + let trait_impl = quote! { + impl BotCommands for #type_name { + #fn_descriptions + #fn_parse + #fn_commands + } + }; + + Ok(TokenStream::from(trait_impl)) +} + +fn impl_commands( + infos: &[Command], + global: &CommandEnum, +) -> proc_macro2::TokenStream { + let commands = infos + .iter() + .filter(|command| command.description_is_enabled()) + .map(|command| { + let c = command.get_matched_value(global); + let d = command.description.as_deref().unwrap_or_default(); + quote! { BotCommand::new(#c,#d) } + }); + + quote! { + fn bot_commands() -> Vec { + use teloxide::types::BotCommand; + vec![#(#commands),*] + } + } +} + +fn impl_descriptions( + infos: &[Command], + global: &CommandEnum, +) -> proc_macro2::TokenStream { + let command_descriptions = infos + .iter() + .filter(|command| command.description_is_enabled()) + .map(|c| { + let (prefix, command) = c.get_matched_value2(global); + let description = c.description.clone().unwrap_or_default(); + quote! { CommandDescription { prefix: #prefix, command: #command, description: #description } } + }); + + let global_description = match global.description.as_deref() { + Some(gd) => quote! { .global_description(#gd) }, + None => quote! {}, + }; + + quote! { + fn descriptions() -> teloxide::utils::command::CommandDescriptions<'static> { + use teloxide::utils::command::{CommandDescriptions, CommandDescription}; + use std::borrow::Cow; + + CommandDescriptions::new(&[ + #(#command_descriptions),* + ]) + #global_description + } + } +} + +fn impl_parse( + infos: &[Command], + global: &CommandEnum, + variants_initialization: &[proc_macro2::TokenStream], +) -> proc_macro2::TokenStream { + let matching_values = infos.iter().map(|c| c.get_matched_value(global)); + + quote! { + fn parse(s: &str, bot_name: N) -> Result + where + N: Into + { + // FIXME: we should probably just call a helper function from `teloxide`, instead of parsing command syntax ourselves + 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 command and the rest of arguments. + let mut words = s.splitn(2, ' '); + + // Unwrap: split iterators always have at least one item + let mut full_command = words.next().unwrap().split('@'); + let command = full_command.next().unwrap(); + + let bot_username = full_command.next(); + match bot_username { + None => {} + Some(username) if username.eq_ignore_ascii_case(&bot_name.into()) => {} + Some(n) => return Err(ParseError::WrongBotName(n.to_owned())), + } + + let args = words.next().unwrap_or("").to_owned(); + match command { + #( + #matching_values => Ok(#variants_initialization), + )* + _ => Err(ParseError::UnknownCommand(command.to_owned())), + } + } + } +} + +fn get_enum_data(input: &DeriveInput) -> Result<&syn::DataEnum> { + match &input.data { + syn::Data::Enum(data) => Ok(data), + _ => Err(compile_error("`BotCommands` is only allowed for enums")), + } +} diff --git a/src/error.rs b/src/error.rs index 96d0c694..db737a1d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,9 +13,9 @@ where Error(TokenStream::from(quote! { compile_error! { #data } })) } -impl From for proc_macro::TokenStream { +impl From for proc_macro2::TokenStream { fn from(Error(e): Error) -> Self { - e.into() + e } } diff --git a/src/fields_parse.rs b/src/fields_parse.rs index af33a490..d3a4ab34 100644 --- a/src/fields_parse.rs +++ b/src/fields_parse.rs @@ -1,5 +1,3 @@ -extern crate quote; - use quote::quote; use syn::{Fields, FieldsNamed, FieldsUnnamed, Type}; diff --git a/src/lib.rs b/src/lib.rs index f6397f26..e590ead3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ // TODO: refactor this shit. +extern crate proc_macro; + mod attr; +mod bot_commands; mod command; mod command_enum; mod error; @@ -8,162 +11,15 @@ mod fields_parse; mod rename_rules; mod unzip; -extern crate proc_macro; -extern crate quote; -extern crate syn; -use crate::{ - attr::CommandAttrs, command::Command, command_enum::CommandEnum, - fields_parse::impl_parse_args, unzip::Unzip, -}; -use proc_macro::TokenStream; -use quote::quote; -use syn::DeriveInput; +pub(crate) use error::{compile_error, Result}; +use syn::{parse_macro_input, DeriveInput}; -pub(crate) use error::{compile_error, Error, Result}; +use crate::bot_commands::bot_commands_impl; +use proc_macro::TokenStream; #[proc_macro_derive(BotCommands, attributes(command))] pub fn bot_commands_derive(tokens: TokenStream) -> TokenStream { - bot_commands_impl(tokens).unwrap_or_else(Error::into) -} - -fn bot_commands_impl(tokens: TokenStream) -> Result { - let input = syn::parse_macro_input::parse::(tokens)?; - - let data_enum = get_enum_data(&input)?; - let enum_attrs = CommandAttrs::from_attributes(&input.attrs)?; - let command_enum = CommandEnum::try_from(enum_attrs)?; - - let Unzip(var_init, var_info) = data_enum - .variants - .iter() - .map(|variant| { - let attrs = CommandAttrs::from_attributes(&variant.attrs)?; - let command = Command::try_from(attrs, &variant.ident.to_string())?; - - let variant_name = &variant.ident; - let self_variant = quote! { Self::#variant_name }; - - let parser = - command.parser.as_ref().unwrap_or(&command_enum.parser_type); - let parse = impl_parse_args(&variant.fields, self_variant, parser); - - Ok((parse, command)) - }) - .collect::, Vec<_>>, Error>>()?; - - let type_name = &input.ident; - let fn_descriptions = impl_descriptions(&var_info, &command_enum); - let fn_parse = impl_parse(&var_info, &command_enum, &var_init); - let fn_commands = impl_commands(&var_info, &command_enum); - - let trait_impl = quote! { - impl BotCommands for #type_name { - #fn_descriptions - #fn_parse - #fn_commands - } - }; - - Ok(TokenStream::from(trait_impl)) -} - -fn impl_commands( - infos: &[Command], - global: &CommandEnum, -) -> proc_macro2::TokenStream { - let commands = infos - .iter() - .filter(|command| command.description_is_enabled()) - .map(|command| { - let c = command.get_matched_value(global); - let d = command.description.as_deref().unwrap_or_default(); - quote! { BotCommand::new(#c,#d) } - }); - - quote! { - fn bot_commands() -> Vec { - use teloxide::types::BotCommand; - vec![#(#commands),*] - } - } -} - -fn impl_descriptions( - infos: &[Command], - global: &CommandEnum, -) -> proc_macro2::TokenStream { - let command_descriptions = infos - .iter() - .filter(|command| command.description_is_enabled()) - .map(|c| { - let (prefix, command) = c.get_matched_value2(global); - let description = c.description.clone().unwrap_or_default(); - quote! { CommandDescription { prefix: #prefix, command: #command, description: #description } } - }); - - let global_description = match global.description.as_deref() { - Some(gd) => quote! { .global_description(#gd) }, - None => quote! {}, - }; - - quote! { - fn descriptions() -> teloxide::utils::command::CommandDescriptions<'static> { - use teloxide::utils::command::{CommandDescriptions, CommandDescription}; - use std::borrow::Cow; - - CommandDescriptions::new(&[ - #(#command_descriptions),* - ]) - #global_description - } - } -} - -fn impl_parse( - infos: &[Command], - global: &CommandEnum, - variants_initialization: &[proc_macro2::TokenStream], -) -> proc_macro2::TokenStream { - let matching_values = infos.iter().map(|c| c.get_matched_value(global)); - - quote! { - fn parse(s: &str, bot_name: N) -> Result - where - N: Into - { - // FIXME: we should probably just call a helper function from `teloxide`, instead of parsing command syntax ourselves - 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 command and the rest of arguments. - let mut words = s.splitn(2, ' '); - - // Unwrap: split iterators always have at least one item - let mut full_command = words.next().unwrap().split('@'); - let command = full_command.next().unwrap(); - - let bot_username = full_command.next(); - match bot_username { - None => {} - Some(username) if username.eq_ignore_ascii_case(&bot_name.into()) => {} - Some(n) => return Err(ParseError::WrongBotName(n.to_owned())), - } - - let args = words.next().unwrap_or("").to_owned(); - match command { - #( - #matching_values => Ok(#variants_initialization), - )* - _ => Err(ParseError::UnknownCommand(command.to_owned())), - } - } - } -} - -fn get_enum_data(input: &DeriveInput) -> Result<&syn::DataEnum> { - match &input.data { - syn::Data::Enum(data) => Ok(data), - _ => Err(compile_error("`BotCommands` is only allowed for enums")), - } + let input = parse_macro_input!(tokens as DeriveInput); + + bot_commands_impl(input).unwrap_or_else(<_>::into).into() } diff --git a/src/rename_rules.rs b/src/rename_rules.rs index 159724a1..55280617 100644 --- a/src/rename_rules.rs +++ b/src/rename_rules.rs @@ -1,12 +1,12 @@ // Some concepts are from Serde. +use crate::error::{compile_error, Result}; + use heck::{ ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, }; -use crate::error::{compile_error, Result}; - #[derive(Copy, Clone, Debug)] pub(crate) enum RenameRule { /// -> `lowercase`