Merge pull request #2 from teloxide/readme

added type safe arguments
This commit is contained in:
p0lunin 2020-04-25 20:12:22 +03:00 committed by GitHub
commit 34cc9a9f31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 112 additions and 44 deletions

View file

@ -9,7 +9,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
quote = "1.0.2" quote = "1.0.3"
syn = "1.0.13" syn = "1.0.13"
[lib] [lib]

39
Readme.md Normal file
View file

@ -0,0 +1,39 @@
# teloxide-macros
The teloxide's procedural macros.
## Example
```rust
use teloxide::utils::command::BotCommand;
#[derive(BotCommand, PartialEq, Debug)]
#[command(rename = "lowercase")]
enum AdminCommand {
Mute,
Ban,
}
let (command, args) = AdminCommand::parse("/ban 5 h", "bot_name").unwrap();
assert_eq!(command, AdminCommand::Ban);
assert_eq!(args, vec!["5", "h"]);
```
## Enum attributes
1. `#[command(rename = "rule")]`
Rename all commands by rule. Allowed rules are `lowercase`. If you will not
use this attribute, commands will be parsed by their original names.
2. `#[command(prefix = "prefix")]`
Change a prefix for all commands (the default is `/`).
3. `#[command(description = "description")]`
Add a sumary description of commands before all commands.
## Variant attributes
1. `#[command(rename = "rule")]`
Rename one command by a rule. Allowed rules are `lowercase`, `%some_name%`,
where `%some_name%` is any string, a new name.
2. `#[command(prefix = "prefix")]`
Change a prefix for one command (the default is `/`).
3. `#[command(description = "description")]`
Add a description of one command.
All variant attributes overlap the `enum` attributes.

View file

@ -1,8 +1,8 @@
use crate::enum_attributes::CommandEnum;
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>,
@ -40,7 +40,11 @@ impl Command {
} else { } else {
"/" "/"
}; };
String::from(prefix) + &self.name if let Some(rule) = &global_parameters.rename_rule {
String::from(prefix) + &rename_by_rule(&self.name, rule.as_str())
} else {
String::from(prefix) + &self.name
}
} }
} }

View file

@ -1,5 +1,6 @@
use crate::attr::{Attr, BotCommandAttribute}; use crate::attr::{Attr, BotCommandAttribute};
#[derive(Debug)]
pub struct CommandEnum { pub struct CommandEnum {
pub prefix: Option<String>, pub prefix: Option<String>,
pub description: Option<String>, pub description: Option<String>,

13
src/fields_parse.rs Normal file
View file

@ -0,0 +1,13 @@
extern crate quote;
use quote::quote;
use syn::FieldsUnnamed;
pub fn impl_parse_args_unnamed(data: &FieldsUnnamed) -> quote::__private::TokenStream {
let iter = 0..data.unnamed.len();
let mut tokens = quote! {};
for _ in iter {
tokens.extend(quote! { CommandArgument::parse(&mut args)?, });
}
quote! { (#tokens) }
}

View file

@ -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, Variant};
macro_rules! get_or_return { macro_rules! get_or_return {
($($some:tt)*) => { ($($some:tt)*) => {
@ -36,10 +39,23 @@ 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 vec_impl_create = vec![];
for variant in &variants {
match &variant.fields {
Fields::Unnamed(fields) => {
vec_impl_create.push(impl_parse_args_unnamed(fields));
}
Fields::Unit => {
vec_impl_create.push(quote! {});
}
_ => panic!("only unnamed fields"), // TODO: named fields
}
}
let mut variant_infos = vec![]; let mut variant_infos = vec![];
for variant in variants.iter() { for variant in &variants {
let mut attrs = Vec::new(); let mut attrs = Vec::new();
for attr in &variant.attrs { for attr in &variant.attrs {
match attr.parse_args::<VecAttrs>() { match attr.parse_args::<VecAttrs>() {
@ -59,13 +75,11 @@ pub fn derive_telegram_command_enum(tokens: TokenStream) -> TokenStream {
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(&variants, &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 +88,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 +103,44 @@ 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(
variants: &[&Variant],
infos: &[Command],
global: &CommandEnum,
variants_initialization: &[quote::__private::TokenStream],
) -> 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! { quote! {
fn parse<N>(s: &str, bot_name: N) -> Option<(Self, Vec<&str>)> fn parse<N>(s: &str, bot_name: N) -> Option<Self>
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()?.split('@');
let command_raw = splited.next()?; let command_raw = splited.next()?;
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, _ => return None,
} }
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 => Some(Self::#variant_ident #variants_initialization),
)*
_ => None,
}
}
} }
} }