Skip to main content

zyn_core/ext/
variant.rs

1//! Extension trait for `syn::Variant` metadata querying.
2//!
3//! [`VariantExt`] adds dot-path metadata navigation to enum variants.
4//!
5//! # Examples
6//!
7//! ```ignore
8//! use zyn::ext::VariantExt;
9//!
10//! // enum Foo {
11//! //     #[serde(rename = "bar")]
12//! //     A { x: i32 },
13//! // }
14//! let rename = variant.get("serde.rename"); // → Some(NameValue meta)
15//! ```
16
17use syn::Meta;
18
19use super::AttrExt;
20
21/// Extension methods for a single `syn::Variant`.
22///
23/// Provides dot-path metadata querying across the variant's attributes.
24/// The first path segment matches the attribute name, subsequent segments
25/// drill into nested metadata.
26///
27/// # Examples
28///
29/// ```ignore
30/// use zyn::ext::VariantExt;
31///
32/// // #[serde(rename = "bar")]
33/// // A { x: i32 },
34/// let meta = variant.get("serde.rename"); // → Some(NameValue meta)
35/// let serde = variant.get("serde");       // → Some(List meta)
36/// ```
37pub trait VariantExt {
38    /// Navigates into a variant's attributes using a dot-separated path.
39    ///
40    /// The first segment matches the attribute name, subsequent segments
41    /// drill into nested metadata using [`MetaPath`](crate::path::MetaPath) syntax.
42    ///
43    /// # Examples
44    ///
45    /// ```ignore
46    /// use zyn::ext::VariantExt;
47    ///
48    /// // #[serde(rename = "bar", skip)]
49    /// let rename = variant.get("serde.rename"); // → Some(NameValue)
50    /// let skip = variant.get("serde.skip");     // → Some(Path)
51    /// ```
52    fn get(&self, path: &str) -> Option<Meta>;
53}
54
55impl VariantExt for syn::Variant {
56    fn get(&self, path: &str) -> Option<Meta> {
57        let parsed = crate::path::MetaPath::parse(path).ok()?;
58        let first = parsed.first()?;
59
60        let attr_name = match first {
61            crate::path::Segment::Key(name) => name,
62            _ => return None,
63        };
64
65        let attr = self.attrs.iter().find(|a| a.is(attr_name))?;
66        let tail = parsed.tail();
67
68        if tail.is_empty() {
69            Some(attr.meta.clone())
70        } else {
71            attr.get(&tail.to_string())
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::ext::MetaExt;
80
81    fn parse_variant(input: &str) -> syn::Variant {
82        let item: syn::ItemEnum = syn::parse_str(input).unwrap();
83        item.variants.into_iter().next().unwrap()
84    }
85
86    mod get {
87        use super::*;
88
89        #[test]
90        fn single_segment() {
91            let v = parse_variant("enum Foo { #[serde(skip)] A }");
92            let meta = v.get("serde").unwrap();
93            assert!(meta.path().is_ident("serde"));
94        }
95
96        #[test]
97        fn dot_path() {
98            let v = parse_variant("enum Foo { #[serde(rename = \"bar\", skip)] A }");
99            let meta = v.get("serde.skip").unwrap();
100            assert!(meta.path().is_ident("skip"));
101        }
102
103        #[test]
104        fn name_value() {
105            let v = parse_variant("enum Foo { #[serde(rename = \"bar\")] A }");
106            let meta = v.get("serde.rename").unwrap();
107            assert!(meta.is_name_value());
108        }
109
110        #[test]
111        fn missing_attr() {
112            let v = parse_variant("enum Foo { A }");
113            assert!(v.get("serde").is_none());
114        }
115
116        #[test]
117        fn missing_nested() {
118            let v = parse_variant("enum Foo { #[serde(skip)] A }");
119            assert!(v.get("serde.rename").is_none());
120        }
121
122        #[test]
123        fn among_multiple_attrs() {
124            let v = parse_variant("enum Foo { #[doc = \"hi\"] #[serde(skip)] A }");
125            let meta = v.get("serde.skip").unwrap();
126            assert!(meta.path().is_ident("skip"));
127        }
128    }
129}