teloxide/crates/teloxide-macros/src/command_attr.rs

215 lines
7.3 KiB
Rust
Raw Normal View History

2022-08-28 13:07:19 +02:00
use crate::{
attr::{fold_attrs, Attr},
2022-08-28 13:07:19 +02:00
error::compile_error_at,
fields_parse::ParserType,
rename_rules::RenameRule,
Result,
};
2023-09-12 13:12:11 +02:00
use proc_macro::TokenStream;
use proc_macro2::Span;
2023-09-12 13:12:11 +02:00
use syn::{
parse::{ParseStream, Peek},
Attribute, Token,
};
2022-08-28 13:07:19 +02:00
2022-10-02 16:03:10 +02:00
/// All attributes that can be used for `derive(BotCommands)`
2022-08-28 13:07:19 +02:00
pub(crate) struct CommandAttrs {
2022-10-02 16:03:10 +02:00
pub prefix: Option<(String, Span)>,
/// The bool is true if the description contains a doc comment
pub description: Option<(String, bool, Span)>,
2022-10-02 16:03:10 +02:00
pub rename_rule: Option<(RenameRule, Span)>,
2022-10-02 16:13:54 +02:00
pub rename: Option<(String, Span)>,
2022-10-02 16:03:10 +02:00
pub parser: Option<(ParserType, Span)>,
pub separator: Option<(String, Span)>,
pub command_separator: Option<(String, Span)>,
pub hide: Option<((), Span)>,
2022-08-28 13:07:19 +02:00
}
2022-10-02 16:03:10 +02:00
/// 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 {
2022-08-28 13:07:19 +02:00
kind: CommandAttrKind,
sp: Span,
}
2022-10-02 16:03:10 +02:00
/// Kind of [`CommandAttr`].
enum CommandAttrKind {
2022-08-28 13:07:19 +02:00
Prefix(String),
/// Description of the command. and if its doc comment or not
Description(String, bool),
2022-10-02 14:24:28 +02:00
RenameRule(RenameRule),
2022-10-02 16:13:54 +02:00
Rename(String),
2022-08-28 13:07:19 +02:00
ParseWith(ParserType),
Separator(String),
CommandSeparator(String),
Hide,
2022-08-28 13:07:19 +02:00
}
impl CommandAttrs {
pub fn from_attributes(attributes: &[Attribute]) -> Result<Self> {
use CommandAttrKind::*;
2022-08-28 13:07:19 +02:00
fold_attrs(
attributes,
|attr| is_command_attribute(attr) || is_doc_comment(attr),
2022-08-28 13:07:19 +02:00
CommandAttr::parse,
Self {
prefix: None,
description: None,
rename_rule: None,
2022-10-02 16:13:54 +02:00
rename: None,
2022-08-28 13:07:19 +02:00
parser: None,
separator: None,
command_separator: None,
hide: None,
2022-08-28 13:07:19 +02:00
},
|mut this, attr| {
2022-11-07 13:13:29 +01:00
fn insert<T>(opt: &mut Option<(T, Span)>, x: T, sp: Span) -> Result<()> {
2022-08-28 13:07:19 +02:00
match opt {
slot @ None => {
2022-10-02 16:03:10 +02:00
*slot = Some((x, sp));
2022-08-28 13:07:19 +02:00
Ok(())
}
2022-11-07 13:13:29 +01:00
Some(_) => Err(compile_error_at("duplicate attribute", sp)),
2022-08-28 13:07:19 +02:00
}
}
fn join_string(opt: &mut Option<(String, bool, Span)>, new_str: &str, sp: Span) {
match opt {
slot @ None => {
*slot = Some((new_str.to_owned(), false, sp));
}
Some((old_str, ..)) => {
*old_str = format!("{old_str}\n{new_str}");
}
}
}
2022-08-28 13:07:19 +02:00
match attr.kind {
Prefix(p) => insert(&mut this.prefix, p, attr.sp),
Description(d, is_doc) => {
join_string(
&mut this.description,
// Sometimes doc comments include a space before them, this removes it
d.strip_prefix(' ').unwrap_or(&d),
attr.sp,
);
if is_doc {
if let Some((_, is_doc, _)) = &mut this.description {
*is_doc = true;
}
}
Ok(())
}
2022-10-02 14:24:28 +02:00
RenameRule(r) => insert(&mut this.rename_rule, r, attr.sp),
2022-10-02 16:13:54 +02:00
Rename(r) => insert(&mut this.rename, r, attr.sp),
2022-08-28 13:07:19 +02:00
ParseWith(p) => insert(&mut this.parser, p, attr.sp),
Separator(s) => insert(&mut this.separator, s, attr.sp),
CommandSeparator(s) => insert(&mut this.command_separator, s, attr.sp),
Hide => insert(&mut this.hide, (), attr.sp),
2022-08-28 13:07:19 +02:00
}?;
Ok(this)
},
)
}
}
impl CommandAttr {
fn parse(attr: Attr) -> Result<Self> {
use CommandAttrKind::*;
let sp = attr.span();
let Attr { mut key, value } = attr;
let outermost_key = key.pop().unwrap(); // `Attr`'s invariants ensure `key.len() > 0`
let kind = match &*outermost_key.to_string() {
"doc" => {
if let Some(unexpected_key) = key.last() {
return Err(compile_error_at(
"`doc` can't have nested attributes",
unexpected_key.span(),
));
}
Description(value.expect_string()?, true)
}
"command" => {
let Some(attr) = key.pop()
else {
return Err(compile_error_at(
"expected an attribute name",
outermost_key.span(),
))
};
if let Some(unexpected_key) = key.last() {
return Err(compile_error_at(
&format!("{attr} can't have nested attributes"),
unexpected_key.span(),
));
}
match &*attr.to_string() {
"prefix" => Prefix(value.expect_string()?),
"description" => Description(value.expect_string()?, false),
"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()?),
"command_separator" => CommandSeparator(value.expect_string()?),
"hide" => value.expect_none("hide").map(|_| Hide)?,
_ => {
return Err(compile_error_at(
"unexpected attribute name (expected one of `prefix`, `description`, \
`rename`, `parse_with`, `separator` and `hide`",
attr.span(),
))
}
}
2022-11-07 13:13:29 +01:00
}
2022-08-28 13:07:19 +02:00
_ => {
return Err(compile_error_at(
"unexpected attribute (expected `command` or `doc`)",
outermost_key.span(),
2022-08-28 13:07:19 +02:00
))
}
};
Ok(Self { kind, sp })
}
}
fn is_command_attribute(a: &Attribute) -> bool {
2023-09-12 13:12:11 +02:00
matches!(a.path.get_ident(), Some(ident) if ident == "command")
2022-08-28 13:07:19 +02:00
}
2023-03-03 00:07:52 +01:00
2023-09-12 13:12:11 +02:00
fn is_doc_comment(a: &Attribute) -> bool {
matches!(a.path.get_ident(), Some(ident) if ident == "doc" && peek_at_token_stream(a.tokens.clone().into(), Token![=]))
}
fn peek_at_token_stream(s: TokenStream, p: impl Peek) -> bool {
// syn be fr challenge 2023 (impossible)
use syn::parse::Parser;
(|input: ParseStream<'_>| {
let r = input.peek(p);
_ = input.step(|_| Ok(((), syn::buffer::Cursor::empty())));
Ok(r)
})
.parse(s)
.unwrap()
}