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