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    /// Returns the span of the fields.
176    fn span(&self) -> proc_macro2::Span;
177}
178
179impl FieldsExt for syn::Fields {
180    fn is_named(&self) -> bool {
181        matches!(self, Self::Named(_))
182    }
183
184    fn is_unnamed(&self) -> bool {
185        matches!(self, Self::Unnamed(_))
186    }
187
188    fn is_unit(&self) -> bool {
189        matches!(self, Self::Unit)
190    }
191
192    fn as_named(&self) -> Option<&syn::FieldsNamed> {
193        match self {
194            Self::Named(f) => Some(f),
195            _ => None,
196        }
197    }
198
199    fn as_unnamed(&self) -> Option<&syn::FieldsUnnamed> {
200        match self {
201            Self::Unnamed(f) => Some(f),
202            _ => None,
203        }
204    }
205
206    fn exists(&self, key: &FieldKey) -> bool {
207        self.get(key).is_some()
208    }
209
210    fn keyed(&self) -> impl Iterator<Item = (FieldKey, &Field)> {
211        let pairs: Vec<_> = match self {
212            Self::Named(f) => f
213                .named
214                .iter()
215                .map(|field| {
216                    let ident = field.ident.clone().unwrap();
217                    (FieldKey::Named(ident), field)
218                })
219                .collect(),
220            Self::Unnamed(f) => f
221                .unnamed
222                .iter()
223                .enumerate()
224                .map(|(i, field)| (FieldKey::from(i), field))
225                .collect(),
226            Self::Unit => Vec::new(),
227        };
228
229        pairs.into_iter()
230    }
231
232    fn get(&self, key: &FieldKey) -> Option<&Field> {
233        match key {
234            FieldKey::Named(ident) => {
235                let named = self.as_named()?;
236                named.named.iter().find(|f| f.ident.as_ref() == Some(ident))
237            }
238            FieldKey::Index(index) => {
239                let mut iter: Box<dyn Iterator<Item = &Field>> = match self {
240                    Self::Named(f) => Box::new(f.named.iter()),
241                    Self::Unnamed(f) => Box::new(f.unnamed.iter()),
242                    Self::Unit => return None,
243                };
244                iter.nth(index.index as usize)
245            }
246        }
247    }
248
249    fn span(&self) -> proc_macro2::Span {
250        use syn::spanned::Spanned;
251        Spanned::span(self)
252    }
253}
254
255macro_rules! delegate_fields_ext {
256    ($ty:ty, $field:ident) => {
257        impl FieldsExt for $ty {
258            fn is_named(&self) -> bool {
259                self.$field.is_named()
260            }
261
262            fn is_unnamed(&self) -> bool {
263                self.$field.is_unnamed()
264            }
265
266            fn is_unit(&self) -> bool {
267                self.$field.is_unit()
268            }
269
270            fn as_named(&self) -> Option<&syn::FieldsNamed> {
271                self.$field.as_named()
272            }
273
274            fn as_unnamed(&self) -> Option<&syn::FieldsUnnamed> {
275                self.$field.as_unnamed()
276            }
277
278            fn exists(&self, key: &FieldKey) -> bool {
279                self.$field.exists(key)
280            }
281
282            fn keyed(&self) -> impl Iterator<Item = (FieldKey, &Field)> {
283                self.$field.keyed()
284            }
285
286            fn get(&self, key: &FieldKey) -> Option<&Field> {
287                self.$field.get(key)
288            }
289
290            fn span(&self) -> proc_macro2::Span {
291                use syn::spanned::Spanned;
292                Spanned::span(&self.$field)
293            }
294        }
295    };
296}
297
298delegate_fields_ext!(syn::ItemStruct, fields);
299delegate_fields_ext!(syn::DataStruct, fields);
300delegate_fields_ext!(syn::Variant, fields);
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    fn named_fields() -> syn::Fields {
307        let item: syn::ItemStruct = syn::parse_str("struct Foo { x: i32, y: String }").unwrap();
308        item.fields
309    }
310
311    fn unnamed_fields() -> syn::Fields {
312        let item: syn::ItemStruct = syn::parse_str("struct Foo(i32, String);").unwrap();
313        item.fields
314    }
315
316    fn unit_fields() -> syn::Fields {
317        let item: syn::ItemStruct = syn::parse_str("struct Foo;").unwrap();
318        item.fields
319    }
320
321    mod field_key {
322        use super::*;
323
324        #[test]
325        fn from_str() {
326            let key: FieldKey = "id".into();
327            assert!(key.is_named());
328            assert!(!key.is_index());
329        }
330
331        #[test]
332        fn from_usize() {
333            let key: FieldKey = 0usize.into();
334            assert!(key.is_index());
335            assert!(!key.is_named());
336        }
337
338        #[test]
339        fn as_named_some() {
340            let key: FieldKey = "id".into();
341            assert!(key.as_named().is_some());
342        }
343
344        #[test]
345        fn as_named_none() {
346            let key: FieldKey = 0usize.into();
347            assert!(key.as_named().is_none());
348        }
349
350        #[test]
351        fn as_index_some() {
352            let key: FieldKey = 0usize.into();
353            assert!(key.as_index().is_some());
354        }
355
356        #[test]
357        fn as_index_none() {
358            let key: FieldKey = "id".into();
359            assert!(key.as_index().is_none());
360        }
361
362        #[test]
363        fn display_named() {
364            let key: FieldKey = "id".into();
365            assert_eq!(key.to_string(), "id");
366        }
367
368        #[test]
369        fn display_index() {
370            let key: FieldKey = 3usize.into();
371            assert_eq!(key.to_string(), "3");
372        }
373    }
374
375    mod predicates {
376        use super::*;
377
378        #[test]
379        fn named_struct() {
380            let fields = named_fields();
381            assert!(fields.is_named());
382            assert!(!fields.is_unnamed());
383            assert!(!fields.is_unit());
384        }
385
386        #[test]
387        fn tuple_struct() {
388            let fields = unnamed_fields();
389            assert!(!fields.is_named());
390            assert!(fields.is_unnamed());
391            assert!(!fields.is_unit());
392        }
393
394        #[test]
395        fn unit_struct() {
396            let fields = unit_fields();
397            assert!(!fields.is_named());
398            assert!(!fields.is_unnamed());
399            assert!(fields.is_unit());
400        }
401    }
402
403    mod conversions {
404        use super::*;
405
406        #[test]
407        fn as_named_some() {
408            let fields = named_fields();
409            assert!(fields.as_named().is_some());
410        }
411
412        #[test]
413        fn as_named_none() {
414            let fields = unnamed_fields();
415            assert!(fields.as_named().is_none());
416        }
417
418        #[test]
419        fn as_unnamed_some() {
420            let fields = unnamed_fields();
421            assert!(fields.as_unnamed().is_some());
422        }
423
424        #[test]
425        fn as_unnamed_none() {
426            let fields = named_fields();
427            assert!(fields.as_unnamed().is_none());
428        }
429    }
430
431    mod get {
432        use super::*;
433
434        #[test]
435        fn by_name() {
436            let fields = named_fields();
437            let key: FieldKey = "x".into();
438            assert!(fields.get(&key).is_some());
439        }
440
441        #[test]
442        fn by_name_missing() {
443            let fields = named_fields();
444            let key: FieldKey = "z".into();
445            assert!(fields.get(&key).is_none());
446        }
447
448        #[test]
449        fn by_index_named() {
450            let fields = named_fields();
451            let key: FieldKey = 0usize.into();
452            assert!(fields.get(&key).is_some());
453        }
454
455        #[test]
456        fn by_index_unnamed() {
457            let fields = unnamed_fields();
458            let key: FieldKey = 1usize.into();
459            assert!(fields.get(&key).is_some());
460        }
461
462        #[test]
463        fn by_index_out_of_bounds() {
464            let fields = named_fields();
465            let key: FieldKey = 10usize.into();
466            assert!(fields.get(&key).is_none());
467        }
468
469        #[test]
470        fn on_unit() {
471            let fields = unit_fields();
472            let key: FieldKey = 0usize.into();
473            assert!(fields.get(&key).is_none());
474        }
475    }
476
477    mod exists {
478        use super::*;
479
480        #[test]
481        fn existing_field() {
482            let fields = named_fields();
483            let key: FieldKey = "x".into();
484            assert!(fields.exists(&key));
485        }
486
487        #[test]
488        fn missing_field() {
489            let fields = named_fields();
490            let key: FieldKey = "z".into();
491            assert!(!fields.exists(&key));
492        }
493    }
494
495    mod keyed {
496        use super::*;
497
498        #[test]
499        fn named_yields_named_keys() {
500            let fields = named_fields();
501            let pairs: Vec<_> = fields.keyed().collect();
502            assert_eq!(pairs.len(), 2);
503            assert!(pairs[0].0.is_named());
504            assert_eq!(pairs[0].0.to_string(), "x");
505            assert_eq!(pairs[1].0.to_string(), "y");
506        }
507
508        #[test]
509        fn unnamed_yields_index_keys() {
510            let fields = unnamed_fields();
511            let pairs: Vec<_> = fields.keyed().collect();
512            assert_eq!(pairs.len(), 2);
513            assert!(pairs[0].0.is_index());
514            assert_eq!(pairs[0].0.to_string(), "0");
515            assert_eq!(pairs[1].0.to_string(), "1");
516        }
517
518        #[test]
519        fn unit_yields_empty() {
520            let fields = unit_fields();
521            let pairs: Vec<_> = fields.keyed().collect();
522            assert!(pairs.is_empty());
523        }
524    }
525}