Skip to main content

tusks_lib/parsing/util/
get_attribute_value.rs

1use syn::{Expr, Lit, Meta};
2
3use crate::parsing::util::attr::HasAttributes;
4
5/// Public trait for attribute value extraction
6pub trait AttributeValue {
7    fn get_attribute_value(&self, attr_name: &str, key: &str) -> Option<String>;
8
9    fn get_attribute_bool(&self, attr_name: &str, key: &str) -> bool {
10        self.get_attribute_value(attr_name, key)
11            .map(|v| v == "true")
12            .unwrap_or(false)
13    }
14}
15
16/* -------------------------------------------------------
17 * Generic AttributeValue implementation
18 * -------------------------------------------------------*/
19impl<T: HasAttributes> AttributeValue for T {
20    fn get_attribute_value(&self, attr_name: &str, key: &str) -> Option<String> {
21        // Find the matching attribute
22        let attr = self.attrs().iter().find(|attr| {
23            attr.path().segments.last()
24                .map(|seg| seg.ident == attr_name)
25                .unwrap_or(false)
26        })?;
27
28        // Parse the attribute meta
29        let meta = attr.meta.clone();
30        
31        match meta {
32            // #[attr(key1, key2 = value, ...)]
33            Meta::List(list) => {
34                // Parse tokens manually
35                let tokens = list.tokens.clone();
36                let parser = |input: syn::parse::ParseStream| {
37                    syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated(input)
38                };
39                let parsed = syn::parse::Parser::parse2(parser, tokens);
40                
41                if let Ok(nested) = parsed {
42                    for meta in nested {
43                        match meta {
44                            // Case 1: #[attr(key)] -> key exists as flag
45                            Meta::Path(path) => {
46                                if path.segments.last()?.ident == key {
47                                    return Some("true".to_string());
48                                }
49                            }
50                            // Case 2: #[attr(key = value)]
51                            Meta::NameValue(nv) => {
52                                if nv.path.segments.last()?.ident == key {
53                                    return Some(extract_value(&nv.value));
54                                }
55                            }
56                            // Case 3: #[attr(key(...))] - nested
57                            Meta::List(inner_list) => {
58                                if inner_list.path.segments.last()?.ident == key {
59                                    // Return the inner tokens as string
60                                    return Some(inner_list.tokens.to_string());
61                                }
62                            }
63                        }
64                    }
65                }
66            }
67            // #[attr = value] - direct name-value
68            Meta::NameValue(nv) => {
69                if key == attr_name {
70                    return Some(extract_value(&nv.value));
71                }
72            }
73            // #[attr] - just the path
74            Meta::Path(_) => {
75                if key == attr_name {
76                    return Some("true".to_string());
77                }
78            }
79        }
80        
81        None
82    }
83}
84
85/// Extract string representation from Expr
86fn extract_value(expr: &Expr) -> String {
87    match expr {
88        Expr::Lit(lit_expr) => {
89            match &lit_expr.lit {
90                Lit::Str(s) => s.value(),
91                Lit::Bool(b) => b.value.to_string(),
92                Lit::Int(i) => i.base10_digits().to_string(),
93                Lit::Float(f) => f.base10_digits().to_string(),
94                Lit::Char(c) => c.value().to_string(),
95                _ => quote::quote!(#expr).to_string(),
96            }
97        }
98        _ => quote::quote!(#expr).to_string(),
99    }
100}
101
102/* -------------------------------------------------------
103 * Tests
104 * -------------------------------------------------------*/
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use syn::{parse_quote, ItemStruct};
109
110    #[test]
111    fn test_attribute_values() {
112        let item: ItemStruct = parse_quote! {
113            #[demo(flag1, flag2 = false, flag3 = "hello", flag4 = 42)]
114            struct Test;
115        };
116
117        assert_eq!(item.get_attribute_value("demo", "flag1"), Some("true".to_string()));
118        assert_eq!(item.get_attribute_value("demo", "flag2"), Some("false".to_string()));
119        assert_eq!(item.get_attribute_value("demo", "flag3"), Some("hello".to_string()));
120        assert_eq!(item.get_attribute_value("demo", "flag4"), Some("42".to_string()));
121        assert_eq!(item.get_attribute_value("demo", "nonexistent"), None);
122    }
123
124    #[test]
125    fn test_numeric_values() {
126        let item: ItemStruct = parse_quote! {
127            #[config(num = 42, float = 3.14, ch = 'x')]
128            struct Test;
129        };
130
131        assert_eq!(item.get_attribute_value("config", "num"), Some("42".to_string()));
132        assert_eq!(item.get_attribute_value("config", "float"), Some("3.14".to_string()));
133        assert_eq!(item.get_attribute_value("config", "ch"), Some("x".to_string()));
134    }
135
136    #[test]
137    fn test_nested_attributes() {
138        let item: ItemStruct = parse_quote! {
139            #[serde(rename_all = "camelCase", deny_unknown_fields)]
140            struct Test;
141        };
142
143        assert_eq!(item.get_attribute_value("serde", "rename_all"), Some("camelCase".to_string()));
144        assert_eq!(item.get_attribute_value("serde", "deny_unknown_fields"), Some("true".to_string()));
145    }
146}