mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 14:35:36 +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