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}