Skip to main content

zyn_core/ext/
field.rs

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