diff --git a/crates/teloxide-macros/src/attr.rs b/crates/teloxide-macros/src/attr.rs
index a44cb2d4..64be81a0 100644
--- a/crates/teloxide-macros/src/attr.rs
+++ b/crates/teloxide-macros/src/attr.rs
@@ -1,10 +1,10 @@
use crate::{error::compile_error_at, Result};
-use proc_macro2::Span;
+use proc_macro2::{Delimiter, Span};
use syn::{
- parse::{Parse, ParseBuffer, ParseStream},
+ parse::{Parse, ParseStream, Parser},
spanned::Spanned,
- Attribute, Ident, Lit, LitStr, Path, Token,
+ Attribute, Ident, Lit, Path, Token,
};
pub(crate) fn fold_attrs(
@@ -17,31 +17,38 @@ pub(crate) fn fold_attrs(
attrs
.filter(filter)
.flat_map(|attribute| {
- // FIXME: don't allocate here
- if crate::command_attr::is_doc_comment(&attribute) {
- vec![parse(Attr {
- key: Ident::new("description", Span::call_site()),
- value: AttrValue::Lit(
- LitStr::new(
- &crate::command_attr::parse_doc_comment(&attribute)
- .expect("it is doc comment"),
- Span::call_site(),
- )
- .into(),
- ),
- })]
- } else {
- match attribute.parse_args_with(|input: &ParseBuffer| {
- input.parse_terminated::<_, Token![,]>(Attr::parse)
- }) {
- Ok(ok) => ok.into_iter().map(&parse).collect(),
- Err(err) => vec![Err(err.into())],
- }
+ let Some(key) = attribute.path.get_ident().cloned()
+ else { return vec![Err(compile_error_at("expected an ident", attribute.path.span()))] };
+
+ match (|input: ParseStream<'_>| Attrs::parse_with_key(input, key))
+ .parse(attribute.tokens.into())
+ {
+ Ok(ok) => ok.0.into_iter().map(&parse).collect(),
+ Err(err) => vec![Err(err.into())],
}
})
- .try_fold(init, |acc, r| r.and_then(|r| f(acc, r)))
+ .try_fold(init, |acc, r| f(acc, r?))
}
+/// A helper to parse a set of attributes.
+///
+/// For example:
+/// ```text
+/// #[blahblah(key = "puff", value = 12, nope, inner(what = some::path))]
+/// ```
+///
+/// The code above will produce
+/// ```test
+/// [
+/// Attr { key: [key, blahblah], value: "puff" },
+/// Attr { key: [value, blahblah], value: 12 },
+/// Attr { key: [nope, blahblah], value: none },
+/// Attr { key: [what, inner, blahblah], value: some::path },
+/// ]
+/// ```
+#[derive(Default, Debug)]
+struct Attrs(Vec);
+
/// An attribute key-value pair.
///
/// For example:
@@ -49,8 +56,17 @@ pub(crate) fn fold_attrs(
/// #[blahblah(key = "puff", value = 12, nope)]
/// ^^^^^^^^^^^^ ^^^^^^^^^^ ^^^^
/// ```
+#[derive(Debug)]
pub(crate) struct Attr {
- pub key: Ident,
+ /// The key captures the full "path" in the reverse order, for example here:
+ ///
+ /// ```text
+ /// #[blahblah(key = "puff")]
+ /// ^^^^^^^^^^^^
+ /// ```
+ ///
+ /// The `key` will be `[key, blahblah]`. See [Attrs] for more examples.
+ pub key: Vec,
pub value: AttrValue,
}
@@ -61,16 +77,53 @@ pub(crate) struct Attr {
/// #[blahblah(key = "puff", value = 12, nope)]
/// ^^^^^^ ^^ ^-- (None pseudo-value)
/// ```
+#[derive(Debug)]
pub(crate) enum AttrValue {
Path(Path),
Lit(Lit),
None(Span),
}
-impl Parse for Attr {
- fn parse(input: ParseStream) -> syn::Result {
+impl Parse for Attrs {
+ fn parse(input: ParseStream) -> syn::Result {
let key = input.parse::()?;
+ Attrs::parse_with_key(input, key)
+ }
+}
+
+impl Attrs {
+ fn parse_with_key(input: ParseStream, key: Ident) -> syn::Result {
+ // Parse an attribute group
+ let attrs = input.step(|cursor| {
+ if let Some((group, _sp, next_cursor)) = cursor.group(Delimiter::Parenthesis) {
+ if !next_cursor.eof() {
+ return Err(syn::Error::new(next_cursor.span(), "unexpected tokens"));
+ }
+
+ let mut attrs =
+ (|input: ParseStream<'_>| input.parse_terminated::<_, Token![,]>(Attrs::parse))
+ .parse(group.token_stream().into())?
+ .into_iter()
+ .reduce(|mut l, r| {
+ l.0.extend(r.0);
+ l
+ })
+ .unwrap_or_default();
+
+ attrs.0.iter_mut().for_each(|attr| attr.key.push(key.clone()));
+
+ Ok((Some(attrs), next_cursor))
+ } else {
+ Ok((None, *cursor))
+ }
+ })?;
+
+ if let Some(attrs) = attrs {
+ return Ok(attrs);
+ }
+
+ // Parse a single attribute
let value = match input.peek(Token![=]) {
true => {
input.parse::()?;
@@ -79,13 +132,18 @@ impl Parse for Attr {
false => AttrValue::None(input.span()),
};
- Ok(Self { key, value })
+ Ok(Attrs(vec![Attr { key: vec![key], value }]))
}
}
impl Attr {
pub(crate) fn span(&self) -> Span {
- self.key.span().join(self.value.span()).unwrap_or_else(|| self.key.span())
+ self.key().span().join(self.value.span()).unwrap_or_else(|| self.key().span())
+ }
+
+ fn key(&self) -> &Ident {
+ // It's an invariant of the type that `self.key` is non-empty
+ self.key.first().unwrap()
}
}
diff --git a/crates/teloxide-macros/src/command_attr.rs b/crates/teloxide-macros/src/command_attr.rs
index e34b6c27..421de9ca 100644
--- a/crates/teloxide-macros/src/command_attr.rs
+++ b/crates/teloxide-macros/src/command_attr.rs
@@ -111,22 +111,63 @@ impl CommandAttr {
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))?)
+ 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(),
+ ));
+ }
+
+ // FIXME(awiteb): flag here that this is a doc comment
+ Description(value.expect_string()?)
}
- "rename" => Rename(value.expect_string()?),
- "parse_with" => ParseWith(ParserType::parse(value)?),
- "separator" => Separator(value.expect_string()?),
- "hide" => value.expect_none("hide").map(|_| Hide)?,
+
+ "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()?),
+ "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()?),
+ "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(),
+ ))
+ }
+ }
+ }
+
_ => {
return Err(compile_error_at(
- "unexpected attribute name (expected one of `prefix`, `description`, \
- `rename`, `parse_with`, `separator` and `hide`",
- key.span(),
+ "unexpected attribute (expected `command` or `doc`)",
+ outermost_key.span(),
))
}
};
@@ -148,14 +189,3 @@ pub(crate) fn is_doc_comment(a: &Attribute) -> bool {
_ => false,
}
}
-
-pub(crate) fn parse_doc_comment(attr: &Attribute) -> Option {
- if is_doc_comment(attr) {
- if let syn::Meta::NameValue(syn::MetaNameValue { lit: syn::Lit::Str(s), .. }) =
- attr.parse_meta().ok()?
- {
- return Some(s.value().trim().to_owned());
- }
- }
- None
-}