1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! Utility functions
use crate::{prelude::*, Error};

/// Parse a tagged attribute. This is very helpful for implementing [`FromAttribute`].
///
/// A tagged attribute is an attribute in the form of `#[prefix(result)]`. This function will return `Some(result)` if the `prefix` matches.
///
/// The contents of the result can be either:
/// - `ParsedAttribute::Tagged(Ident)`, e.g. `#[serde(skip)]` will be `Tagged("skip")`
/// - `ParsedAttribute::Property(Ident, lit)`, e.g. `#[bincode(crate = "foo")]` will be `Property("crate", "foo")`
///
/// # Examples
/// ```no_run
/// # use virtue::prelude::*;
/// # use std::str::FromStr;
/// # fn parse_token_stream_group(input: &'static str) -> Group {
/// #     let token_stream: TokenStream = proc_macro2::TokenStream::from_str(input).unwrap().into();
/// #     match token_stream.into_iter().next() {
/// #         Some(TokenTree::Group(group)) => group,
/// #         _ => unreachable!(),
/// #     }
/// # }
/// use virtue::utils::{parse_tagged_attribute, ParsedAttribute};
///
/// // The attribute being parsed
/// let group: Group = parse_token_stream_group("#[prefix(result, foo = \"bar\")]");
///
/// let attributes = parse_tagged_attribute(&group, "prefix").unwrap().unwrap();
/// let mut iter = attributes.into_iter();
///
/// // The stream will contain the contents of the `prefix(...)`
/// match iter.next() {
///     Some(ParsedAttribute::Tag(i)) => {
///         assert_eq!(i.to_string(), String::from("result"));
///     },
///     x => panic!("Unexpected attribute: {:?}", x)
/// }
///  match iter.next() {
///     Some(ParsedAttribute::Property(key, val)) => {
///         assert_eq!(key.to_string(), String::from("foo"));
///         assert_eq!(val.to_string(), String::from("\"bar\""));
///     },
///     x => panic!("Unexpected attribute: {:?}", x)
/// }
///
/// ```
pub fn parse_tagged_attribute(group: &Group, prefix: &str) -> Result<Option<Vec<ParsedAttribute>>> {
    let stream = &mut group.stream().into_iter();
    if let Some(TokenTree::Ident(attribute_ident)) = stream.next() {
        #[allow(clippy::cmp_owned)] // clippy is wrong
        if attribute_ident.to_string() == prefix {
            if let Some(TokenTree::Group(group)) = stream.next() {
                let mut result = Vec::new();
                let mut stream = group.stream().into_iter().peekable();
                while let Some(token) = stream.next() {
                    match (token, stream.peek()) {
                        (TokenTree::Ident(key), Some(TokenTree::Punct(p)))
                            if p.as_char() == ',' =>
                        {
                            result.push(ParsedAttribute::Tag(key));
                            stream.next();
                        }
                        (TokenTree::Ident(key), None) => {
                            result.push(ParsedAttribute::Tag(key));
                            stream.next();
                        }
                        (TokenTree::Ident(key), Some(TokenTree::Punct(p)))
                            if p.as_char() == '=' =>
                        {
                            stream.next();
                            if let Some(TokenTree::Literal(lit)) = stream.next() {
                                result.push(ParsedAttribute::Property(key, lit));

                                match stream.next() {
                                    Some(TokenTree::Punct(p)) if p.as_char() == ',' => {}
                                    None => {}
                                    x => {
                                        return Err(Error::custom_at_opt_token("Expected `,`", x));
                                    }
                                }
                            }
                        }
                        (x, _) => {
                            return Err(Error::custom_at(
                                "Expected `key` or `key = \"val\"`",
                                x.span(),
                            ));
                        }
                    }
                }

                return Ok(Some(result));
            }
        }
    }
    Ok(None)
}

#[derive(Clone, Debug)]
#[non_exhaustive]
/// A parsed attribute. See [`parse_tagged_attribute`] for more information.
pub enum ParsedAttribute {
    /// A tag, created by parsing `#[prefix(foo)]`
    Tag(Ident),
    /// A property, created by parsing `#[prefix(foo = "bar")]`
    Property(Ident, Literal),
}

#[test]
fn test_parse_tagged_attribute() {
    let group: Group = match crate::token_stream("[prefix(result, foo = \"bar\", baz)]").next() {
        Some(TokenTree::Group(group)) => group,
        x => panic!("Unexpected token {:?}", x),
    };

    let attributes = parse_tagged_attribute(&group, "prefix").unwrap().unwrap();
    let mut iter = attributes.into_iter();

    // The stream will contain the contents of the `prefix(...)`
    match iter.next() {
        Some(ParsedAttribute::Tag(i)) => {
            assert_eq!(i.to_string(), String::from("result"));
        }
        x => panic!("Unexpected attribute: {:?}", x),
    }
    match iter.next() {
        Some(ParsedAttribute::Property(key, val)) => {
            assert_eq!(key.to_string(), String::from("foo"));
            assert_eq!(val.to_string(), String::from("\"bar\""));
        }
        x => panic!("Unexpected attribute: {:?}", x),
    }
    match iter.next() {
        Some(ParsedAttribute::Tag(i)) => {
            assert_eq!(i.to_string(), String::from("baz"));
        }
        x => panic!("Unexpected attribute: {:?}", x),
    }
}