1use proc_macro::TokenStream;
78use quote::{format_ident, quote, ToTokens};
79use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields, Ident, Lit, Meta};
80
81#[proc_macro_derive(ProbarEntity, attributes(probar))]
102pub fn derive_probar_entity(input: TokenStream) -> TokenStream {
103 let input = parse_macro_input!(input as DeriveInput);
104 let name = &input.ident;
105
106 let entity_name =
108 extract_name_attribute(&input.attrs).unwrap_or_else(|| to_snake_case(&name.to_string()));
109
110 let type_id = generate_type_id(&entity_name);
112
113 let expanded = quote! {
114 impl ::probar::ProbarEntity for #name {
115 fn entity_name() -> &'static str {
116 #entity_name
117 }
118
119 fn entity_type_id() -> u64 {
120 #type_id
121 }
122 }
123
124 impl #name {
125 #[inline]
127 pub const fn probar_name() -> &'static str {
128 #entity_name
129 }
130
131 #[inline]
133 pub const fn probar_type_id() -> u64 {
134 #type_id
135 }
136 }
137 };
138
139 TokenStream::from(expanded)
140}
141
142#[proc_macro_derive(ProbarComponent, attributes(probar))]
170pub fn derive_probar_component(input: TokenStream) -> TokenStream {
171 let input = parse_macro_input!(input as DeriveInput);
172 let name = &input.ident;
173
174 let component_name =
176 extract_name_attribute(&input.attrs).unwrap_or_else(|| to_snake_case(&name.to_string()));
177
178 let type_id = generate_type_id(&component_name);
180
181 let fields_info = extract_fields(&input.data);
183 let field_names: Vec<&str> = fields_info
184 .iter()
185 .filter(|(_, skip)| !skip)
186 .map(|(name, _)| name.as_str())
187 .collect();
188 let field_count = field_names.len();
189
190 let expanded = quote! {
191 impl ::probar::ProbarComponent for #name {
192 fn component_name() -> &'static str {
193 #component_name
194 }
195
196 fn component_type_id() -> u64 {
197 #type_id
198 }
199
200 fn field_names() -> &'static [&'static str] {
201 &[#(#field_names),*]
202 }
203
204 fn field_count() -> usize {
205 #field_count
206 }
207 }
208
209 impl #name {
210 #[inline]
212 pub const fn probar_name() -> &'static str {
213 #component_name
214 }
215
216 #[inline]
218 pub const fn probar_type_id() -> u64 {
219 #type_id
220 }
221
222 #[inline]
224 pub const fn probar_fields() -> &'static [&'static str] {
225 &[#(#field_names),*]
226 }
227 }
228 };
229
230 TokenStream::from(expanded)
231}
232
233#[proc_macro_derive(ProbarSelector, attributes(probar))]
257pub fn derive_probar_selector(input: TokenStream) -> TokenStream {
258 let input = parse_macro_input!(input as DeriveInput);
259 let name = &input.ident;
260
261 let (entities, components) = parse_selector_attributes(&input.attrs);
263
264 let entity_enum_name = format_ident!("{}Entity", name);
265 let component_enum_name = format_ident!("{}Component", name);
266
267 let entity_variants: Vec<_> = entities.iter().map(|e| format_ident!("{}", e)).collect();
269 let entity_names: Vec<String> = entities.iter().map(|e| to_snake_case(e)).collect();
270
271 let component_variants: Vec<_> = components.iter().map(|c| format_ident!("{}", c)).collect();
273 let component_names: Vec<String> = components.iter().map(|c| to_snake_case(c)).collect();
274
275 let expanded = quote! {
276 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
278 pub enum #entity_enum_name {
279 #(#entity_variants),*
280 }
281
282 impl #entity_enum_name {
283 pub const fn all() -> &'static [Self] {
285 &[#(Self::#entity_variants),*]
286 }
287
288 pub const fn name(&self) -> &'static str {
290 match self {
291 #(Self::#entity_variants => #entity_names),*
292 }
293 }
294
295 pub const fn count() -> usize {
297 #(let _ = Self::#entity_variants;)* [#(Self::#entity_variants),*].len()
299 }
300 }
301
302 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
304 pub enum #component_enum_name {
305 #(#component_variants),*
306 }
307
308 impl #component_enum_name {
309 pub const fn all() -> &'static [Self] {
311 &[#(Self::#component_variants),*]
312 }
313
314 pub const fn name(&self) -> &'static str {
316 match self {
317 #(Self::#component_variants => #component_names),*
318 }
319 }
320
321 pub const fn count() -> usize {
323 #(let _ = Self::#component_variants;)*
324 [#(Self::#component_variants),*].len()
325 }
326 }
327
328 impl #name {
329 pub type Entity = #entity_enum_name;
331 pub type Component = #component_enum_name;
333
334 pub const fn entities() -> &'static [#entity_enum_name] {
336 #entity_enum_name::all()
337 }
338
339 pub const fn components() -> &'static [#component_enum_name] {
341 #component_enum_name::all()
342 }
343 }
344 };
345
346 TokenStream::from(expanded)
347}
348
349#[proc_macro_attribute]
364pub fn probar_test(attr: TokenStream, item: TokenStream) -> TokenStream {
365 let input = parse_macro_input!(item as syn::ItemFn);
366 let fn_name = &input.sig.ident;
367 let fn_block = &input.block;
368 let fn_vis = &input.vis;
369 let fn_attrs = &input.attrs;
370 let fn_async = &input.sig.asyncness;
371
372 let timeout_ms: u64 = parse_timeout_attr(attr).unwrap_or(30000);
374
375 let test_name = fn_name.to_string();
376
377 let expanded = if fn_async.is_some() {
378 quote! {
379 #(#fn_attrs)*
380 #[test]
381 #fn_vis fn #fn_name() {
382 let rt = ::tokio::runtime::Runtime::new().expect("Failed to create runtime");
383 let result = rt.block_on(async {
384 let timeout = ::std::time::Duration::from_millis(#timeout_ms);
385 ::tokio::time::timeout(timeout, async #fn_block).await
386 });
387
388 match result {
389 Ok(Ok(())) => (),
390 Ok(Err(e)) => panic!("Test '{}' failed: {:?}", #test_name, e),
391 Err(_) => panic!("Test '{}' timed out after {}ms", #test_name, #timeout_ms),
392 }
393 }
394 }
395 } else {
396 quote! {
397 #(#fn_attrs)*
398 #[test]
399 #fn_vis fn #fn_name() {
400 let start = ::std::time::Instant::now();
401 let timeout = ::std::time::Duration::from_millis(#timeout_ms);
402
403 let result: Result<(), Box<dyn ::std::error::Error>> = (|| #fn_block)();
404
405 if start.elapsed() > timeout {
406 panic!("Test '{}' timed out after {}ms", #test_name, #timeout_ms);
407 }
408
409 if let Err(e) = result {
410 panic!("Test '{}' failed: {:?}", #test_name, e);
411 }
412 }
413 }
414 };
415
416 TokenStream::from(expanded)
417}
418
419fn extract_name_attribute(attrs: &[Attribute]) -> Option<String> {
425 attrs.iter().find_map(extract_name_from_attr)
426}
427
428fn extract_name_from_attr(attr: &Attribute) -> Option<String> {
430 if !attr.path().is_ident("probar") {
431 return None;
432 }
433 let nv = attr.parse_args::<Meta>().ok()?;
434 let Meta::NameValue(nv) = nv else { return None };
435 if !nv.path.is_ident("name") {
436 return None;
437 }
438 let syn::Expr::Lit(syn::ExprLit {
439 lit: Lit::Str(s), ..
440 }) = &nv.value
441 else {
442 return None;
443 };
444 Some(s.value())
445}
446
447fn extract_fields(data: &Data) -> Vec<(String, bool)> {
449 match data {
450 Data::Struct(data_struct) => match &data_struct.fields {
451 Fields::Named(fields) => fields
452 .named
453 .iter()
454 .map(|f| {
455 let name = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
456 let skip = f.attrs.iter().any(|attr| {
457 attr.path().is_ident("probar")
458 && attr
459 .parse_args::<Ident>()
460 .map(|i| i == "skip")
461 .unwrap_or(false)
462 });
463 (name, skip)
464 })
465 .collect(),
466 Fields::Unnamed(fields) => fields
467 .unnamed
468 .iter()
469 .enumerate()
470 .map(|(i, _)| (format!("field_{i}"), false))
471 .collect(),
472 Fields::Unit => vec![],
473 },
474 _ => vec![],
475 }
476}
477
478fn parse_selector_attributes(attrs: &[Attribute]) -> (Vec<String>, Vec<String>) {
480 let mut entities = Vec::new();
481 let mut components = Vec::new();
482
483 for attr in attrs {
484 if !attr.path().is_ident("probar") {
485 continue;
486 }
487 let tokens = attr.meta.to_token_stream().to_string();
488
489 if tokens.contains("entities") {
490 entities.extend(extract_list_items(&tokens, 0));
491 }
492 if tokens.contains("components") {
493 let offset = if tokens.contains("entities") {
496 tokens.find(']').map(|i| i + 1).unwrap_or(0)
497 } else {
498 0
499 };
500 components.extend(extract_list_items(&tokens, offset));
501 }
502 }
503
504 (entities, components)
505}
506
507fn extract_list_items(tokens: &str, offset: usize) -> Vec<String> {
509 let rest = &tokens[offset..];
510 let Some(start) = rest.find('[') else {
511 return vec![];
512 };
513 let Some(end) = rest.find(']') else {
514 return vec![];
515 };
516
517 rest[start + 1..end]
518 .split(',')
519 .map(str::trim)
520 .filter(|s| !s.is_empty())
521 .map(String::from)
522 .collect()
523}
524
525fn parse_timeout_attr(attr: TokenStream) -> Option<u64> {
527 let attr_str = attr.to_string();
528 if attr_str.contains("timeout_ms") {
529 for part in attr_str.split('=') {
531 if let Ok(n) = part.trim().parse::<u64>() {
532 return Some(n);
533 }
534 }
535 }
536 None
537}
538
539fn to_snake_case(s: &str) -> String {
541 let mut result = String::with_capacity(s.len() + 4);
542 let mut prev_lower = false;
543
544 for c in s.chars() {
545 if c.is_uppercase() {
546 if prev_lower {
547 result.push('_');
548 }
549 result.push(c.to_ascii_lowercase());
550 prev_lower = false;
551 } else {
552 result.push(c);
553 prev_lower = true;
554 }
555 }
556
557 result
558}
559
560fn generate_type_id(name: &str) -> u64 {
562 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
563 const FNV_PRIME: u64 = 0x0100_0000_01b3;
564
565 let mut hash = FNV_OFFSET;
566 for byte in name.as_bytes() {
567 hash ^= u64::from(*byte);
568 hash = hash.wrapping_mul(FNV_PRIME);
569 }
570 hash
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn test_to_snake_case() {
579 assert_eq!(to_snake_case("Player"), "player");
580 assert_eq!(to_snake_case("PlayerHealth"), "player_health");
581 assert_eq!(to_snake_case("HTTPServer"), "httpserver");
583 assert_eq!(to_snake_case("ID"), "id");
584 assert_eq!(to_snake_case("Position"), "position");
586 assert_eq!(to_snake_case("Health"), "health");
587 assert_eq!(to_snake_case("EnemySpawner"), "enemy_spawner");
588 }
589
590 #[test]
591 fn test_to_snake_case_edge_cases() {
592 assert_eq!(to_snake_case(""), "");
593 assert_eq!(to_snake_case("A"), "a");
594 assert_eq!(to_snake_case("AB"), "ab");
595 assert_eq!(to_snake_case("abc"), "abc");
596 assert_eq!(to_snake_case("ABc"), "abc");
597 assert_eq!(to_snake_case("AbC"), "ab_c");
598 }
599
600 #[test]
601 fn test_generate_type_id() {
602 let id1 = generate_type_id("player");
603 let id2 = generate_type_id("enemy");
604 let id3 = generate_type_id("player");
605
606 assert_ne!(id1, id2);
607 assert_eq!(id1, id3); }
609
610 #[test]
611 fn test_generate_type_id_deterministic() {
612 let expected = generate_type_id("test_component");
614 for _ in 0..100 {
615 assert_eq!(generate_type_id("test_component"), expected);
616 }
617 }
618
619 #[test]
620 fn test_generate_type_id_empty() {
621 let id = generate_type_id("");
622 assert_ne!(id, 0);
623 }
624
625 #[test]
626 fn test_generate_type_id_unicode() {
627 let id1 = generate_type_id("プレイヤー");
628 let id2 = generate_type_id("敵");
629 assert_ne!(id1, id2);
630 }
631
632 #[test]
633 fn test_extract_list_items() {
634 let tokens = "probar(entities = [Player, Enemy])";
635 let items = extract_list_items(tokens, 0);
636 assert_eq!(items, vec!["Player", "Enemy"]);
637 }
638
639 #[test]
640 fn test_extract_list_items_with_offset() {
641 let tokens = "probar(entities = [Player], components = [Position, Health])";
642 let offset = tokens.find(']').map(|i| i + 1).unwrap_or(0);
643 let items = extract_list_items(tokens, offset);
644 assert_eq!(items, vec!["Position", "Health"]);
645 }
646
647 #[test]
648 fn test_extract_list_items_empty() {
649 let tokens = "probar(entities = [])";
650 let items = extract_list_items(tokens, 0);
651 assert!(items.is_empty());
652 }
653
654 #[test]
655 fn test_extract_list_items_no_brackets() {
656 let tokens = "probar(name = \"test\")";
657 let items = extract_list_items(tokens, 0);
658 assert!(items.is_empty());
659 }
660
661 #[test]
662 fn test_extract_list_items_single() {
663 let tokens = "probar(entities = [Player])";
664 let items = extract_list_items(tokens, 0);
665 assert_eq!(items, vec!["Player"]);
666 }
667
668 #[test]
669 fn test_extract_list_items_whitespace() {
670 let tokens = "probar(entities = [ Player , Enemy , Boss ])";
671 let items = extract_list_items(tokens, 0);
672 assert_eq!(items, vec!["Player", "Enemy", "Boss"]);
673 }
674
675 #[test]
676 fn test_to_snake_case_numbers() {
677 assert_eq!(to_snake_case("Test123"), "test123");
678 assert_eq!(to_snake_case("Item2D"), "item2_d");
679 }
680
681 #[test]
682 fn test_to_snake_case_underscores() {
683 assert_eq!(to_snake_case("already_snake"), "already_snake");
684 assert_eq!(to_snake_case("Mixed_Case"), "mixed__case");
685 }
686
687 #[test]
688 fn test_generate_type_id_special_chars() {
689 let id1 = generate_type_id("test-name");
690 let id2 = generate_type_id("test_name");
691 let id3 = generate_type_id("test.name");
692 assert_ne!(id1, id2);
694 assert_ne!(id2, id3);
695 assert_ne!(id1, id3);
696 }
697
698 #[test]
699 fn test_extract_list_items_complex() {
700 let tokens = "probar(entities = [A, B, C], other = value)";
701 let items = extract_list_items(tokens, 0);
702 assert_eq!(items, vec!["A", "B", "C"]);
703 }
704
705 #[test]
706 fn test_extract_list_items_nested_offset() {
707 let tokens = "first = [X], second = [Y, Z]";
708 let offset = tokens.find("second").unwrap_or(0);
709 let items = extract_list_items(tokens, offset);
710 assert_eq!(items, vec!["Y", "Z"]);
711 }
712
713 #[test]
714 fn test_parse_timeout_attr_with_timeout() {
715 let result = parse_timeout_attr_from_str("timeout_ms = 5000");
717 assert_eq!(result, Some(5000));
718 }
719
720 #[test]
721 fn test_parse_timeout_attr_no_timeout() {
722 let result = parse_timeout_attr_from_str("category = \"test\"");
723 assert_eq!(result, None);
724 }
725
726 #[test]
727 fn test_parse_timeout_attr_empty() {
728 let result = parse_timeout_attr_from_str("");
729 assert_eq!(result, None);
730 }
731
732 fn parse_timeout_attr_from_str(attr_str: &str) -> Option<u64> {
734 if attr_str.contains("timeout_ms") {
735 for part in attr_str.split('=') {
736 if let Ok(n) = part.trim().parse::<u64>() {
737 return Some(n);
738 }
739 }
740 }
741 None
742 }
743
744 #[test]
745 fn test_extract_fields_unit_struct() {
746 let data = syn::parse_quote! {
747 struct Unit;
748 };
749 let input: DeriveInput = data;
750 let fields = extract_fields(&input.data);
751 assert!(fields.is_empty());
752 }
753
754 #[test]
755 fn test_extract_fields_named_struct() {
756 let data = syn::parse_quote! {
757 struct Named {
758 x: f32,
759 y: f32,
760 }
761 };
762 let input: DeriveInput = data;
763 let fields = extract_fields(&input.data);
764 assert_eq!(fields.len(), 2);
765 assert_eq!(fields[0], ("x".to_string(), false));
766 assert_eq!(fields[1], ("y".to_string(), false));
767 }
768
769 #[test]
770 fn test_extract_fields_tuple_struct() {
771 let data = syn::parse_quote! {
772 struct Tuple(u32, u32, u32);
773 };
774 let input: DeriveInput = data;
775 let fields = extract_fields(&input.data);
776 assert_eq!(fields.len(), 3);
777 assert_eq!(fields[0].0, "field_0");
778 assert_eq!(fields[1].0, "field_1");
779 assert_eq!(fields[2].0, "field_2");
780 }
781
782 #[test]
783 fn test_extract_fields_enum() {
784 let data = syn::parse_quote! {
785 enum TestEnum {
786 A,
787 B,
788 }
789 };
790 let input: DeriveInput = data;
791 let fields = extract_fields(&input.data);
792 assert!(fields.is_empty()); }
794
795 #[test]
796 fn test_extract_name_from_attr_valid() {
797 let attr: Attribute = syn::parse_quote! {
798 #[probar(name = "custom_name")]
799 };
800 let result = extract_name_from_attr(&attr);
801 assert_eq!(result, Some("custom_name".to_string()));
802 }
803
804 #[test]
805 fn test_extract_name_from_attr_wrong_path() {
806 let attr: Attribute = syn::parse_quote! {
807 #[other(name = "custom_name")]
808 };
809 let result = extract_name_from_attr(&attr);
810 assert_eq!(result, None);
811 }
812
813 #[test]
814 fn test_extract_name_from_attr_wrong_key() {
815 let attr: Attribute = syn::parse_quote! {
816 #[probar(other = "value")]
817 };
818 let result = extract_name_from_attr(&attr);
819 assert_eq!(result, None);
820 }
821
822 #[test]
823 fn test_parse_selector_attributes_entities_only() {
824 let attrs: Vec<Attribute> =
825 vec![syn::parse_quote! { #[probar(entities = [Player, Enemy])] }];
826 let (entities, components) = parse_selector_attributes(&attrs);
827 assert_eq!(entities, vec!["Player", "Enemy"]);
828 assert!(components.is_empty());
829 }
830
831 #[test]
832 fn test_parse_selector_attributes_components_only() {
833 let attrs: Vec<Attribute> =
834 vec![syn::parse_quote! { #[probar(components = [Position, Health])] }];
835 let (entities, components) = parse_selector_attributes(&attrs);
836 assert!(entities.is_empty());
837 assert_eq!(components, vec!["Position", "Health"]);
838 }
839
840 #[test]
841 fn test_parse_selector_attributes_empty() {
842 let attrs: Vec<Attribute> = vec![];
843 let (entities, components) = parse_selector_attributes(&attrs);
844 assert!(entities.is_empty());
845 assert!(components.is_empty());
846 }
847
848 #[test]
849 fn test_parse_selector_attributes_non_probar() {
850 let attrs: Vec<Attribute> = vec![
851 syn::parse_quote! { #[derive(Debug)] },
852 syn::parse_quote! { #[allow(unused)] },
853 ];
854 let (entities, components) = parse_selector_attributes(&attrs);
855 assert!(entities.is_empty());
856 assert!(components.is_empty());
857 }
858
859 #[test]
860 fn test_extract_name_attribute_multiple() {
861 let attrs: Vec<Attribute> = vec![
862 syn::parse_quote! { #[derive(Debug)] },
863 syn::parse_quote! { #[probar(name = "found_name")] },
864 syn::parse_quote! { #[allow(unused)] },
865 ];
866 let result = extract_name_attribute(&attrs);
867 assert_eq!(result, Some("found_name".to_string()));
868 }
869
870 #[test]
871 fn test_extract_name_attribute_none() {
872 let attrs: Vec<Attribute> = vec![syn::parse_quote! { #[derive(Debug)] }];
873 let result = extract_name_attribute(&attrs);
874 assert_eq!(result, None);
875 }
876
877 #[test]
878 fn test_extract_name_from_attr_non_string_value() {
879 let attr: Attribute = syn::parse_quote! {
881 #[probar(name = 123)]
882 };
883 let result = extract_name_from_attr(&attr);
884 assert_eq!(result, None);
885 }
886
887 #[test]
888 fn test_extract_fields_with_skip() {
889 let data = syn::parse_quote! {
890 struct WithSkip {
891 x: f32,
892 #[probar(skip)]
893 internal: u32,
894 y: f32,
895 }
896 };
897 let input: DeriveInput = data;
898 let fields = extract_fields(&input.data);
899 assert_eq!(fields.len(), 3);
900 assert_eq!(fields[0], ("x".to_string(), false));
901 assert_eq!(fields[1], ("internal".to_string(), true)); assert_eq!(fields[2], ("y".to_string(), false));
903 }
904
905 #[test]
906 fn test_parse_selector_attributes_separate_attrs() {
907 let attrs: Vec<Attribute> = vec![
908 syn::parse_quote! { #[probar(entities = [Player])] },
909 syn::parse_quote! { #[probar(components = [Position])] },
910 ];
911 let (entities, components) = parse_selector_attributes(&attrs);
912 assert_eq!(entities, vec!["Player"]);
913 assert_eq!(components, vec!["Position"]);
914 }
915
916 #[test]
917 fn test_parse_timeout_attr_various_formats() {
918 assert_eq!(parse_timeout_attr_from_str("timeout_ms=1000"), Some(1000));
920 assert_eq!(parse_timeout_attr_from_str("timeout_ms =2000"), Some(2000));
921 assert_eq!(parse_timeout_attr_from_str("timeout_ms= 3000"), Some(3000));
922 }
923
924 #[test]
925 fn test_parse_timeout_attr_with_other_attrs() {
926 let result = parse_timeout_attr_from_str("category = \"test\", timeout_ms = 7500");
927 assert_eq!(result, Some(7500));
928 }
929
930 #[test]
931 fn test_extract_list_items_malformed() {
932 let tokens = "probar(entities = [A, B";
934 let items = extract_list_items(tokens, 0);
935 assert!(items.is_empty());
936 }
937
938 #[test]
939 fn test_extract_list_items_reversed_brackets() {
940 let tokens = "probar(entities = [])";
943 let items = extract_list_items(tokens, 0);
944 assert!(items.is_empty());
945 }
946
947 #[test]
948 fn test_to_snake_case_all_uppercase() {
949 assert_eq!(to_snake_case("ABC"), "abc");
950 assert_eq!(to_snake_case("ABCDEF"), "abcdef");
951 }
952
953 #[test]
954 fn test_to_snake_case_single_char_words() {
955 assert_eq!(to_snake_case("AaBbCc"), "aa_bb_cc");
956 }
957
958 #[test]
959 fn test_generate_type_id_long_string() {
960 let long_name = "a".repeat(1000);
961 let id = generate_type_id(&long_name);
962 assert_ne!(id, 0);
963 assert_eq!(id, generate_type_id(&long_name));
965 }
966
967 #[test]
968 fn test_extract_name_from_attr_list_style() {
969 let attr: Attribute = syn::parse_quote! {
971 #[probar(skip)]
972 };
973 let result = extract_name_from_attr(&attr);
974 assert_eq!(result, None);
975 }
976
977 #[test]
978 fn test_extract_fields_empty_named() {
979 let data = syn::parse_quote! {
980 struct Empty {}
981 };
982 let input: DeriveInput = data;
983 let fields = extract_fields(&input.data);
984 assert!(fields.is_empty());
985 }
986
987 #[test]
988 fn test_extract_fields_single_field() {
989 let data = syn::parse_quote! {
990 struct Single {
991 value: i32,
992 }
993 };
994 let input: DeriveInput = data;
995 let fields = extract_fields(&input.data);
996 assert_eq!(fields.len(), 1);
997 assert_eq!(fields[0].0, "value");
998 }
999
1000 #[test]
1001 fn test_extract_fields_tuple_single() {
1002 let data = syn::parse_quote! {
1003 struct Wrapper(String);
1004 };
1005 let input: DeriveInput = data;
1006 let fields = extract_fields(&input.data);
1007 assert_eq!(fields.len(), 1);
1008 assert_eq!(fields[0].0, "field_0");
1009 }
1010
1011 #[test]
1012 fn test_parse_selector_attributes_mixed_attrs() {
1013 let attrs: Vec<Attribute> = vec![
1015 syn::parse_quote! { #[derive(Debug)] },
1016 syn::parse_quote! { #[probar(entities = [A, B])] },
1017 syn::parse_quote! { #[serde(rename_all = "camelCase")] },
1018 syn::parse_quote! { #[probar(components = [X, Y, Z])] },
1019 ];
1020 let (entities, components) = parse_selector_attributes(&attrs);
1021 assert_eq!(entities, vec!["A", "B"]);
1022 assert_eq!(components, vec!["X", "Y", "Z"]);
1023 }
1024
1025 #[test]
1026 fn test_extract_name_attribute_empty_list() {
1027 let attrs: Vec<Attribute> = vec![];
1028 let result = extract_name_attribute(&attrs);
1029 assert_eq!(result, None);
1030 }
1031
1032 #[test]
1033 fn test_generate_type_id_collision_resistance() {
1034 let id_player = generate_type_id("player");
1036 let id_player1 = generate_type_id("player1");
1037 let id_players = generate_type_id("players");
1038 let id_player_caps = generate_type_id("Player");
1039
1040 assert_ne!(id_player, id_player1);
1041 assert_ne!(id_player, id_players);
1042 assert_ne!(id_player, id_player_caps); }
1044}