Merge teloxide-macros into main repository

This commit is contained in:
Maybe Waffle 2022-11-01 16:12:14 +04:00
commit 6ea8fb1670
16 changed files with 1537 additions and 0 deletions

View file

@ -0,0 +1,72 @@
on: [push, pull_request]
name: Continuous integration
jobs:
ci:
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
- beta
- nightly
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt, clippy
- name: stable/beta build
uses: actions-rs/cargo@v1
if: matrix.rust == 'stable' || matrix.rust == 'beta'
with:
command: build
args: --verbose --features ""
- name: nightly build
uses: actions-rs/cargo@v1
if: matrix.rust == 'nightly'
with:
command: build
args: --verbose --all-features
- name: stable/beta test
uses: actions-rs/cargo@v1
if: matrix.rust == 'stable' || matrix.rust == 'beta'
with:
command: test
args: --verbose --features ""
- name: nightly test
uses: actions-rs/cargo@v1
if: matrix.rust == 'nightly'
with:
command: test
args: --verbose --all-features
- name: fmt
uses: actions-rs/cargo@v1
if: matrix.rust == 'nightly'
with:
command: fmt
args: --all -- --check
- name: stable/beta clippy
uses: actions-rs/cargo@v1
if: matrix.rust == 'stable' || matrix.rust == 'beta'
with:
command: clippy
args: --all-targets --features "" -- -D warnings
- name: nightly clippy
uses: actions-rs/cargo@v1
if: matrix.rust == 'nightly'
with:
command: clippy
args: --all-targets --all-features -- -D warnings

View file

