1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, Data, DeriveInput, Field, Fields};
4
5#[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 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 let match_arms = fields.iter().filter_map(generate_field_match_arm);
88
89 let nested_match_arms = fields.iter().filter_map(generate_nested_field_match_arm);
91
92 let field_infos = fields.iter().filter_map(generate_field_info);
94
95 let field_value_arms = fields.iter().filter_map(generate_field_value_arm);
97
98 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 match field_name {
111 #(#match_arms)*
112 _ => {
113 if let Some(dot_pos) = field_name.find('.') {
115 let (outer, inner) = field_name.split_at(dot_pos);
116 let inner = &inner[1..]; match outer {
118 #(#nested_match_arms)*
119 _ => false,
120 }
121 } else {
122 false }
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
166fn 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 if has_skip_attribute(field) {
173 return None;
174 }
175
176 let type_str = quote!(#field_type).to_string().replace(" ", "");
178
179 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 let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
189
190 let value_conversion = generate_value_conversion(field, field_name, field_type);
192
193 Some(quote! {
194 #query_name => #value_conversion,
195 })
196}
197
198fn 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 if has_skip_attribute(field) {
207 return None;
208 }
209
210 let type_str = quote!(#field_type).to_string().replace(" ", "");
212
213 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 if !is_value_convertible(&type_str) {
222 return None;
223 }
224
225 let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
227
228 let value_conversion = if type_str.starts_with("Option<") {
230 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 quote! {
240 Some(dnf::Value::from(&self.#field_name))
241 }
242 };
243
244 Some(quote! {
245 #query_name => #value_conversion,
246 })
247}
248
249fn is_value_convertible(type_str: &str) -> bool {
252 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 if type_str.starts_with("&") && type_str.contains("str") {
264 return true;
265 }
266
267 if type_str.starts_with("Cow<") && type_str.contains("str") {
269 return true;
270 }
271
272 if type_str == "Box<str>" {
274 return true;
275 }
276
277 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 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 return primitives.contains(&inner) && inner != "f32" && inner != "f64";
295 }
296 }
297
298 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
311fn is_nested_type(type_str: &str) -> bool {
315 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 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 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 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 false
358}
359
360fn is_map_type(type_str: &str) -> bool {
362 type_str.starts_with("HashMap<") || type_str.starts_with("BTreeMap<")
363}
364
365fn 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 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
394fn is_string_key(key_type: &str) -> bool {
396 let t = key_type.trim();
397 matches!(t, "String" | "str" | "&str")
399 || (t.starts_with("&'") && (t.ends_with("str") || t.ends_with(" str")))
401}
402
403fn 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
422fn 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
441fn 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 if has_skip_attribute(field) {
454 return None;
455 }
456
457 let type_str = quote!(#field_type).to_string().replace(" ", "");
459
460 let has_iter = get_iter_attribute(field).is_some();
462 if has_iter {
463 return None;
464 }
465
466 if !has_nested_attribute(field) && !is_nested_type(&type_str) {
468 return None;
469 }
470
471 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 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 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 quote! {
491 if let Some(rest) = inner.strip_prefix("@values.") {
492 self.#field_name.values().any(|item| item.evaluate_field(rest, operator, value))
494 } else if inner == "@keys" {
495 operator.any(self.#field_name.keys(), value)
497 } else if inner.starts_with("[\"") {
498 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 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 false
517 }
518 }
519 } else if type_str.starts_with("Option<HashMap<") || type_str.starts_with("Option<BTreeMap<") {
520 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 false
546 }
547 },
548 None => false,
549 }
550 }
551 } else if type_str.starts_with("Option<") {
552 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 quote! {
562 self.#field_name.evaluate_field(inner, operator, value)
563 }
564 };
565
566 Some(quote! {
567 #query_name => #delegation_code,
568 })
569}
570
571fn 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 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 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
619fn 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
642fn 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 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
672fn generate_value_conversion(
679 field: &Field,
680 field_name: &syn::Ident,
681 _field_type: &syn::Type,
682) -> proc_macro2::TokenStream {
683 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 quote! {
694 dnf::DnfField::evaluate(&self.#field_name, operator, value)
695 }
696}
697
698fn is_primitive_or_builtin(type_str: &str) -> bool {
706 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 if type_str.starts_with("&") && type_str.contains("str") {
718 return true;
719 }
720
721 if type_str.starts_with("Cow<") && type_str.contains("str") {
723 return true;
724 }
725
726 if type_str == "Box<str>" {
728 return true;
729 }
730
731 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 return is_primitive_or_builtin(inner);
737 }
738 }
739 }
740
741 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 if inner == "f32" || inner == "f64" {
748 return false;
749 }
750 return is_primitive_or_builtin(inner);
752 }
753 }
754 }
755
756 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
766fn 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 if has_skip_attribute(field) {
773 return None;
774 }
775
776 let query_name = get_rename_attribute(field).unwrap_or_else(|| field_name.to_string());
778
779 let type_str = quote!(#field_type).to_string();
781 let type_str_normalized = type_str.replace(" ", "");
782
783 let field_kind = if get_iter_attribute(field).is_some() {
785 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 let types = [
813 "String",
815 "u32",
816 "i64",
817 "f64",
818 "bool",
819 "Vec<String>",
821 "HashSet<i32>",
822 "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 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 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 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 assert!(
904 conversion_str.contains("any") && conversion_str.contains(". items ()"),
905 "Expected any with .items(), got: {}",
906 conversion_str
907 );
908 }
909}