Skip to main content

zyn_core/ext/
fields.rs

1//! Extension traits for `syn::Fields` and a unified [`FieldKey`] lookup type.
2//!
3//! [`FieldsExt`] adds variant predicates and field lookup to any type that
4//! contains fields — `syn::Fields`, `syn::ItemStruct`, `syn::DataStruct`,
5//! and `syn::Variant`.
6//!
7//! [`FieldKey`] unifies named and indexed field access so callers don't need
8//! to branch on field kind.
9//!
10//! # Examples
11//!
12//! ```ignore
13//! use zyn::ext::{FieldsExt, FieldKey};
14//!
15//! if fields.is_named() {
16//!     let id_field = fields.get(&"id".into());
17//! }
18//!
19//! let first = fields.get(&0usize.into());
20//! let exists = fields.exists(&"name".into());
21//! ```
22
23use proc_macro2::Span;
24use quote::ToTokens;
25use syn::Field;
26
27/// Identifies a struct field by name or position.
28///
29/// Construct from a `syn::Ident`, `syn::Index`, `&str`, or `usize`.
30/// Implements [`ToTokens`] and [`Display`](std::fmt::Display) for use in
31/// generated code and error messages.
32///
33/// # Examples
34///
35/// ```ignore
36/// use zyn::ext::FieldKey;
37///
38/// let named = "id".into();
39/// let indexed = 0usize.into();
40///
41/// assert!(named.is_named());
42/// assert!(indexed.is_index());
43/// ```
44pub enum FieldKey {
45    /// A named field identified by its `syn::Ident`.
46    Named(syn::Ident),
47    /// A positional field identified by its `syn::Index`.
48    Index(syn::Index),
49}
50
51impl FieldKey {
52    /// Returns `true` if this key refers to a named field.
53    pub fn is_named(&self) -> bool {
54        matches!(self, Self::Named(_))
55    }
56
57    /// Returns `true` if this key refers to a positional field.
58    pub fn is_index(&self) -> bool {
59        matches!(self, Self::Index(_))
60    }
61
62    /// Returns the identifier if this is a named key.
63    pub fn as_named(&self) -> Option<&syn::Ident> {
64        match self {
65            Self::Named(ident) => Some(ident),
66            _ => None,
67        }
68    }
69
70    /// Returns the index if this is a positional key.
71    pub fn as_index(&self) -> Option<&syn::Index> {
72        match self {
73            Self::Index(index) => Some(index),
74            _ => None,
75        }
76    }
77}
78
79impl From<syn::Ident> for FieldKey {
80    fn from(ident: syn::Ident) -> Self {
81        Self::Named(ident)
82    }
83}
84
85impl From<syn::Index> for FieldKey {
86    fn from(index: syn::Index) -> Self {
87        Self::Index(index)
88    }
89}
90
91impl From<usize> for FieldKey {
92    fn from(index: usize) -> Self {
93        Self::Index(syn::Index {
94            index: index as u32,
95            span: Span::call_site(),
96        })
97    }
98}
99
100impl From<&str> for FieldKey {
101    fn from(name: &str) -> Self {
102        Self::Named(syn::Ident::new(name, Span::call_site()))
103    }
104}
105
106impl std::fmt::Display for FieldKey {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        match self {
109            Self::Named(ident) => write!(f, "{}", ident),
110            Self::Index(index) => write!(f, "{}", index.index),
111        }
112    }
113}
114
115impl ToTokens for FieldKey {
116    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
117        match self {
118            Self::Named(ident) => ident.to_tokens(tokens),
119            Self::Index(index) => index.to_tokens(tokens),
120        }
121    }
122}
123
124/// Extension methods for types that contain `syn::Fields`.
125///
126/// Provides variant predicates (`is_named`, `is_unnamed`, `is_unit`),
127/// conversions (`as_named`, `as_unnamed`), and field lookup via [`FieldKey`].
128///
129/// Implemented for `syn::Fields`, `syn::ItemStruct`, `syn::DataStruct`,
130/// and `syn::Variant`.
131///
132/// # Examples
133///
134/// ```ignore
135/// use zyn::ext::{FieldsExt, FieldKey};
136///
137/// #[zyn::element]
138/// fn my_element(fields: zyn::Fields) -> zyn::TokenStream {
139///     if fields.is_named() {
140///         let f = fields.get(&"id".into());
141///     }
142///     // ...
143/// }
144/// ```
145pub trait FieldsExt {
146    /// Returns `true` if the fields are named (struct `{ a: T, b: U }`).
147    fn is_named(&self) -> bool;
148    /// Returns `true` if the fields are unnamed (tuple `(T, U)`).
149    fn is_unnamed(&self) -> bool;
150    /// Returns `true` if there are no fields (unit struct).
151    fn is_unit(&self) -> bool;
152    /// Returns the inner `syn::FieldsNamed` if the fields are named.
153    fn as_named(&self) -> Option<&syn::FieldsNamed>;
154    /// Returns the inner `syn::FieldsUnnamed` if the fields are unnamed.
155    fn as_unnamed(&self) -> Option<&syn::FieldsUnnamed>;
156    /// Returns `true` if a field matching the given [`FieldKey`] exists.
157    fn exists(&self, key: &FieldKey) -> bool;
158    /// Returns the first field matching the given [`FieldKey`], or `None`.
159    fn get(&self, key: &FieldKey) -> Option<&Field>;
160    /// Returns all fields as `(FieldKey, &Field)` pairs.
161    ///
162    /// Named fields yield [`FieldKey::Named`], unnamed fields yield
163    /// [`FieldKey::Index`], and unit fields yield an empty vec.
164    ///
165    /// # Examples
166    ///
167    /// ```ignore
168    /// use zyn::ext::{FieldsExt, FieldKey};
169    ///
170    /// for (key, field) in fields.keyed() {
171    ///     println!("{}: {:?}", key, field.ty);
172    /// }
173    /// ```
174    fn keyed(&self) -> impl Iterator<Item = (FieldKey, &Field)>;
175}
176
177impl FieldsExt for syn::Fields {
178    fn is_named(&self) -> bool {
179        matches!(self, Self::Named(_))
180    }
181
182    fn is_unnamed(&self) -> bool {
183        matches!(self, Self::Unnamed(_))
184    }
185
186    fn is_unit(&self) -> bool {
187        matches!(self, Self::Unit)
188    }
189
190    fn as_named(&self) -> Option<&syn::FieldsNamed> {
191        match self {
192            Self::Named(f) => Some(f),
193            _ => None,
194        }
195    }
196
197    fn as_unnamed(&self) -> Option<&syn::FieldsUnnamed> {
198        match self {
199            Self::Unnamed(f) => Some(f),
200            _ => None,
201        }
202    }
203
204    fn exists(&self, key: &FieldKey) -> bool {
205        self.get(key).is_some()
206    }
207
208    fn keyed(&self) -> impl Iterator<Item = (FieldKey, &Field)> {
209        let pairs: Vec<_> = match self {
210            Self::Named(f) => f
211                .named
212                .iter()
213                .map(|field| {
214                    let ident = field.ident.clone().unwrap();
215                    (FieldKey::Named(ident), field)
216                })
217                .collect(),
218            Self::Unnamed(f) => f
219                .unnamed
220                .iter()
221                .enumerate()
222                .map(|(i, field)| (FieldKey::from(i), field))
223                .collect(),
224            Self::Unit => Vec::new(),
225        };
226
227        pairs.into_iter()
228    }
229
230    fn get(&self, key: &FieldKey) -> Option<&Field> {
231        match key {
232            FieldKey::Named(ident) => {
233                let named = self.as_named()?;
234                named.named.iter().find(|f| f.ident.as_ref() == Some(ident))
235            }
236            FieldKey::Index(index) => {
237                let mut iter: Box<dyn Iterator<Item = &Field>> = match self {
238                    Self::Named(f) => Box::new(f.named.iter()),
239                    Self::Unnamed(f) => Box::new(f.unnamed.iter()),
240                    Self::Unit => return None,
241                };
242                iter.nth(index.index as usize)
243            }
244        }
245    }
246}
247
248macro_rules! delegate_fields_ext {
249    ($ty:ty, $field:ident) => {
250        impl FieldsExt for $ty {
251            fn is_named(&self) -> bool {
252                self.$field.is_named()
253            }
254
255            fn is_unnamed(&self) -> bool {
256                self.$field.is_unnamed()
257            }
258
259            fn is_unit(&self) -> bool {
260                self.$field.is_unit()
261            }
262
263            fn as_named(&self) -> Option<&syn::FieldsNamed> {
264                self.$field.as_named()
265            }
266
267            fn as_unnamed(&self) -> Option<&syn::FieldsUnnamed> {
268                self.$field.as_unnamed()
269            }
270
271            fn exists(&self, key: &FieldKey) -> bool {
272                self.$field.exists(key)
273            }
274
275            fn keyed(&self) -> impl Iterator<Item = (FieldKey, &Field)> {
276                self.$field.keyed()
277            }
278
279            fn get(&self, key: &FieldKey) -> Option<&Field> {
280                self.$field.get(key)
281            }
282        }
283    };
284}
285
286delegate_fields_ext!(syn::ItemStruct, fields);
287delegate_fields_ext!(syn::DataStruct, fields);
288delegate_fields_ext!(syn::Variant, fields);
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    fn named_fields() -> syn::Fields {
295        let item: syn::ItemStruct = syn::parse_str("struct Foo { x: i32, y: String }").unwrap();
296        item.fields
297    }
298
299    fn unnamed_fields() -> syn::Fields {
300        let item: syn::ItemStruct = syn::parse_str("struct Foo(i32, String);").unwrap();
301        item.fields
302    }
303
304    fn unit_fields() -> syn::Fields {
305        let item: syn::ItemStruct = syn::parse_str("struct Foo;").unwrap();
306        item.fields
307    }
308
309    mod field_key {
310        use super::*;
311
312        #[test]
313        fn from_str() {
314            let key: FieldKey = "id".into();
315            assert!(key.is_named());
316            assert!(!key.is_index());
317        }
318
319        #[test]
320        fn from_usize() {
321            let key: FieldKey = 0usize.into();
322            assert!(key.is_index());
323            assert!(!key.is_named());
324        }
325
326        #[test]
327        fn as_named_some() {
328            let key: FieldKey = "id".into();
329            assert!(key.as_named().is_some());
330        }
331
332        #[test]
333        fn as_named_none() {
334            let key: FieldKey = 0usize.into();
335            assert!(key.as_named().is_none());
336        }
337
338        #[test]
339        fn as_index_some() {
340            let key: FieldKey = 0usize.into();
341            assert!(key.as_index().is_some());
342        }
343
344        #[test]
345        fn as_index_none() {
346            let key: FieldKey = "id".into();
347            assert!(key.as_index().is_none());
348        }
349
350        #[test]
351        fn display_named() {
352            let key: FieldKey = "id".into();
353            assert_eq!(key.to_string(), "id");
354        }
355
356        #[test]
357        fn display_index() {
358            let key: FieldKey = 3usize.into();
359            assert_eq!(key.to_string(), "3");
360        }
361    }
362
363    mod predicates {
364        use super::*;
365
366        #[test]
367        fn named_struct() {
368            let fields = named_fields();
369            assert!(fields.is_named());
370            assert!(!fields.is_unnamed());
371            assert!(!fields.is_unit());
372        }
373
374        #[test]
375        fn tuple_struct() {
376            let fields = unnamed_fields();
377            assert!(!fields.is_named());
378            assert!(fields.is_unnamed());
379            assert!(!fields.is_unit());
380        }
381
382        #[test]
383        fn unit_struct() {
384            let fields = unit_fields();
385            assert!(!fields.is_named());
386            assert!(!fields.is_unnamed());
387            assert!(fields.is_unit());
388        }
389    }
390
391    mod conversions {
392        use super::*;
393
394        #[test]
395        fn as_named_some() {
396            let fields = named_fields();
397            assert!(fields.as_named().is_some());
398        }
399
400        #[test]
401        fn as_named_none() {
402            let fields = unnamed_fields();
403            assert!(fields.as_named().is_none());
404        }
405
406        #[test]
407        fn as_unnamed_some() {
408            let fields = unnamed_fields();
409            assert!(fields.as_unnamed().is_some());
410        }
411
412        #[test]
413        fn as_unnamed_none() {
414            let fields = named_fields();
415            assert!(fields.as_unnamed().is_none());
416        }
417    }
418
419    mod get {
420        use super::*;
421
422        #[test]
423        fn by_name() {
424            let fields = named_fields();
425            let key: FieldKey = "x".into();
426            assert!(fields.get(&key).is_some());
427        }
428
429        #[test]
430        fn by_name_missing() {
431            let fields = named_fields();
432            let key: FieldKey = "z".into();
433            assert!(fields.get(&key).is_none());
434        }
435
436        #[test]
437        fn by_index_named() {
438            let fields = named_fields();
439            let key: FieldKey = 0usize.into();
440            assert!(fields.get(&key).is_some());
441        }
442
443        #[test]
444        fn by_index_unnamed() {
445            let fields = unnamed_fields();
446            let key: FieldKey = 1usize.into();
447            assert!(fields.get(&key).is_some());
448        }
449
450        #[test]
451        fn by_index_out_of_bounds() {
452            let fields = named_fields();
453            let key: FieldKey = 10usize.into();
454            assert!(fields.get(&key).is_none());
455        }
456
457        #[test]
458        fn on_unit() {
459            let fields = unit_fields();
460            let key: FieldKey = 0usize.into();
461            assert!(fields.get(&key).is_none());
462        }
463    }
464
465    mod exists {
466        use super::*;
467
468        #[test]
469        fn existing_field() {
470            let fields = named_fields();
471            let key: FieldKey = "x".into();
472            assert!(fields.exists(&key));
473        }
474
475        #[test]
476        fn missing_field() {
477            let fields = named_fields();
478            let key: FieldKey = "z".into();
479            assert!(!fields.exists(&key));
480        }
481    }
482
483    mod keyed {
484        use super::*;
485
486        #[test]
487        fn named_yields_named_keys() {
488            let fields = named_fields();
489            let pairs: Vec<_> = fields.keyed().collect();
490            assert_eq!(pairs.len(), 2);
491            assert!(pairs[0].0.is_named());
492            assert_eq!(pairs[0].0.to_string(), "x");
493            assert_eq!(pairs[1].0.to_string(), "y");
494        }
495
496        #[test]
497        fn unnamed_yields_index_keys() {
498            let fields = unnamed_fields();
499            let pairs: Vec<_> = fields.keyed().collect();
500            assert_eq!(pairs.len(), 2);
501            assert!(pairs[0].0.is_index());
502            assert_eq!(pairs[0].0.to_string(), "0");
503            assert_eq!(pairs[1].0.to_string(), "1");
504        }
505
506        #[test]
507        fn unit_yields_empty() {
508            let fields = unit_fields();
509            let pairs: Vec<_> = fields.keyed().collect();
510            assert!(pairs.is_empty());
511        }
512    }
513}