mattro/
macro_attr.rs

1use std::fmt::{Display, Formatter, Write};
2use std::ops::Index;
3use std::slice::SliceIndex;
4
5use syn::{
6    AttrStyle, Attribute, AttributeArgs, Lit
7};
8
9use crate::{NameValue, NameValueAttribute, NameValueError};
10use crate::visitor::{AttributeArgsVisitor, join_path_to_string};
11
12/// Represents a macro attribute and its arguments like:
13///
14/// `#[attribute(key="value")]`
15#[derive(Debug, Clone, Eq, PartialEq, Hash)]
16pub struct MacroAttribute {
17    pub(crate) path: String,
18    pub(crate) args: Vec<MetaItem>,
19    pub(crate) style: Option<AttrStyle>,
20}
21
22impl MacroAttribute {
23    /// Constructs a new `MacroAttribute` from an `Attribute`.
24    pub fn new(attribute: Attribute) -> syn::Result<Self> {
25        let path = join_path_to_string(&attribute.path);
26        let attr_args = get_attribute_args(&attribute)?;
27        let args = AttributeArgsVisitor::visit(attr_args);
28        let style = Some(attribute.style);
29
30        Ok(MacroAttribute { path, args, style })
31    }
32
33    /// Constructs a `MacroAttribute` from an `AttributeArgs`.
34    pub fn from_attribute_args(
35        path: &str,
36        attribute_args: AttributeArgs,
37        style: AttrStyle,
38    ) -> Self {
39        let args = AttributeArgsVisitor::visit(attribute_args);
40        MacroAttribute {
41            path: path.to_string(),
42            args,
43            style: Some(style),
44        }
45    }
46
47    /// Returns the `path` of an attribute.
48    ///
49    /// For `#[attribute(name="value")]` the path is `"attribute"`.
50    pub fn path(&self) -> &str {
51        self.path.as_str()
52    }
53
54    /// Returns the style of this attribute: outer or inner.
55    pub fn style(&self) -> Option<&AttrStyle> {
56        self.style.as_ref()
57    }
58
59    /// Returns the arguments of the attribute.
60    ///
61    /// For `#[attribute(name="value", number=10)]` the arguments are `"name=value"` and `"number=10"`.
62    pub fn args(&self) -> &[MetaItem] {
63        self.args.as_slice()
64    }
65
66    /// Returns the number of arguments in this attribute.
67    pub fn len(&self) -> usize {
68        self.args.len()
69    }
70
71    /// Returns `true` is this macro attribute have no arguments.
72    pub fn is_empty(&self) -> bool {
73        self.args.is_empty()
74    }
75
76    /// Returns the `MetaItem` in the given index, or `None` if not found.
77    pub fn get(&self, index: usize) -> Option<&MetaItem> {
78        self.args.get(index)
79    }
80
81    /// Returns an iterator over the arguments in this attribute.
82    pub fn iter(&self) -> impl Iterator<Item = &MetaItem> {
83        self.args.iter()
84    }
85
86    /// Converts this macro attribute into a name-value attribute.
87    pub fn into_name_values(self) -> Result<NameValueAttribute, NameValueError> {
88        NameValueAttribute::new(self.path.as_str(), self.args, self.style.unwrap())
89    }
90
91    /// Converts this macro attribute into a list of its arguments.
92    pub fn into_inner(self) -> Vec<MetaItem> {
93        self.args
94    }
95}
96
97impl<I: SliceIndex<[MetaItem]>> Index<I> for MacroAttribute {
98    type Output = I::Output;
99
100    fn index(&self, index: I) -> &Self::Output {
101        self.args.index(index)
102    }
103}
104
105impl<'a> IntoIterator for &'a MacroAttribute {
106    type Item = &'a MetaItem;
107    type IntoIter = std::slice::Iter<'a, MetaItem>;
108
109    fn into_iter(self) -> Self::IntoIter {
110        self.args.iter()
111    }
112}
113
114impl IntoIterator for MacroAttribute {
115    type Item = MetaItem;
116    type IntoIter = std::vec::IntoIter<MetaItem>;
117
118    fn into_iter(self) -> Self::IntoIter {
119        self.args.into_iter()
120    }
121}
122
123impl Display for MacroAttribute {
124    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
125        write!(
126            f,
127            "{}",
128            attribute_to_string(self.style.as_ref(), self.path(), self.args.as_slice())
129        )
130    }
131}
132
133/// Represents the data in an attribute.
134#[derive(Debug, Clone, Eq, PartialEq, Hash)]
135pub enum MetaItem {
136    /// A path like: `#[attribute]`
137    Path(String),
138    /// A literal like: `#[attribute("hello world")]`
139    Literal(Lit),
140    /// A key-value like: `#[attribute(key="value")]` or `#[attribute(array=1,2,3,4)]`
141    NameValue(NameValue),
142    /// Nested data like: `#[attribute(inner("hello"))]`
143    Nested(MacroAttribute),
144}
145
146impl MetaItem {
147    /// Returns `true` if this meta item is a path like: `#[attribute]`.
148    pub fn is_path(&self) -> bool {
149        matches!(self, MetaItem::Path(_))
150    }
151
152    /// Returns `true` if this meta item is a literal like: `#[attribute("hola mundo")]`.
153    pub fn is_literal(&self) -> bool {
154        matches!(self, MetaItem::Literal(_))
155    }
156
157    /// Returns `true` if this meta item is a name-value pair like: `#[attribute(name="value")]`.
158    pub fn is_name_value(&self) -> bool {
159        matches!(self, MetaItem::NameValue(_))
160    }
161
162    /// Returns `true` if this meta item is a nested attribute like: `#[attribute(inner("hello"))]`.
163    pub fn is_nested(&self) -> bool {
164        matches!(self, MetaItem::Nested(_))
165    }
166
167    /// Converts this meta item into a `String` or `None` if is not a path.
168    pub fn into_path(self) -> Option<String> {
169        match self {
170            MetaItem::Path(x) => Some(x),
171            _ => None,
172        }
173    }
174
175    /// Converts this meta item into a `Lit` or `None` if is not a literal.
176    pub fn into_literal(self) -> Option<Lit> {
177        match self {
178            MetaItem::Literal(x) => Some(x),
179            _ => None,
180        }
181    }
182
183    /// Converts this meta item into a `NameValue` or `None` if is not a name-value pair.
184    pub fn into_name_value(self) -> Option<NameValue> {
185        match self {
186            MetaItem::NameValue(x) => Some(x),
187            _ => None,
188        }
189    }
190
191    /// Converts this meta item into a its inner `MacroAttribute` or `None` if is not a nested attribute.
192    pub fn into_nested(self) -> Option<MacroAttribute> {
193        match self {
194            MetaItem::Nested(x) => Some(x),
195            _ => None,
196        }
197    }
198
199    /// Returns a reference to this meta item as a `&str` or `None` if is not a path.
200    pub fn as_path(&self) -> Option<&str> {
201        match self {
202            MetaItem::Path(x) => Some(x.as_str()),
203            _ => None,
204        }
205    }
206
207    /// Returns a reference to this meta item as a `Lit` or `None` if is not a literal.
208    pub fn as_literal(&self) -> Option<&Lit> {
209        match self {
210            MetaItem::Literal(x) => Some(x),
211            _ => None,
212        }
213    }
214
215    /// Returns a reference to this meta item as a `NameValue` or `None` if is not a name-value pair.
216    pub fn as_name_value(&self) -> Option<&NameValue> {
217        match self {
218            MetaItem::NameValue(x) => Some(x),
219            _ => None,
220        }
221    }
222
223    /// Returns a reference to this meta item as a nested macro attribute or `None` if is not a macro attribute.
224    pub fn as_nested(&self) -> Option<&MacroAttribute> {
225        match self {
226            MetaItem::Nested(x) => Some(x),
227            _ => None,
228        }
229    }
230}
231
232impl Display for MetaItem {
233    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
234        match self {
235            MetaItem::Path(s) => write!(f, "{}", s),
236            MetaItem::Literal(lit) => display_lit(f, lit),
237            MetaItem::NameValue(name_value) => write!(f, "{}", name_value),
238            MetaItem::Nested(nested) => {
239                assert!(
240                    nested.style.is_none(),
241                    "Nested macro attributes cannot have an style"
242                );
243                write!(
244                    f,
245                    "{}",
246                    attribute_to_string(None, nested.path(), nested.args.as_slice())
247                )
248            }
249        }
250    }
251}
252
253fn get_attribute_args(attr: &Attribute) -> syn::Result<AttributeArgs> {
254    // Get the token tree of the attribute.
255    let mut token_tree = attr.tokens.clone().into_iter();
256
257    // Skips the first parentheses of the attribute `(` and `)`
258    if let Some(proc_macro2::TokenTree::Group(group)) = token_tree.next() {
259        use syn::parse_macro_input::ParseMacroInput;
260        let tokens = group.stream();
261        syn::parse::Parser::parse2(AttributeArgs::parse, tokens)
262    } else {
263        // If the attribute is just #[attribute()] or #[attribute]
264        Ok(AttributeArgs::new())
265    }
266}
267
268#[doc(hidden)]
269pub fn lit_to_string(lit: &Lit) -> String {
270    match lit {
271        Lit::Str(s) => s.value(),
272        Lit::ByteStr(s) => unsafe { String::from_utf8_unchecked(s.value()) },
273        Lit::Byte(s) => s.value().to_string(),
274        Lit::Char(s) => s.value().to_string(),
275        Lit::Int(s) => s.to_string(),
276        Lit::Float(s) => s.to_string(),
277        Lit::Bool(s) => s.value.to_string(),
278        Lit::Verbatim(s) => s.to_string(),
279    }
280}
281
282#[doc(hidden)]
283pub fn display_lit<W: Write>(f: &mut W, lit: &Lit) -> std::fmt::Result {
284    match lit {
285        Lit::Str(s) => write!(f, "\"{}\"{}", s.value(), s.suffix()),
286        Lit::ByteStr(s) => write!(
287            f,
288            "b{:?}{}",
289            unsafe { String::from_utf8_unchecked(s.value()) },
290            s.suffix()
291        ),
292        Lit::Byte(s) => write!(
293            f,
294            "b\'{}\'{}",
295            std::char::from_u32(s.value() as u32).unwrap(),
296            s.suffix()
297        ),
298        Lit::Char(s) => write!(f, "\'{}\'{}", s.value(), s.suffix()),
299        Lit::Int(s) => write!(f, "{}{}", s.base10_digits(), s.suffix()),
300        Lit::Float(s) => write!(f, "{}{}", s.base10_digits(), s.suffix()),
301        Lit::Bool(s) => write!(f, "{}", s.value),
302        Lit::Verbatim(s) => write!(f, "{}", s),
303    }
304}
305
306#[doc(hidden)]
307pub fn attribute_to_string<'a, I>(style: Option<&AttrStyle>, path: &str, meta_items: I) -> String
308where
309    I: IntoIterator<Item = &'a MetaItem>,
310{
311    let meta = meta_items
312        .into_iter()
313        .map(|s| s.to_string())
314        .collect::<Vec<String>>();
315
316    if let Some(style) = style {
317        let style = if matches!(style, AttrStyle::Outer) {
318            "#".to_owned()
319        } else {
320            "#!".to_owned()
321        };
322
323        if meta.is_empty() {
324            format!("{}[{}]", style, path)
325        } else {
326            format!("{}[{}({})]", style, path, meta.join(", "))
327        }
328    } else {
329        format!("{}({})", path, meta.join(", "))
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use proc_macro2::TokenStream;
336    use quote::*;
337    use syn::parse_quote::ParseQuote;
338
339    use super::*;
340
341    fn parse_attr(tokens: TokenStream) -> Attribute {
342        syn::parse::Parser::parse2(Attribute::parse, tokens).expect("invalid attribute")
343    }
344
345    #[test]
346    fn new_macro_attr_test() {
347        let tokens = quote! { #[person(name="Kaori", age=20, job(salary=200.0))] };
348        let attr = MacroAttribute::new(parse_attr(tokens)).unwrap();
349
350        assert_eq!(attr.path, "person".to_owned());
351        assert_eq!(attr.len(), 3);
352        assert!(attr[0].is_name_value());
353        assert!(attr[1].is_name_value());
354        assert!(attr[2].is_nested());
355    }
356
357    #[test]
358    fn path_and_nested_test() {
359        let tokens = quote! { #[attribute(path, nested())] };
360        let attr = MacroAttribute::new(parse_attr(tokens)).unwrap();
361
362        assert_eq!(attr.path, "attribute".to_owned());
363        assert_eq!(attr.len(), 2);
364        assert!(attr[0].is_path());
365        assert!(attr[1].is_nested());
366    }
367
368    #[test]
369    fn to_string_test() {
370        let tokens = quote! {
371            #[attribute(
372                path,
373                nested(name="Alan"),
374                empty(),
375                string="string",
376                byte_str= b"byte_string",
377                int=100usize,
378                float=0.5,
379                byte=b'a',
380                boolean=true,
381                character='z',
382            )]
383        };
384        let attr = MacroAttribute::new(parse_attr(tokens)).unwrap();
385
386        assert_eq!(
387            attr.to_string().as_str(),
388            "#[attribute(\
389                path, \
390                nested(name=\"Alan\"), \
391                empty(), \
392                string=\"string\", \
393                byte_str=b\"byte_string\", \
394                int=100usize, \
395                float=0.5, \
396                byte=b'a', \
397                boolean=true, \
398                character='z'\
399            )]"
400        )
401    }
402
403    #[test]
404    fn invalid_attribute_test() {
405        let tokens = quote! { #[attribute(let x = 10)] };
406        let attr = parse_attr(tokens);
407        let attribute = MacroAttribute::new(attr);
408        assert!(attribute.is_err());
409    }
410}