Skip to main content

dnf_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, Data, DeriveInput, Field, Fields};
4
5/// Primitive type names recognized by the derive macro. These all have
6/// `From<&T>` implementations for `Value` and a `DnfField` impl.
7const PRIMITIVES: &[&str] = &[
8    "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64", "usize", "f32", "f64", "bool",
9    "String",
10];
11
12/// Derives `DnfEvaluable` for a struct with named fields.
13///
14/// The macro generates a `DnfEvaluable` implementation that dispatches on
15/// the field name, delegating to the `DnfField` impl of each field's type.
16/// It supports nested access via dot notation, map targets via `@keys` /
17/// `@values`, and per-field configuration via `#[dnf(...)]` attributes.
18///
19/// # Supported field types
20///
21/// - Primitives: `i8`–`i64`, `u8`–`u64`, `f32`, `f64`, `bool`, `String`,
22///   `Cow<str>`.
23/// - Collections: `Vec<T>`, `HashSet<T>` where `T` is a primitive.
24/// - Maps: `HashMap<String, V>`, `BTreeMap<String, V>` where `V` is a
25///   primitive or another `DnfEvaluable` struct.
26/// - Options: `Option<T>` where `T` is any supported type.
27/// - User types implementing `DnfField` (or `DnfEvaluable` for nested
28///   structs).
29///
30/// For computed fields, implement `DnfEvaluable` manually instead.
31///
32/// # Attributes
33///
34/// - `#[dnf(skip)]` — exclude the field from queries.
35/// - `#[dnf(rename = "name")]` — expose the field under a different name in queries.
36/// - `#[dnf(nested)]` — force nested-access dispatch (auto-detected for non-primitive types).
37/// - `#[dnf(iter)]` or `#[dnf(iter = "method")]` — use a custom iterator on
38///   the field for collection-style queries.
39///
40/// # Nested access
41///
42/// Non-primitive fields are queried with dot notation, e.g.
43/// `address.city == "Boston"`.
44///
45/// - `Vec<T>`: `items.field` matches if any item satisfies the predicate.
46/// - `HashMap<K, V>`: `map.@keys`, `map.@values.field`,
47///   `map.["key"].field`.
48///
49/// # Examples
50///
51/// ```ignore
52/// use dnf::{DnfEvaluable, DnfQuery, Op};
53///
54/// #[derive(DnfEvaluable)]
55/// struct User {
56///     age: u32,
57///     name: String,
58/// }
59///
60/// let query = DnfQuery::builder()
61///     .or(|c| c.and("age", Op::GT, 18).and("name", Op::CONTAINS, "Al"))
62///     .build();
63///
64/// let alice = User { age: 25, name: "Alice".into() };
65/// assert!(query.evaluate(&alice));
66/// ```
67#[proc_macro_derive(DnfEvaluable, attributes(dnf))]
68pub fn derive_dnf_evaluable(input: TokenStream) -> TokenStream {
69    let input = parse_macro_input!(input as DeriveInput);
70
71    let name = &input.ident;
72
73    // Only support structs with named fields
74    let fields = match &input.data {
75        Data::Struct(data) => match &data.fields {
76            Fields::Named(fields) => &fields.named,
77            _ => {
78                return syn::Error::new_spanned(
79                    &input,
80                    "DnfEvaluable can only be derived for structs with named fields",
81                )
82                .to_compile_error()
83                .into();
84            }
85        },
86        _ => {
87            return syn::Error::new_spanned(&input, "DnfEvaluable can only be derived for structs")
88                .to_compile_error()
89                .into();
90        }
91    };
92
93    // Generate match arms for each field
94    let match_arms = fields.iter().filter_map(generate_field_match_arm);
95
96    // Generate nested field match arms for fields marked with #[dnf(nested)]
97    let nested_match_arms = fields.iter().filter_map(generate_nested_field_match_arm);
98
99    // Generate field info list
100    let field_infos = fields.iter().filter_map(generate_field_info);
101
102    // Generate field_value match arms for custom operator support
103    let field_value_arms = fields.iter().filter_map(generate_field_value_arm);
104
105    // Generate validate_field_path arms that recurse into scalar nested structs
106    let validate_path_arms = fields.iter().filter_map(generate_validate_field_path_arm);
107
108    let expanded = quote! {
109        impl dnf::DnfEvaluable for #name {
110            fn evaluate_field(
111                &self,
112                field_name: &str,
113                operator: &dnf::Op,
114                value: &dnf::Value
115            ) -> bool {
116                // Try direct field match first
117                match field_name {
118                    #(#match_arms)*
119                    _ => {
120                        // Try nested field access (field.subfield.subsubfield)
121                        if let Some(dot_pos) = field_name.find('.') {
122                            let (outer, inner) = field_name.split_at(dot_pos);
123                            let inner = &inner[1..]; // Skip the dot
124                            match outer {
125                                #(#nested_match_arms)*
126                                _ => false,
127                            }
128                        } else {
129                            false // Unknown field
130                        }
131                    }
132                }
133            }
134
135            fn field_value(&self, field_name: &str) -> Option<dnf::Value> {
136                match field_name {
137                    #(#field_value_arms)*
138                    _ => None,
139                }
140            }
141
142            fn fields() -> impl Iterator<Item = dnf::FieldInfo> {
143                [
144                    #(#field_infos),*
145                ].into_iter()
146            }
147
148            fn validate_field_path(path: &str) -> Option<dnf::FieldKind> {
149                if let Some(dot) = path.find('.') {
150                    let (head, tail) = path.split_at(dot);
151                    let tail = &tail[1..];
152                    match head {
153                        #(#validate_path_arms)*
154                        _ => {
155                            let _ = tail;
156                            <Self as dnf::DnfEvaluable>::fields()
157                                .find(|f| f.name() == head)
158                                .map(|f| f.kind())
159                        }
160                    }
161                } else {
162                    <Self as dnf::DnfEvaluable>::fields()
163                        .find(|f| f.name() == path)
164                        .map(|f| f.kind())
165                }
166            }
167        }
168    };
169
170    TokenStream::from(expanded)
171}
172
173/// Generate a match arm for a field
174fn generate_field_match_arm(field: &Field) -> Option<proc_macro2::TokenStream> {
175    let field_name = field.ident.as_ref()?;
176    let field_type = &field.ty;
177
178    // Check for #[dnf(skip)] attribute
179    if has_skip_attribute(field) {
180        return None;
181    }
182
183    // Get type string for analysis
184    let type_str = quote!(#field_type).to_string().replace(" ", "");
185
186    // Skip nested fields - they're handled by generate_nested_field_match_arm
187    // This includes explicit #[dnf(nested)] AND auto-detected nested types
188    // BUT NOT fields with #[dnf(iter)] - those are iterator-based collections
189    let has_iter = get_iter_attribute(field).is_some();
190    if has_nested_attribute(field) || (!has_iter && is_nested_type(&type_str)) {
191        return None;
192    }
193
194    // Get the query field name (either from rename attribute or field name)
195    let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
196
197    // Generate the value conversion using DnfField::evaluate()
198    let value_conversion = generate_value_conversion(field, field_name, field_type);
199
200    Some(quote! {
201        #query_name => #value_conversion,
202    })
203}
204
205/// Generate a match arm for field_value (custom operator support).
206/// Converts the field to Value for custom operator evaluation.
207/// Only generates for types that have `From<&T>` implementation for Value.
208fn generate_field_value_arm(field: &Field) -> Option<proc_macro2::TokenStream> {
209    let field_name = field.ident.as_ref()?;
210    let field_type = &field.ty;
211
212    // Check for #[dnf(skip)] attribute
213    if has_skip_attribute(field) {
214        return None;
215    }
216
217    // Get type string for analysis
218    let type_str = quote!(#field_type).to_string().replace(" ", "");
219
220    // Skip nested fields - custom ops don't make sense for nested access
221    let has_iter = get_iter_attribute(field).is_some();
222    if has_nested_attribute(field) || (!has_iter && is_nested_type(&type_str)) {
223        return None;
224    }
225
226    // Only generate for types we KNOW have From<&T> impl for Value
227    // This is the whitelist approach - safer than blacklist
228    if !is_value_convertible(&type_str) {
229        return None;
230    }
231
232    // Get the query field name (either from rename attribute or field name)
233    let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
234
235    // Generate Value conversion based on type
236    let value_conversion = if type_str.starts_with("Option<") {
237        // For Option<T>, convert to Value::None if None
238        quote! {
239            match &self.#field_name {
240                Some(v) => Some(dnf::Value::from(v)),
241                None => Some(dnf::Value::None),
242            }
243        }
244    } else {
245        // For direct types, convert to Value
246        quote! {
247            Some(dnf::Value::from(&self.#field_name))
248        }
249    };
250
251    Some(quote! {
252        #query_name => #value_conversion,
253    })
254}
255
256/// Check if a type can be converted to Value via From<&T>.
257/// Only returns true for types we know have this implementation.
258fn is_value_convertible(type_str: &str) -> bool {
259    if PRIMITIVES.contains(&type_str) {
260        return true;
261    }
262    if type_str.starts_with("&") && type_str.contains("str") {
263        return true;
264    }
265    if type_str.starts_with("Cow<") && type_str.contains("str") {
266        return true;
267    }
268    if type_str == "Box<str>" {
269        return true;
270    }
271    if let Some(inner) = type_str
272        .strip_prefix("Vec<")
273        .and_then(|s| s.strip_suffix(">"))
274    {
275        return PRIMITIVES.contains(&inner);
276    }
277    if let Some(inner) = type_str
278        .strip_prefix("HashSet<")
279        .and_then(|s| s.strip_suffix(">"))
280    {
281        // HashSet doesn't work with f32/f64 (not Hash)
282        return PRIMITIVES.contains(&inner) && inner != "f32" && inner != "f64";
283    }
284    if let Some(inner) = type_str
285        .strip_prefix("Option<")
286        .and_then(|s| s.strip_suffix(">"))
287    {
288        return is_value_convertible(inner);
289    }
290    false
291}
292
293/// Check if a type requires nested field access (collection/map of non-primitive inner type)
294/// Returns true only for collections/maps that contain nested structs needing evaluate_field delegation.
295/// Custom scalar types use DnfField::evaluate() directly and are NOT considered nested.
296fn is_nested_type(type_str: &str) -> bool {
297    // Check Vec<T> where T is non-primitive (nested struct)
298    if type_str.starts_with("Vec<") {
299        if let Some(inner) = type_str
300            .strip_prefix("Vec<")
301            .and_then(|s| s.strip_suffix(">"))
302        {
303            return !is_primitive_or_builtin(inner);
304        }
305    }
306
307    // Check Option<Vec<T>> where T is non-primitive
308    if type_str.starts_with("Option<Vec<") {
309        if let Some(inner) = type_str
310            .strip_prefix("Option<Vec<")
311            .and_then(|s| s.strip_suffix(">>"))
312        {
313            return !is_primitive_or_builtin(inner);
314        }
315    }
316
317    // Check HashMap<K, V> or BTreeMap<K, V> where V is non-primitive
318    if is_map_type(type_str) {
319        if let Some((_, value_type)) = extract_map_types(type_str) {
320            return !is_primitive_or_builtin(&value_type);
321        }
322    }
323
324    // Check Option<HashMap/BTreeMap>
325    if type_str.starts_with("Option<HashMap<") || type_str.starts_with("Option<BTreeMap<") {
326        if let Some(inner) = type_str
327            .strip_prefix("Option<")
328            .and_then(|s| s.strip_suffix(">"))
329        {
330            if let Some((_, value_type)) = extract_map_types(inner) {
331                return !is_primitive_or_builtin(&value_type);
332            }
333        }
334    }
335
336    // Custom scalar types (Score, Status, etc.) are NOT nested.
337    // They use DnfField::evaluate() directly.
338    // Only explicit #[dnf(nested)] or collection/map of nested structs triggers nested handling.
339    false
340}
341
342/// Check if a type is a map type (HashMap or BTreeMap)
343fn is_map_type(type_str: &str) -> bool {
344    type_str.starts_with("HashMap<") || type_str.starts_with("BTreeMap<")
345}
346
347/// Extract key and value types from HashMap<K, V> or BTreeMap<K, V>
348/// Returns (key_type, value_type) or None if not a map type
349fn extract_map_types(type_str: &str) -> Option<(String, String)> {
350    let inner = type_str
351        .strip_prefix("HashMap<")
352        .or_else(|| type_str.strip_prefix("BTreeMap<"))?;
353    let inner = inner.strip_suffix(">")?;
354
355    // Find the first comma that's not inside angle brackets
356    let mut depth = 0;
357    let mut comma_pos = None;
358    for (i, c) in inner.char_indices() {
359        match c {
360            '<' => depth += 1,
361            '>' => depth -= 1,
362            ',' if depth == 0 => {
363                comma_pos = Some(i);
364                break;
365            }
366            _ => {}
367        }
368    }
369
370    let pos = comma_pos?;
371    let key = inner[..pos].trim().to_string();
372    let value = inner[pos + 1..].trim().to_string();
373    Some((key, value))
374}
375
376/// Check if key type is string-like (String, &str, &'lifetime str)
377fn is_string_key(key_type: &str) -> bool {
378    let t = key_type.trim();
379    // Exact matches for common cases
380    matches!(t, "String" | "str" | "&str")
381        // Reference with lifetime: &'static str, &'a str, etc.
382        || (t.starts_with("&'") && (t.ends_with("str") || t.ends_with(" str")))
383}
384
385/// Walks every `#[dnf(...)]` attribute on `field` and invokes `visit` on
386/// each nested meta entry. Errors during nested parsing are silently
387/// swallowed — the helper attribute syntax is checked at compile time by
388/// the proc-macro caller.
389fn for_each_dnf_meta(field: &Field, mut visit: impl FnMut(&syn::meta::ParseNestedMeta<'_>)) {
390    for attr in &field.attrs {
391        if attr.path().is_ident("dnf") {
392            let _ = attr.parse_nested_meta(|meta| {
393                visit(&meta);
394                Ok(())
395            });
396        }
397    }
398}
399
400/// Check if field has a `#[dnf(<flag>)]` marker.
401fn has_dnf_flag(field: &Field, flag: &str) -> bool {
402    let mut found = false;
403    for_each_dnf_meta(field, |meta| {
404        if meta.path.is_ident(flag) {
405            found = true;
406        }
407    });
408    found
409}
410
411/// Check if field has #[dnf(skip)] attribute
412fn has_skip_attribute(field: &Field) -> bool {
413    has_dnf_flag(field, "skip")
414}
415
416/// Check if field has #[dnf(nested)] attribute
417fn has_nested_attribute(field: &Field) -> bool {
418    has_dnf_flag(field, "nested")
419}
420
421/// Generate a match arm for nested field access.
422/// Auto-detects nested types (non-primitive inner types) or uses explicit #[dnf(nested)].
423///
424/// Supports:
425/// - Scalar nested: `address.city` → delegates to `self.address.evaluate_field("city", ...)`
426/// - Vec nested: `addresses.city` → `self.addresses.iter().any(|item| item.evaluate_field("city", ...))`
427/// - HashMap nested: `branches.@values.city` → iterate values, delegate (explicit @values required)
428fn generate_nested_field_match_arm(field: &Field) -> Option<proc_macro2::TokenStream> {
429    let field_name = field.ident.as_ref()?;
430    let field_type = &field.ty;
431
432    // Skip fields marked with #[dnf(skip)]
433    if has_skip_attribute(field) {
434        return None;
435    }
436
437    // Detect collection type and generate appropriate code
438    let type_str = quote!(#field_type).to_string().replace(" ", "");
439
440    // Fields with #[dnf(iter)] are iterator-based collections, not nested
441    let has_iter = get_iter_attribute(field).is_some();
442    if has_iter {
443        return None;
444    }
445
446    // Only generate for nested types (explicit attribute OR auto-detected)
447    if !has_nested_attribute(field) && !is_nested_type(&type_str) {
448        return None;
449    }
450
451    // Get the query field name (either from rename attribute or field name)
452    let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
453
454    let delegation_code = if type_str.starts_with("Vec<") {
455        // Vec<T> with nested: iterate and delegate with any() semantics
456        quote! {
457            self.#field_name.iter().any(|item| item.evaluate_field(inner, operator, value))
458        }
459    } else if type_str.starts_with("Option<Vec<") {
460        // Option<Vec<T>> with nested
461        quote! {
462            match &self.#field_name {
463                Some(vec) => vec.iter().any(|item| item.evaluate_field(inner, operator, value)),
464                None => false,
465            }
466        }
467    } else if type_str.starts_with("HashMap<") || type_str.starts_with("BTreeMap<") {
468        // HashMap<K, V> with nested values
469        // Requires explicit syntax: @values.field, @keys, ["key"].field
470        let body = nested_map_dispatch(quote! { self.#field_name });
471        quote! { #body }
472    } else if type_str.starts_with("Option<HashMap<") || type_str.starts_with("Option<BTreeMap<") {
473        // Option<HashMap<K, V>> with nested - requires explicit syntax
474        let body = nested_map_dispatch(quote! { map });
475        quote! {
476            match &self.#field_name {
477                Some(map) => { #body },
478                None => false,
479            }
480        }
481    } else if type_str.starts_with("Option<") {
482        // Option<T> scalar nested
483        quote! {
484            match &self.#field_name {
485                Some(inner_val) => inner_val.evaluate_field(inner, operator, value),
486                None => false,
487            }
488        }
489    } else {
490        // Scalar nested struct: direct delegation
491        quote! {
492            self.#field_name.evaluate_field(inner, operator, value)
493        }
494    };
495
496    Some(quote! {
497        #query_name => #delegation_code,
498    })
499}
500
501/// Emits the runtime dispatch for nested map access (`@values.field`,
502/// `@keys`, `["key"].field`). Used by both bare `HashMap`/`BTreeMap`
503/// fields and their `Option<…>` wrappers; `map_expr` is the expression
504/// that evaluates to the map (`self.field` or a bound `map` ident).
505fn nested_map_dispatch(map_expr: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
506    quote! {
507        if let Some(rest) = inner.strip_prefix("@values.") {
508            #map_expr.values().any(|item| item.evaluate_field(rest, operator, value))
509        } else if inner == "@keys" {
510            operator.any(#map_expr.keys(), value)
511        } else if inner.starts_with("[\"") {
512            if let Some(end_bracket) = inner.find("\"]") {
513                let key = &inner[2..end_bracket];
514                let rest = inner.get(end_bracket + 2..).unwrap_or("").trim_start_matches('.');
515                if rest.is_empty() {
516                    false
517                } else {
518                    match #map_expr.get(key) {
519                        Some(item) => item.evaluate_field(rest, operator, value),
520                        None => false,
521                    }
522                }
523            } else {
524                false
525            }
526        } else {
527            false
528        }
529    }
530}
531
532/// Emits a `validate_field_path` arm that recurses into a scalar nested
533/// struct so the full dotted path is validated. Only fires for non-collection
534/// nested types — collection/map nesting is left to the fallback arm, which
535/// returns the field's kind without descending further.
536fn generate_validate_field_path_arm(field: &Field) -> Option<proc_macro2::TokenStream> {
537    let field_name = field.ident.as_ref()?;
538    let field_type = &field.ty;
539
540    if has_skip_attribute(field) {
541        return None;
542    }
543
544    let type_str = quote!(#field_type).to_string().replace(" ", "");
545
546    // Only recurse for explicit scalar nested fields. Collection/map nested
547    // types use runtime-interpreted syntax (@values, ["key"], etc.) and are
548    // handled by the fallback arm.
549    if !has_nested_attribute(field) {
550        return None;
551    }
552    let is_collection = type_str.starts_with("Vec<")
553        || type_str.starts_with("Option<Vec<")
554        || type_str.starts_with("HashMap<")
555        || type_str.starts_with("BTreeMap<")
556        || type_str.starts_with("Option<HashMap<")
557        || type_str.starts_with("Option<BTreeMap<")
558        || type_str.starts_with("HashSet<")
559        || type_str.starts_with("BTreeSet<");
560    if is_collection {
561        return None;
562    }
563
564    let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
565
566    // Strip Option<T> for the recurse target so Option<Address> recurses
567    // through Address::validate_field_path.
568    let inner_type_str = type_str
569        .strip_prefix("Option<")
570        .and_then(|s| s.strip_suffix(">"))
571        .unwrap_or(&type_str)
572        .to_string();
573    let inner_type: syn::Type = syn::parse_str(&inner_type_str).ok()?;
574
575    Some(quote! {
576        #query_name => <#inner_type as dnf::DnfEvaluable>::validate_field_path(tail),
577    })
578}
579
580/// Get rename attribute value if present
581fn get_rename_attribute(field: &Field) -> Option<String> {
582    let mut rename_value = None;
583    for_each_dnf_meta(field, |meta| {
584        if meta.path.is_ident("rename") {
585            if let Ok(value) = meta.value() {
586                if let Ok(lit_str) = value.parse::<syn::LitStr>() {
587                    rename_value = Some(lit_str.value());
588                }
589            }
590        }
591    });
592    rename_value
593}
594
595/// Get iter attribute value if present.
596/// Returns:
597/// - `Some(None)` for `#[dnf(iter)]` (uses `.iter()` method)
598/// - `Some(Some("method"))` for `#[dnf(iter = "method")]` (uses custom method)
599/// - `None` if not present
600fn get_iter_attribute(field: &Field) -> Option<Option<String>> {
601    let mut has_iter = false;
602    let mut iter_method = None;
603    for_each_dnf_meta(field, |meta| {
604        if meta.path.is_ident("iter") {
605            has_iter = true;
606            if let Ok(value) = meta.value() {
607                if let Ok(lit_str) = value.parse::<syn::LitStr>() {
608                    iter_method = Some(lit_str.value());
609                }
610            }
611        }
612    });
613    if has_iter {
614        Some(iter_method)
615    } else {
616        None
617    }
618}
619
620/// Generate value conversion code based on field type.
621///
622/// All types now use `DnfField::evaluate()` for type-safe comparison.
623/// This simplifies code generation and keeps all logic in trait implementations.
624///
625/// Fields with `#[dnf(iter)]` attribute use custom iterator method.
626fn generate_value_conversion(
627    field: &Field,
628    field_name: &syn::Ident,
629    _field_type: &syn::Type,
630) -> proc_macro2::TokenStream {
631    // Check for #[dnf(iter)] attribute - use any for custom collection types
632    if let Some(iter_method) = get_iter_attribute(field) {
633        let method = iter_method.unwrap_or_else(|| "iter".to_string());
634        let method_ident = syn::Ident::new(&method, field_name.span());
635        return quote! {
636            operator.any(self.#field_name.#method_ident(), value)
637        };
638    }
639
640    // All types use DnfField::evaluate() - simple and type-safe
641    quote! {
642        dnf::DnfField::evaluate(&self.#field_name, operator, value)
643    }
644}
645
646/// Check if a type is a built-in primitive that implements `DnfField`.
647///
648/// Returns true for types that can be used directly in queries without nested field access:
649/// - Primitives: i8-i64, u8-u64, f32, f64, bool, String, &str
650/// - Collections of primitives: `Vec<T>`, `HashSet<T>`, `HashMap<String, V>` where T/V is primitive
651///
652/// Returns false for custom types that require nested field access delegation.
653fn is_primitive_or_builtin(type_str: &str) -> bool {
654    if PRIMITIVES.contains(&type_str) {
655        return true;
656    }
657    if type_str.starts_with("&") && type_str.contains("str") {
658        return true;
659    }
660    if type_str.starts_with("Cow<") && type_str.contains("str") {
661        return true;
662    }
663    if type_str == "Box<str>" {
664        return true;
665    }
666    if let Some(inner) = type_str
667        .strip_prefix("Vec<")
668        .and_then(|s| s.strip_suffix(">"))
669    {
670        return is_primitive_or_builtin(inner);
671    }
672    if let Some(inner) = type_str
673        .strip_prefix("HashSet<")
674        .and_then(|s| s.strip_suffix(">"))
675    {
676        if inner == "f32" || inner == "f64" {
677            return false;
678        }
679        return is_primitive_or_builtin(inner);
680    }
681    if is_map_type(type_str) {
682        if let Some((key_type, value_type)) = extract_map_types(type_str) {
683            return is_string_key(&key_type) && is_primitive_or_builtin(&value_type);
684        }
685    }
686    false
687}
688
689/// Generate a FieldInfo for a field
690fn generate_field_info(field: &Field) -> Option<proc_macro2::TokenStream> {
691    let field_name = field.ident.as_ref()?;
692    let field_type = &field.ty;
693
694    // Skip fields with #[dnf(skip)] attribute
695    if has_skip_attribute(field) {
696        return None;
697    }
698
699    // Get the query field name (either from rename attribute or field name)
700    let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
701
702    // Get the type as a string
703    let type_str = quote!(#field_type).to_string();
704    let type_str_normalized = type_str.replace(" ", "");
705
706    // Determine field kind
707    let field_kind = if get_iter_attribute(field).is_some() {
708        // Fields with #[dnf(iter)] are treated as iterator-based
709        quote! { dnf::FieldKind::Iter }
710    } else if is_map_type(&type_str_normalized) {
711        quote! { dnf::FieldKind::Map }
712    } else if type_str_normalized.starts_with("Vec<")
713        || type_str_normalized.starts_with("HashSet<")
714        || type_str_normalized.starts_with("BTreeSet<")
715    {
716        quote! { dnf::FieldKind::Iter }
717    } else {
718        quote! { dnf::FieldKind::Scalar }
719    };
720
721    Some(quote! {
722        dnf::FieldInfo::with_kind(#query_name, #type_str, #field_kind)
723    })
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729
730    #[test]
731    fn test_all_field_types_use_dnf_field() {
732        // Every supported field-type category should expand to a
733        // `DnfField::evaluate(...)` call: primitives, collections, and
734        // user-defined scalar types alike.
735        let types = [
736            // primitives
737            "String",
738            "u32",
739            "i64",
740            "f64",
741            "bool",
742            // collections
743            "Vec<String>",
744            "HashSet<i32>",
745            // user-defined scalar types
746            "Score",
747            "CustomEnum",
748            "MyStruct",
749        ];
750
751        for type_str in types {
752            let input_str = format!("struct User {{ field: {} }}", type_str);
753            let input: proc_macro2::TokenStream = input_str.parse().unwrap();
754
755            let parsed: DeriveInput = syn::parse2(input).unwrap();
756            let fields = match &parsed.data {
757                Data::Struct(data) => match &data.fields {
758                    Fields::Named(fields) => &fields.named,
759                    _ => continue,
760                },
761                _ => continue,
762            };
763
764            if let Some(field) = fields.first() {
765                let conversion =
766                    generate_value_conversion(field, field.ident.as_ref().unwrap(), &field.ty);
767                let conversion_str = conversion.to_string();
768
769                assert!(
770                    conversion_str.contains("DnfField :: evaluate"),
771                    "Type {} should use DnfField::evaluate(), got: {}",
772                    type_str,
773                    conversion_str
774                );
775            }
776        }
777    }
778
779    #[test]
780    fn test_iter_attribute_generates_any() {
781        // Test that #[dnf(iter)] generates any call
782        let input_str = "struct User { #[dnf(iter)] field: LinkedList<String> }";
783        let input: proc_macro2::TokenStream = input_str.parse().unwrap();
784
785        let parsed: DeriveInput = syn::parse2(input).unwrap();
786        let fields = match &parsed.data {
787            Data::Struct(data) => match &data.fields {
788                Fields::Named(fields) => &fields.named,
789                _ => panic!("Expected named fields"),
790            },
791            _ => panic!("Expected struct"),
792        };
793
794        let field = fields.first().unwrap();
795        let conversion = generate_value_conversion(field, field.ident.as_ref().unwrap(), &field.ty);
796        let conversion_str = conversion.to_string();
797
798        // Should use any with .iter()
799        assert!(
800            conversion_str.contains("any") && conversion_str.contains(". iter ()"),
801            "Expected any with .iter(), got: {}",
802            conversion_str
803        );
804    }
805
806    #[test]
807    fn test_iter_attribute_with_custom_method() {
808        // Test that #[dnf(iter = "items")] generates any with custom method
809        let input_str = "struct User { #[dnf(iter = \"items\")] field: CustomList<i32> }";
810        let input: proc_macro2::TokenStream = input_str.parse().unwrap();
811
812        let parsed: DeriveInput = syn::parse2(input).unwrap();
813        let fields = match &parsed.data {
814            Data::Struct(data) => match &data.fields {
815                Fields::Named(fields) => &fields.named,
816                _ => panic!("Expected named fields"),
817            },
818            _ => panic!("Expected struct"),
819        };
820
821        let field = fields.first().unwrap();
822        let conversion = generate_value_conversion(field, field.ident.as_ref().unwrap(), &field.ty);
823        let conversion_str = conversion.to_string();
824
825        // Should use any with .items()
826        assert!(
827            conversion_str.contains("any") && conversion_str.contains(". items ()"),
828            "Expected any with .items(), got: {}",
829            conversion_str
830        );
831    }
832}