1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, Data, DeriveInput, Field, Fields};
4
5const PRIMITIVES: &[&str] = &[
8 "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64", "usize", "f32", "f64", "bool",
9 "String",
10];
11
12#[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 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 let match_arms = fields.iter().filter_map(generate_field_match_arm);
95
96 let nested_match_arms = fields.iter().filter_map(generate_nested_field_match_arm);
98
99 let field_infos = fields.iter().filter_map(generate_field_info);
101
102 let field_value_arms = fields.iter().filter_map(generate_field_value_arm);
104
105 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 match field_name {
118 #(#match_arms)*
119 _ => {
120 if let Some(dot_pos) = field_name.find('.') {
122 let (outer, inner) = field_name.split_at(dot_pos);
123 let inner = &inner[1..]; match outer {
125 #(#nested_match_arms)*
126 _ => false,
127 }
128 } else {
129 false }
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
173fn 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 if has_skip_attribute(field) {
180 return None;
181 }
182
183 let type_str = quote!(#field_type).to_string().replace(" ", "");
185
186 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 let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
196
197 let value_conversion = generate_value_conversion(field, field_name, field_type);
199
200 Some(quote! {
201 #query_name => #value_conversion,
202 })
203}
204
205fn 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 if has_skip_attribute(field) {
214 return None;
215 }
216
217 let type_str = quote!(#field_type).to_string().replace(" ", "");
219
220 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 if !is_value_convertible(&type_str) {
229 return None;
230 }
231
232 let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
234
235 let value_conversion = if type_str.starts_with("Option<") {
237 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 quote! {
247 Some(dnf::Value::from(&self.#field_name))
248 }
249 };
250
251 Some(quote! {
252 #query_name => #value_conversion,
253 })
254}
255
256fn 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 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
293fn is_nested_type(type_str: &str) -> bool {
297 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 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 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 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 false
340}
341
342fn is_map_type(type_str: &str) -> bool {
344 type_str.starts_with("HashMap<") || type_str.starts_with("BTreeMap<")
345}
346
347fn 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 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
376fn is_string_key(key_type: &str) -> bool {
378 let t = key_type.trim();
379 matches!(t, "String" | "str" | "&str")
381 || (t.starts_with("&'") && (t.ends_with("str") || t.ends_with(" str")))
383}
384
385fn 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
400fn 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
411fn has_skip_attribute(field: &Field) -> bool {
413 has_dnf_flag(field, "skip")
414}
415
416fn has_nested_attribute(field: &Field) -> bool {
418 has_dnf_flag(field, "nested")
419}
420
421fn 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 if has_skip_attribute(field) {
434 return None;
435 }
436
437 let type_str = quote!(#field_type).to_string().replace(" ", "");
439
440 let has_iter = get_iter_attribute(field).is_some();
442 if has_iter {
443 return None;
444 }
445
446 if !has_nested_attribute(field) && !is_nested_type(&type_str) {
448 return None;
449 }
450
451 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 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 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 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 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 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 quote! {
492 self.#field_name.evaluate_field(inner, operator, value)
493 }
494 };
495
496 Some(quote! {
497 #query_name => #delegation_code,
498 })
499}
500
501fn 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
532fn 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 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 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
580fn 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
595fn 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
620fn generate_value_conversion(
627 field: &Field,
628 field_name: &syn::Ident,
629 _field_type: &syn::Type,
630) -> proc_macro2::TokenStream {
631 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 quote! {
642 dnf::DnfField::evaluate(&self.#field_name, operator, value)
643 }
644}
645
646fn 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
689fn 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 if has_skip_attribute(field) {
696 return None;
697 }
698
699 let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
701
702 let type_str = quote!(#field_type).to_string();
704 let type_str_normalized = type_str.replace(" ", "");
705
706 let field_kind = if get_iter_attribute(field).is_some() {
708 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 let types = [
736 "String",
738 "u32",
739 "i64",
740 "f64",
741 "bool",
742 "Vec<String>",
744 "HashSet<i32>",
745 "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 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 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 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 assert!(
827 conversion_str.contains("any") && conversion_str.contains(". items ()"),
828 "Expected any with .items(), got: {}",
829 conversion_str
830 );
831 }
832}