@ -0,0 +1,144 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## unreleased
## 0.7.0 - 2022-10-06
### Removed
- `derive(DialogueState)` macro
### Changed
- `#[command(rename = "...")]` now always renames to `"..."`; to rename multiple commands using the same pattern, use `#[command(rename_rule = "snake_case")]` and the like.
- `#[command(parse_with = ...)]` now requires a path, instead of a string, when specifying custom parsers.
### Fixed
- `#[derive(BotCommands)]` even if the trait is not imported ([issue #717](https://github.com/teloxide/teloxide/issues/717)).
## 0.6.3 - 2022-07-19
### Fixed
- Allow specifying a path to a command handler in `parse_with` ([PR #27](https://github.com/teloxide/teloxide-macros/pull/27)).
## 0.6.2 - 2022-05-27
### Fixed
- Fix `#[command(rename = "...")]` for custom command names ([issue 633](https://github.com/teloxide/teloxide/issues/633)).
## 0.6.1 - 2022-04-26
### Fixed
- Fix `#[derive(DialogueState)]` (function return type `dptree::Handler`).
## 0.6.0 - 2022-04-09
### Removed
- Support for the old dispatching: `#[teloxide(subtransition)]` [**BC**].
### Deprecated
- `#[derive(DialogueState)]` in favour of `teloxide::handler!`.
## 0.5.1 - 2022-03-23
### Fixed
- Make bot name check case-insensitive ([PR #16](https://github.com/teloxide/teloxide-macros/pull/16)).
### Added
- More command rename rules: `UPPERCASE`, `PascalCase`, `camelCase`, `snake_case`, `SCREAMING_SNAKE_CASE`, `kebab-case`, and `SCREAMING-KEBAB-CASE` ([PR #18](https://github.com/teloxide/teloxide-macros/pull/18)).
## 0.5.0 - 2022-02-05
### Added
- The `BotCommand::bot_commands()` method that returns `Vec<BotCommand>` ([PR #13](https://github.com/teloxide/teloxide-macros/pull/13)).
- `#[derive(DialogueState)]`, `#[handler_out(...)]`, `#[handler(...)]`.
## 0.4.1 - 2021-07-11
### Fixed
- Fix generics support for a variant's arguments ([PR #8](https://github.com/teloxide/teloxide-macros/issues/8)).
## 0.4.0 - 2021-03-19
### Changed
- Adjust dialogues with the latest teloxide (v0.4.0).
## 0.3.2 - 2020-07-27
### Added
- `#[derive(Transition)]` with `#[teloxide(subtransition)]`.
### Removed
- The `dev` branch.
## 0.3.1 - 2020-07-04
### Added
- Now you can remove command from showing in descriptions by defining `description` attribute as `"off"`.
## 0.3.0 - 2020-07-03
### Changed
- The description in `Cargo.toml` was changed to from "The teloxide's macros for internal usage" to "The teloxide's procedural macros".
- Now parsing of arguments happens using special function. There are 3 possible variants:
- Using `default` parser, which only put all text in one String field.
- Using `split` parser, which split all text by `separator` (by default is whitespace) and then use FromStr::from_str to construct value.
- Using custom separator.
- Now function `parse` return Result<T, ParseError> instead of Option<T>.
### Added
- This `CHANGELOG.md`.
- `.gitignore`.
- `#[parse_with]` attribute.
- `#[separator='%sep%']` attribute.
## 0.2.1 - 2020-02-25
### Changed
- The description in `Cargo.toml` was changed to from "The teloxide's macros for internal usage" to "The teloxide's procedural macros".
### Added
- This `CHANGELOG.md`.
- `.gitignore`.
- The functionality to parse commands only with a correct bot's name (breaks backwards compatibility).
## 0.1.2 - 2020-02-24
### Changed
- The same as v0.1.1, but fixes [the issue](https://github.com/teloxide/teloxide/issues/176) about backwards compatibility.
## 0.2.0 - YANKED
### Changed
- Fixes [the issue](https://github.com/teloxide/teloxide/issues/176) about backwards compatibility, but fairly soon I realised that semver recommends to use v0.1.2 instead.
## 0.1.1 - 2020-02-23
### Added
- The `LICENSE` file.
### Changed
- Backwards compatibility is broken and was fixed in v0.1.2.
## 0.1.0 - 2020-02-19
### Added
- This project.

View file

@ -0,0 +1,21 @@
[package]
name = "teloxide-macros"
version = "0.7.0"
description = "The teloxide's procedural macros"
license = "MIT"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
quote = "1.0.7"
proc-macro2 = "1.0.19"
syn = { version = "1.0.13", features = ["full"] }
heck = "0.4.0"
[dev-dependencies]
# XXX: Do not enable `macros` feature
teloxide = { git = "https://github.com/teloxide/teloxide.git", rev = "b5e237a8a22f9f987b6e4245b9b6c3ca1f804c19" }

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-2022 teloxide
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,7 @@
format_code_in_doc_comments = true
wrap_comments = true
format_strings = true
max_width = 80
imports_granularity = "Crate"
use_small_heuristics = "Max"
use_field_init_shorthand = true

View file

@ -0,0 +1,156 @@
use crate::{error::compile_error_at, Result};
use proc_macro2::Span;
use syn::{
parse::{Parse, ParseBuffer, ParseStream},
spanned::Spanned,
Attribute, Ident, Lit, Path, Token,
};
pub(crate) fn fold_attrs<A, R>(
attrs: &[Attribute],
filter: fn(&Attribute) -> bool,
parse: impl Fn(Attr) -> Result<R>,
init: A,
f: impl Fn(A, R) -> Result<A>,
) -> Result<A> {
attrs
.iter()
.filter(|&a| filter(a))
.flat_map(|attribute| {
// FIXME: don't allocate here
let attrs =
match attribute.parse_args_with(|input: &ParseBuffer| {
input.parse_terminated::<_, Token![,]>(Attr::parse)
}) {
Ok(ok) => ok,
Err(err) => return vec![Err(err.into())],
};
attrs.into_iter().map(&parse).collect()
})
.try_fold(init, |acc, r| r.and_then(|r| f(acc, r)))
}
/// An attribute key-value pair.
///
/// For example:
/// ```text
/// #[blahblah(key = "puff", value = 12, nope)]
/// ^^^^^^^^^^^^ ^^^^^^^^^^ ^^^^
/// ```
pub(crate) struct Attr {
pub key: Ident,
pub value: AttrValue,
}
/// Value of an attribute.
///
/// For example:
/// ```text
/// #[blahblah(key = "puff", value = 12, nope)]
/// ^^^^^^ ^^ ^-- (None pseudo-value)
/// ```
pub(crate) enum AttrValue {
Path(Path),
Lit(Lit),
None(Span),
}
impl Parse for Attr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key = input.parse::<Ident>()?;
let value = match input.peek(Token![=]) {
true => {
input.parse::<Token![=]>()?;
input.parse::<AttrValue>()?
}
false => AttrValue::None(input.span()),
};
Ok(Self { key, value })
}
}
impl Attr {
pub(crate) fn span(&self) -> Span {
self.key
.span()
.join(self.value.span())
.unwrap_or_else(|| self.key.span())
}
}
impl AttrValue {
/// Unwraps this value if it's a string literal.
pub fn expect_string(self) -> Result<String> {
self.expect("a string", |this| match this {
AttrValue::Lit(Lit::Str(s)) => Ok(s.value()),
_ => Err(this),
})
}
// /// Unwraps this value if it's a path.
// pub fn expect_path(self) -> Result<Path> {
// self.expect("a path", |this| match this {
// AttrValue::Path(p) => Ok(p),
// _ => Err(this),
// })
// }
pub fn expect<T>(
self,
expected: &str,
f: impl FnOnce(Self) -> Result<T, Self>,
) -> Result<T> {
f(self).map_err(|this| {
compile_error_at(
&format!("expected {expected}, found {}", this.descr()),
this.span(),
)
})
}
fn descr(&self) -> &'static str {
use Lit::*;
match self {
Self::None(_) => "nothing",
Self::Lit(l) => match l {
Str(_) | ByteStr(_) => "a string",
Char(_) => "a character",
Byte(_) | Int(_) => "an integer",
Float(_) => "a floating point integer",
Bool(_) => "a boolean",
Verbatim(_) => ":shrug:",
},
Self::Path(_) => "a path",
}
}
/// Returns span of the value
///
/// ```text
/// #[blahblah(key = "puff", value = 12, nope )]
/// ^^^^^^ ^^ ^
/// ```
fn span(&self) -> Span {
match self {
Self::Path(p) => p.span(),
Self::Lit(l) => l.span(),
Self::None(sp) => *sp,
}
}
}
impl Parse for AttrValue {
fn parse(input: ParseStream) -> syn::Result<Self> {
let this = match input.peek(Lit) {
true => Self::Lit(input.parse()?),
false => Self::Path(input.parse()?),
};
Ok(this)
}
}

View file

@ -0,0 +1,141 @@
use crate::{
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<TokenStream> {
let data_enum = get_enum_data(&input)?;
let command_enum = CommandEnum::from_attributes(&input.attrs)?;
let Unzip(var_init, var_info) = data_enum
.variants
.iter()
.map(|variant| {
let command = Command::new(
&variant.ident.to_string(),
&variant.attrs,
&command_enum,
)?;
let variant_name = &variant.ident;
let self_variant = quote! { Self::#variant_name };
let parse =
impl_parse_args(&variant.fields, self_variant, &command.parser);
Ok((parse, command))
})
.collect::<Result<Unzip<Vec<_>, Vec<_>>>>()?;
let type_name = &input.ident;
let fn_descriptions = impl_descriptions(&var_info, &command_enum);
let fn_parse = impl_parse(&var_info, &var_init);
let fn_commands = impl_commands(&var_info);
let trait_impl = quote! {
impl teloxide::utils::command::BotCommands for #type_name {
#fn_descriptions
#fn_parse
#fn_commands
}
};
Ok(trait_impl)
}
fn impl_commands(infos: &[Command]) -> proc_macro2::TokenStream {
let commands = infos
.iter()
.filter(|command| command.description_is_enabled())
.map(|command| {
let c = command.get_prefixed_command();
let d = command.description.as_deref().unwrap_or_default();
quote! { BotCommand::new(#c,#d) }
});
quote! {
fn bot_commands() -> Vec<teloxide::types::BotCommand> {
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(|Command { prefix, name, description, ..}| {
let description = description.clone().unwrap_or_default();
quote! { CommandDescription { prefix: #prefix, command: #name, 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],
variants_initialization: &[proc_macro2::TokenStream],
) -> proc_macro2::TokenStream {
let matching_values = infos.iter().map(|c| c.get_prefixed_command());
quote! {
fn parse(s: &str, bot_name: &str) -> Result<Self, teloxide::utils::command::ParseError> {
// 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) => {}
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")),
}
}

View file

@ -0,0 +1,65 @@
use crate::{
command_attr::CommandAttrs, command_enum::CommandEnum,
error::compile_error_at, fields_parse::ParserType, Result,
};
pub(crate) struct Command {
/// Prefix of this command, for example "/".
pub prefix: String,
/// Description for the command.
pub description: Option<String>,
/// Name of the command, with all renames already applied.
pub name: String,
/// Parser for arguments of this command.
pub parser: ParserType,
}
impl Command {
pub fn new(
name: &str,
attributes: &[syn::Attribute],
global_options: &CommandEnum,
) -> Result<Self> {
let attrs = CommandAttrs::from_attributes(attributes)?;
let CommandAttrs {
prefix,
description,
rename_rule,
rename,
parser,
// FIXME: error on/do not ignore separator
separator: _,
} = attrs;
let name = match (rename, rename_rule) {
(Some((rename, _)), None) => rename,
(Some(_), Some((_, sp))) => {
return Err(compile_error_at(
"`rename_rule` can't be applied to `rename`-d variant",
sp,
))
}
(None, Some((rule, _))) => rule.apply(name),
(None, None) => global_options.rename_rule.apply(name),
};
let prefix = prefix
.map(|(p, _)| p)
.unwrap_or_else(|| global_options.prefix.clone());
let description = description.map(|(d, _)| d);
let parser = parser
.map(|(p, _)| p)
.unwrap_or_else(|| global_options.parser_type.clone());
Ok(Self { prefix, description, parser, name })
}
pub fn get_prefixed_command(&self) -> String {
let Self { prefix, name, .. } = self;
format!("{prefix}{name}")
}
pub(crate) fn description_is_enabled(&self) -> bool {
self.description != Some("off".to_owned())
}
}

View file

@ -0,0 +1,129 @@
use crate::{
attr::{fold_attrs, Attr},
error::compile_error_at,
fields_parse::ParserType,
rename_rules::RenameRule,
Result,
};
use proc_macro2::Span;
use syn::Attribute;
/// All attributes that can be used for `derive(BotCommands)`
pub(crate) struct CommandAttrs {
pub prefix: Option<(String, Span)>,
pub description: Option<(String, Span)>,
pub rename_rule: Option<(RenameRule, Span)>,
pub rename: Option<(String, Span)>,
pub parser: Option<(ParserType, Span)>,
pub separator: Option<(String, Span)>,
}
/// A single k/v attribute for `BotCommands` derive macro.
///
/// For example:
/// ```text
/// #[command(prefix = "!", rename_rule = "snake_case")]
/// /^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^---- CommandAttr { kind: RenameRule(SnakeCase) }
/// |
/// CommandAttr { kind: Prefix("!") }
/// ```
struct CommandAttr {
kind: CommandAttrKind,
sp: Span,
}
/// Kind of [`CommandAttr`].
enum CommandAttrKind {
Prefix(String),
Description(String),
RenameRule(RenameRule),
Rename(String),
ParseWith(ParserType),
Separator(String),
}
impl CommandAttrs {
pub fn from_attributes(attributes: &[Attribute]) -> Result<Self> {
use CommandAttrKind::*;
fold_attrs(
attributes,
is_command_attribute,
CommandAttr::parse,
Self {
prefix: None,
description: None,
rename_rule: None,
rename: None,
parser: None,
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 {
Prefix(p) => insert(&mut this.prefix, p, attr.sp),
Description(d) => insert(&mut this.description, d, attr.sp),
RenameRule(r) => insert(&mut this.rename_rule, r, attr.sp),
Rename(r) => insert(&mut this.rename, r, attr.sp),
ParseWith(p) => insert(&mut this.parser, p, attr.sp),
Separator(s) => insert(&mut this.separator, s, attr.sp),
}?;
Ok(this)
},
)
}
}
impl CommandAttr {
fn parse(attr: Attr) -> Result<Self> {
use CommandAttrKind::*;
let sp = attr.span();
let Attr { key, value } = attr;
let kind = match &*key.to_string() {
"prefix" => Prefix(value.expect_string()?),
"description" => Description(value.expect_string()?),
"rename_rule" => RenameRule(
value
.expect_string()
.and_then(|r| self::RenameRule::parse(&r))?,
),
"rename" => Rename(value.expect_string()?),
"parse_with" => ParseWith(ParserType::parse(value)?),
"separator" => Separator(value.expect_string()?),
_ => {
return Err(compile_error_at(
"unexpected attribute name (expected one of `prefix`, \
`description`, `rename`, `parse_with` and `separator`",
key.span(),
))
}
};
Ok(Self { kind, sp })
}
}
fn is_command_attribute(a: &Attribute) -> bool {
match a.path.get_ident() {
Some(ident) => ident == "command",
_ => false,
}
}

View file

@ -0,0 +1,50 @@
use crate::{
command_attr::CommandAttrs, error::compile_error_at,
fields_parse::ParserType, rename_rules::RenameRule, Result,
};
pub(crate) struct CommandEnum {
pub prefix: String,
pub description: Option<String>,
pub rename_rule: RenameRule,
pub parser_type: ParserType,
}
impl CommandEnum {
pub fn from_attributes(attributes: &[syn::Attribute]) -> Result<Self> {
let attrs = CommandAttrs::from_attributes(attributes)?;
let CommandAttrs {
prefix,
description,
rename_rule,
rename,
parser,
separator,
} = attrs;
if let Some((_rename, sp)) = rename {
return Err(compile_error_at(
"`rename` attribute can only be applied to enums *variants*",
sp,
));
}
let mut parser = parser.map(|(p, _)| p).unwrap_or(ParserType::Default);
// FIXME: Error on unused separator
if let (ParserType::Split { separator }, Some((s, _))) =
(&mut parser, &separator)
{
*separator = Some(s.clone())
}
Ok(Self {
prefix: prefix.map(|(p, _)| p).unwrap_or_else(|| "/".to_owned()),
description: description.map(|(d, _)| d),
rename_rule: rename_rule
.map(|(rr, _)| rr)
.unwrap_or(RenameRule::Identity),
parser_type: parser,
})
}
}

View file

@ -0,0 +1,54 @@
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
pub(crate) struct Error(TokenStream);
pub(crate) fn compile_error<T>(data: T) -> Error
where
T: ToTokens,
{
Error(quote! { compile_error! { #data } })
}
pub(crate) fn compile_error_at(msg: &str, sp: Span) -> Error {
use proc_macro2::{
Delimiter, Group, Ident, Literal, Punct, Spacing, TokenTree,
};
// compile_error! { $msg }
let ts = TokenStream::from_iter(vec![
TokenTree::Ident(Ident::new("compile_error", sp)),
TokenTree::Punct({
let mut punct = Punct::new('!', Spacing::Alone);
punct.set_span(sp);
punct
}),
TokenTree::Group({
let mut group = Group::new(Delimiter::Brace, {
TokenStream::from_iter(vec![TokenTree::Literal({
let mut string = Literal::string(msg);
string.set_span(sp);
string
})])
});
group.set_span(sp);
group
}),
]);
Error(ts)
}
impl From<Error> for proc_macro2::TokenStream {
fn from(Error(e): Error) -> Self {
e
}
}
impl From<syn::Error> for Error {
fn from(e: syn::Error) -> Self {
Self(e.to_compile_error())
}
}

View file

@ -0,0 +1,164 @@
use quote::quote;
use syn::{Fields, FieldsNamed, FieldsUnnamed, Type};
use crate::{attr::AttrValue, error::Result};
#[derive(Clone)]
pub(crate) enum ParserType {
Default,
Split { separator: Option<String> },
Custom(syn::Path),
}
impl ParserType {
pub fn parse(value: AttrValue) -> Result<Self> {
value.expect(
r#""default", "split", or a path to a custom parser function"#,
|v| match v {
AttrValue::Path(p) => Ok(ParserType::Custom(p)),
AttrValue::Lit(syn::Lit::Str(ref l)) => match &*l.value() {
"default" => Ok(ParserType::Default),
"split" => Ok(ParserType::Split { separator: None }),
_ => Err(v),
},
_ => Err(v),
},
)
}
}
pub(crate) fn impl_parse_args(
fields: &Fields,
self_variant: proc_macro2::TokenStream,
parser: &ParserType,
) -> proc_macro2::TokenStream {
match fields {
Fields::Unit => self_variant,
Fields::Unnamed(fields) => {
impl_parse_args_unnamed(fields, self_variant, parser)
}
Fields::Named(named) => {
impl_parse_args_named(named, self_variant, parser)
}
}
}
pub(crate) fn impl_parse_args_unnamed(
data: &FieldsUnnamed,
variant: proc_macro2::TokenStream,
parser_type: &ParserType,
) -> proc_macro2::TokenStream {
let get_arguments =
create_parser(parser_type, data.unnamed.iter().map(|f| &f.ty));
let iter = (0..data.unnamed.len()).map(syn::Index::from);
let mut initialization = quote! {};
for i in iter {
initialization.extend(quote! { arguments.#i, })
}
let res = quote! {
{
#get_arguments
#variant(#initialization)
}
};
res
}
pub(crate) fn impl_parse_args_named(
data: &FieldsNamed,
variant: proc_macro2::TokenStream,
parser_type: &ParserType,
) -> proc_macro2::TokenStream {
let get_arguments =
create_parser(parser_type, data.named.iter().map(|f| &f.ty));
let i = (0..).map(syn::Index::from);
let name = data.named.iter().map(|f| f.ident.as_ref().unwrap());
let res = quote! {
{
#get_arguments
#variant { #(#name: arguments.#i),* }
}
};
res
}
fn create_parser<'a>(
parser_type: &ParserType,
mut types: impl ExactSizeIterator<Item = &'a Type>,
) -> proc_macro2::TokenStream {
let function_to_parse = match parser_type {
ParserType::Default => match types.len() {
1 => {
let ty = types.next().unwrap();
quote! {
(
|s: String| {
let res = <#ty>::from_str(&s)
.map_err(|e| ParseError::IncorrectFormat(e.into()))?;
Ok((res,))
}
)
}
}
_ => {
quote! { compile_error!("Default parser works only with exactly 1 field") }
}
},
ParserType::Split { separator } => parser_with_separator(
&separator.clone().unwrap_or_else(|| " ".to_owned()),
types,
),
ParserType::Custom(path) => quote! { #path },
};
quote! {
let arguments = #function_to_parse(args)?;
}
}
fn parser_with_separator<'a>(
separator: &str,
types: impl ExactSizeIterator<Item = &'a Type>,
) -> proc_macro2::TokenStream {
let expected = types.len();
let res = {
let found = 0usize..;
quote! {
(
#(
{
let s = splitted.next().ok_or(ParseError::TooFewArguments {
expected: #expected,
found: #found,
message: format!("Expected but not found arg number {}", #found + 1),
})?;
<#types>::from_str(s).map_err(|e| ParseError::IncorrectFormat(e.into()))?
}
),*
)
}
};
let res = quote! {
(
|s: String| {
let mut splitted = s.split(#separator);
let res = #res;
match splitted.next() {
Some(d) => Err(ParseError::TooManyArguments {
expected: #expected,
found: #expected + 1,
message: format!("Excess argument: {}", d),
}),
None => Ok(res)
}
}
)
};
res
}

View file

@ -0,0 +1,24 @@
extern crate proc_macro;
mod attr;
mod bot_commands;
mod command;
mod command_attr;
mod command_enum;
mod error;
mod fields_parse;
mod rename_rules;
mod unzip;
pub(crate) use error::{compile_error, Result};
use syn::{parse_macro_input, DeriveInput};
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 {
let input = parse_macro_input!(tokens as DeriveInput);
bot_commands_impl(input).unwrap_or_else(<_>::into).into()
}

View file

@ -0,0 +1,171 @@
// Some concepts are from Serde.
use crate::error::{compile_error, Result};
use heck::{
ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase,
ToShoutySnakeCase, ToSnakeCase,
};
#[derive(Copy, Clone, Debug)]
pub(crate) enum RenameRule {
/// -> `lowercase`
LowerCase,
/// -> `UPPERCASE`
UpperCase,
/// -> `PascalCase`
PascalCase,
/// -> `camelCase`
CamelCase,
/// -> `snake_case`
SnakeCase,
/// -> `SCREAMING_SNAKE_CASE`
ScreamingSnakeCase,
/// -> `kebab-case`
KebabCase,
/// -> `SCREAMING-KEBAB-CASE`
ScreamingKebabCase,
/// Leaves input as-is
Identity,
}
impl RenameRule {
/// Apply a renaming rule to a string, returning the version expected in the
/// source.
///
/// See tests for the details how it will work.
pub fn apply(self, input: &str) -> String {
use RenameRule::*;
match self {
LowerCase => input.to_lowercase(),
UpperCase => input.to_uppercase(),
PascalCase => input.to_pascal_case(),
CamelCase => input.to_lower_camel_case(),
SnakeCase => input.to_snake_case(),
ScreamingSnakeCase => input.to_shouty_snake_case(),
KebabCase => input.to_kebab_case(),
ScreamingKebabCase => input.to_shouty_kebab_case(),
Identity => input.to_owned(),
}
}
pub fn parse(rule: &str) -> Result<Self> {
use RenameRule::*;
let rule = match rule {
"lowercase" => LowerCase,
"UPPERCASE" => UpperCase,
"PascalCase" => PascalCase,
"camelCase" => CamelCase,
"snake_case" => SnakeCase,
"SCREAMING_SNAKE_CASE" => ScreamingSnakeCase,
"kebab-case" => KebabCase,
"SCREAMING-KEBAB-CASE" => ScreamingKebabCase,
"identity" => Identity,
invalid => {
return Err(compile_error(format!(
"invalid rename rule `{invalid}` (supported rules: \
`lowercase`, `UPPERCASE`, `PascalCase`, `camelCase`, \
`snake_case`, `SCREAMING_SNAKE_CASE`, `kebab-case`, \
`SCREAMING-KEBAB-CASE` and `identity`)"
)))
}
};
Ok(rule)
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_eq {
($input:expr => $output:expr) => {
let rule = RenameRule::parse(TYPE).unwrap();
assert_eq!(rule.apply($input), $output);
};
}
#[test]
fn test_lowercase() {
const TYPE: &str = "lowercase";
test_eq!("HelloWorld" => "helloworld");
test_eq!("Hello_World" => "hello_world");
test_eq!("Hello-World" => "hello-world");
test_eq!("helloWorld" => "helloworld");
}
#[test]
fn test_uppercase() {
const TYPE: &str = "UPPERCASE";
test_eq!("HelloWorld" => "HELLOWORLD");
test_eq!("Hello_World" => "HELLO_WORLD");
test_eq!("Hello-World" => "HELLO-WORLD");
test_eq!("helloWorld" => "HELLOWORLD");
}
#[test]
fn test_pascalcase() {
const TYPE: &str = "PascalCase";
test_eq!("HelloWorld" => "HelloWorld");
test_eq!("Hello_World" => "HelloWorld");
test_eq!("Hello-World" => "HelloWorld");
test_eq!("helloWorld" => "HelloWorld");
}
#[test]
fn test_camelcase() {
const TYPE: &str = "camelCase";
test_eq!("HelloWorld" => "helloWorld");
test_eq!("Hello_World" => "helloWorld");
test_eq!("Hello-World" => "helloWorld");
test_eq!("helloWorld" => "helloWorld");
}
#[test]
fn test_snakecase() {
const TYPE: &str = "snake_case";
test_eq!("HelloWorld" => "hello_world");
test_eq!("Hello_World" => "hello_world");
test_eq!("Hello-World" => "hello_world");
test_eq!("helloWorld" => "hello_world");
}
#[test]
fn test_screaming_snakecase() {
const TYPE: &str = "SCREAMING_SNAKE_CASE";
test_eq!("HelloWorld" => "HELLO_WORLD");
test_eq!("Hello_World" => "HELLO_WORLD");
test_eq!("Hello-World" => "HELLO_WORLD");
test_eq!("helloWorld" => "HELLO_WORLD");
}
#[test]
fn test_kebabcase() {
const TYPE: &str = "kebab-case";
test_eq!("HelloWorld" => "hello-world");
test_eq!("Hello_World" => "hello-world");
test_eq!("Hello-World" => "hello-world");
test_eq!("helloWorld" => "hello-world");
}
#[test]
fn test_screaming_kebabcase() {
const TYPE: &str = "SCREAMING-KEBAB-CASE";
test_eq!("HelloWorld" => "HELLO-WORLD");
test_eq!("Hello_World" => "HELLO-WORLD");
test_eq!("Hello-World" => "HELLO-WORLD");
test_eq!("helloWorld" => "HELLO-WORLD");
}
}

View file

@ -0,0 +1,20 @@
use std::iter::FromIterator;
pub(crate) struct Unzip<A, B>(pub A, pub B);
impl<A, B, T, U> FromIterator<(T, U)> for Unzip<A, B>
where
A: Default + Extend<T>,
B: Default + Extend<U>,
{
fn from_iter<I: IntoIterator<Item = (T, U)>>(iter: I) -> Self {
let (mut a, mut b): (A, B) = Default::default();
for (t, u) in iter {
a.extend([t]);
b.extend([u]);
}
Unzip(a, b)
}
}

View file

@ -0,0 +1,298 @@
//! Test for `teloxide-macros`
use teloxide_macros::BotCommands;
// Import only trait _methods_, such that we can call `parse`, but we also test
// that proc macros work without the trait being imported.
use teloxide::utils::command::BotCommands as _;
#[test]
fn parse_command_with_args() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
enum DefaultCommands {
Start(String),
Help,
}
let data = "/start arg1 arg2";
let expected = DefaultCommands::Start("arg1 arg2".to_string());
let actual = DefaultCommands::parse(data, "").unwrap();
assert_eq!(actual, expected)
}
#[test]
fn parse_command_with_non_string_arg() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
enum DefaultCommands {
Start(i32),
Help,
}
let data = "/start -50";
let expected = DefaultCommands::Start("-50".parse().unwrap());
let actual = DefaultCommands::parse(data, "").unwrap();
assert_eq!(actual, expected)
}
#[test]
fn attribute_prefix() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
enum DefaultCommands {
#[command(prefix = "!")]
Start(String),
Help,
}
let data = "!start arg1 arg2";
let expected = DefaultCommands::Start("arg1 arg2".to_string());
let actual = DefaultCommands::parse(data, "").unwrap();
assert_eq!(actual, expected)
}
#[test]
fn many_attributes() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
enum DefaultCommands {
#[command(prefix = "!", description = "desc")]
Start,
Help,
}
assert_eq!(
DefaultCommands::Start,
DefaultCommands::parse("!start", "").unwrap()
);
assert_eq!(
DefaultCommands::descriptions().to_string(),
"!start — desc\n/help"
);
}
#[test]
fn global_attributes() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(
prefix = "!",
rename_rule = "lowercase",
description = "Bot commands"
)]
enum DefaultCommands {
#[command(prefix = "/")]
Start,
Help,
}
assert_eq!(
DefaultCommands::Start,
DefaultCommands::parse("/start", "MyNameBot").unwrap()
);
assert_eq!(
DefaultCommands::Help,
DefaultCommands::parse("!help", "MyNameBot").unwrap()
);
assert_eq!(
DefaultCommands::descriptions().to_string(),
"Bot commands\n\n/start\n!help"
);
}
#[test]
fn parse_command_with_bot_name() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
enum DefaultCommands {
#[command(prefix = "/")]
Start,
Help,
}
assert_eq!(
DefaultCommands::Start,
DefaultCommands::parse("/start@MyNameBot", "MyNameBot").unwrap()
);
}
#[test]
fn parse_with_split() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
#[command(parse_with = "split")]
enum DefaultCommands {
Start(u8, String),
Help,
}
assert_eq!(
DefaultCommands::Start(10, "hello".to_string()),
DefaultCommands::parse("/start 10 hello", "").unwrap()
);
}
#[test]
fn parse_with_split2() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
#[command(parse_with = "split", separator = "|")]
enum DefaultCommands {
Start(u8, String),
Help,
}
assert_eq!(
DefaultCommands::Start(10, "hello".to_string()),
DefaultCommands::parse("/start 10|hello", "").unwrap()
);
}
#[test]
fn parse_custom_parser() {
mod parser {
use teloxide::utils::command::ParseError;
pub fn custom_parse_function(
s: String,
) -> Result<(u8, String), ParseError> {
let vec = s.split_whitespace().collect::<Vec<_>>();
let (left, right) = match vec.as_slice() {
[l, r] => (l, r),
_ => {
return Err(ParseError::IncorrectFormat(
"might be 2 arguments!".into(),
))
}
};
left.parse::<u8>().map(|res| (res, (*right).to_string())).map_err(
|_| {
ParseError::Custom(
"First argument must be a integer!".to_owned().into(),
)
},
)
}
}
use parser::custom_parse_function;
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
enum DefaultCommands {
#[command(parse_with = custom_parse_function)]
Start(u8, String),
// Test <https://github.com/teloxide/teloxide/issues/668>.
#[command(parse_with = parser::custom_parse_function)]
TestPath(u8, String),
Help,
}
assert_eq!(
DefaultCommands::Start(10, "hello".to_string()),
DefaultCommands::parse("/start 10 hello", "").unwrap()
);
assert_eq!(
DefaultCommands::TestPath(10, "hello".to_string()),
DefaultCommands::parse("/testpath 10 hello", "").unwrap()
);
}
#[test]
fn parse_named_fields() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
#[command(parse_with = "split")]
enum DefaultCommands {
Start { num: u8, data: String },
Help,
}
assert_eq!(
DefaultCommands::Start { num: 10, data: "hello".to_string() },
DefaultCommands::parse("/start 10 hello", "").unwrap()
);
}
#[test]
fn descriptions_off() {
#[derive(BotCommands, Debug, PartialEq)]
#[command(rename_rule = "lowercase")]
enum DefaultCommands {
#[command(description = "off")]
Start,
Help,
}
assert_eq!(DefaultCommands::descriptions().to_string(), "/help".to_owned());
}
#[test]
fn rename_rules() {
#[derive(BotCommands, Debug, PartialEq)]
enum DefaultCommands {
#[command(rename_rule = "lowercase")]
AaaAaa,
#[command(rename_rule = "UPPERCASE")]
BbbBbb,
#[command(rename_rule = "PascalCase")]
CccCcc,
#[command(rename_rule = "camelCase")]
DddDdd,
#[command(rename_rule = "snake_case")]
EeeEee,
#[command(rename_rule = "SCREAMING_SNAKE_CASE")]
FffFff,
#[command(rename_rule = "kebab-case")]
GggGgg,
#[command(rename_rule = "SCREAMING-KEBAB-CASE")]
HhhHhh,
#[command(rename = "Bar")]
Foo,
}
assert_eq!(
DefaultCommands::AaaAaa,
DefaultCommands::parse("/aaaaaa", "").unwrap()
);
assert_eq!(
DefaultCommands::BbbBbb,
DefaultCommands::parse("/BBBBBB", "").unwrap()
);
assert_eq!(
DefaultCommands::CccCcc,
DefaultCommands::parse("/CccCcc", "").unwrap()
);
assert_eq!(
DefaultCommands::DddDdd,
DefaultCommands::parse("/dddDdd", "").unwrap()
);
assert_eq!(
DefaultCommands::EeeEee,
DefaultCommands::parse("/eee_eee", "").unwrap()
);
assert_eq!(
DefaultCommands::FffFff,
DefaultCommands::parse("/FFF_FFF", "").unwrap()
);
assert_eq!(
DefaultCommands::GggGgg,
DefaultCommands::parse("/ggg-ggg", "").unwrap()
);
assert_eq!(
DefaultCommands::HhhHhh,
DefaultCommands::parse("/HHH-HHH", "").unwrap()
);
assert_eq!(
DefaultCommands::Foo,
DefaultCommands::parse("/Bar", "").unwrap()
);
assert_eq!(
"/aaaaaa\n/BBBBBB\n/CccCcc\n/dddDdd\n/eee_eee\n/FFF_FFF\n/ggg-ggg\n/\
HHH-HHH\n/Bar",
DefaultCommands::descriptions().to_string()
);
}