mirror of
https://github.com/teloxide/teloxide.git
synced 2025-01-03 17:52:12 +01:00
Merge teloxide-macros
into main repository
This commit is contained in:
commit
6ea8fb1670
16 changed files with 1537 additions and 0 deletions
72
crates/teloxide-macros/.github/workflows/rust.yml
vendored
Normal file
72
crates/teloxide-macros/.github/workflows/rust.yml
vendored
Normal 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
|
144
crates/teloxide-macros/CHANGELOG.md
Normal file
144
crates/teloxide-macros/CHANGELOG.md
Normal 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.
|
21
crates/teloxide-macros/Cargo.toml
Normal file
21
crates/teloxide-macros/Cargo.toml
Normal 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" }
|
21
crates/teloxide-macros/LICENSE
Normal file
21
crates/teloxide-macros/LICENSE
Normal 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.
|
7
crates/teloxide-macros/rustfmt.toml
Normal file
7
crates/teloxide-macros/rustfmt.toml
Normal 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
|
156
crates/teloxide-macros/src/attr.rs
Normal file
156
crates/teloxide-macros/src/attr.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
141
crates/teloxide-macros/src/bot_commands.rs
Normal file
141
crates/teloxide-macros/src/bot_commands.rs
Normal 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")),
|
||||||
|
}
|
||||||
|
}
|
65
crates/teloxide-macros/src/command.rs
Normal file
65
crates/teloxide-macros/src/command.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
129
crates/teloxide-macros/src/command_attr.rs
Normal file
129
crates/teloxide-macros/src/command_attr.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
50
crates/teloxide-macros/src/command_enum.rs
Normal file
50
crates/teloxide-macros/src/command_enum.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
54
crates/teloxide-macros/src/error.rs
Normal file
54
crates/teloxide-macros/src/error.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
164
crates/teloxide-macros/src/fields_parse.rs
Normal file
164
crates/teloxide-macros/src/fields_parse.rs
Normal 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
|
||||||
|
}
|
24
crates/teloxide-macros/src/lib.rs
Normal file
24
crates/teloxide-macros/src/lib.rs
Normal 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()
|
||||||
|
}
|
171
crates/teloxide-macros/src/rename_rules.rs
Normal file
171
crates/teloxide-macros/src/rename_rules.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
20
crates/teloxide-macros/src/unzip.rs
Normal file
20
crates/teloxide-macros/src/unzip.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
298
crates/teloxide-macros/tests/command.rs
Normal file
298
crates/teloxide-macros/tests/command.rs
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in a new issue