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}