1mod columns;
70mod inspect;
71mod panel;
72mod renderable;
73mod rule;
74mod shared;
75mod table;
76mod tree;
77
78use proc_macro::TokenStream;
79#[cfg(test)]
80use proc_macro2::Span;
81use syn::{parse_macro_input, DeriveInput};
82
83#[proc_macro_derive(Tree, attributes(tree))]
90pub fn derive_tree(input: TokenStream) -> TokenStream {
91 let input = parse_macro_input!(input as DeriveInput);
92 match tree::derive_tree_impl(&input) {
93 Ok(ts) => ts.into(),
94 Err(e) => e.to_compile_error().into(),
95 }
96}
97
98#[proc_macro_derive(Renderable, attributes(renderable))]
107pub fn derive_renderable(input: TokenStream) -> TokenStream {
108 let input = parse_macro_input!(input as DeriveInput);
109 match renderable::derive_renderable_impl(&input) {
110 Ok(ts) => ts.into(),
111 Err(e) => e.to_compile_error().into(),
112 }
113}
114
115#[proc_macro_derive(Inspect, attributes(inspect))]
123pub fn derive_inspect(input: TokenStream) -> TokenStream {
124 let input = parse_macro_input!(input as DeriveInput);
125 match inspect::derive_inspect_impl(&input) {
126 Ok(ts) => ts.into(),
127 Err(e) => e.to_compile_error().into(),
128 }
129}
130
131#[proc_macro_derive(Rule, attributes(rule))]
138pub fn derive_rule(input: TokenStream) -> TokenStream {
139 let input = parse_macro_input!(input as DeriveInput);
140 match rule::derive_rule_impl(&input) {
141 Ok(ts) => ts.into(),
142 Err(e) => e.to_compile_error().into(),
143 }
144}
145
146#[proc_macro_derive(Columns, attributes(columns, field))]
153pub fn derive_columns(input: TokenStream) -> TokenStream {
154 let input = parse_macro_input!(input as DeriveInput);
155 match columns::derive_columns_impl(&input) {
156 Ok(ts) => ts.into(),
157 Err(e) => e.to_compile_error().into(),
158 }
159}
160
161#[proc_macro_derive(Panel, attributes(panel, field))]
168pub fn derive_panel(input: TokenStream) -> TokenStream {
169 let input = parse_macro_input!(input as DeriveInput);
170 match panel::derive_panel_impl(&input) {
171 Ok(ts) => ts.into(),
172 Err(e) => e.to_compile_error().into(),
173 }
174}
175
176#[proc_macro_derive(Table, attributes(table, column))]
183pub fn derive_table(input: TokenStream) -> TokenStream {
184 let input = parse_macro_input!(input as DeriveInput);
185 match table::derive_table_impl(&input) {
186 Ok(ts) => ts.into(),
187 Err(e) => e.to_compile_error().into(),
188 }
189}
190
191#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::shared::{box_style_tokens, justify_tokens, snake_to_title_case};
199 use syn::LitStr;
200
201 #[test]
204 fn test_snake_to_title_case() {
205 assert_eq!(snake_to_title_case("first_name"), "First Name");
206 assert_eq!(snake_to_title_case("age"), "Age");
207 assert_eq!(snake_to_title_case("department_id"), "Department Id");
208 assert_eq!(snake_to_title_case("a_b_c"), "A B C");
209 assert_eq!(snake_to_title_case("single"), "Single");
210 }
211
212 #[test]
213 fn test_snake_to_title_case_edge_cases() {
214 assert_eq!(snake_to_title_case(""), "");
215 assert_eq!(snake_to_title_case("_leading"), "Leading");
216 assert_eq!(snake_to_title_case("trailing_"), "Trailing");
217 assert_eq!(snake_to_title_case("__double__"), "Double");
218 assert_eq!(snake_to_title_case("ALL_CAPS"), "ALL CAPS");
219 }
220
221 #[test]
224 fn test_box_style_tokens_valid() {
225 let valid = [
226 "ASCII",
227 "ASCII2",
228 "ASCII_DOUBLE_HEAD",
229 "SQUARE",
230 "SQUARE_DOUBLE_HEAD",
231 "MINIMAL",
232 "MINIMAL_HEAVY_HEAD",
233 "MINIMAL_DOUBLE_HEAD",
234 "SIMPLE",
235 "SIMPLE_HEAD",
236 "SIMPLE_HEAVY",
237 "HORIZONTALS",
238 "ROUNDED",
239 "HEAVY",
240 "HEAVY_EDGE",
241 "HEAVY_HEAD",
242 "DOUBLE",
243 "DOUBLE_EDGE",
244 "MARKDOWN",
245 ];
246 for name in valid {
247 let lit = LitStr::new(name, Span::call_site());
248 assert!(
249 box_style_tokens(&lit).is_ok(),
250 "box_style_tokens should accept `{}`",
251 name
252 );
253 }
254 }
255
256 #[test]
257 fn test_box_style_tokens_invalid() {
258 let lit = LitStr::new("NONEXISTENT", Span::call_site());
259 let result = box_style_tokens(&lit);
260 assert!(result.is_err());
261 let err_msg = result.unwrap_err().to_string();
262 assert!(
263 err_msg.contains("unknown box_style"),
264 "error should mention unknown box_style, got: {}",
265 err_msg
266 );
267 }
268
269 #[test]
272 fn test_justify_tokens_valid() {
273 for name in ["left", "center", "right", "full"] {
274 let lit = LitStr::new(name, Span::call_site());
275 assert!(
276 justify_tokens(&lit).is_ok(),
277 "justify_tokens should accept `{}`",
278 name
279 );
280 }
281 }
282
283 #[test]
284 fn test_justify_tokens_invalid() {
285 let lit = LitStr::new("middle", Span::call_site());
286 let result = justify_tokens(&lit);
287 assert!(result.is_err());
288 let err_msg = result.unwrap_err().to_string();
289 assert!(
290 err_msg.contains("unknown justify"),
291 "error should mention unknown justify, got: {}",
292 err_msg
293 );
294 }
295
296 #[test]
299 fn test_parse_table_attr_str() {
300 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = "My Title" };
301 let attr: table::TableAttr = syn::parse2(tokens).unwrap();
302 assert_eq!(attr.key, "title");
303 match attr.value {
304 table::TableAttrValue::Str(s) => assert_eq!(s.value(), "My Title"),
305 _ => panic!("expected Str"),
306 }
307 }
308
309 #[test]
310 fn test_parse_table_attr_bool() {
311 let tokens: proc_macro2::TokenStream = syn::parse_quote! { expand = true };
312 let attr: table::TableAttr = syn::parse2(tokens).unwrap();
313 assert_eq!(attr.key, "expand");
314 match attr.value {
315 table::TableAttrValue::Bool(b) => assert!(b.value),
316 _ => panic!("expected Bool"),
317 }
318 }
319
320 #[test]
321 fn test_parse_table_attr_flag() {
322 let tokens: proc_macro2::TokenStream = syn::parse_quote! { expand };
323 let attr: table::TableAttr = syn::parse2(tokens).unwrap();
324 assert_eq!(attr.key, "expand");
325 matches!(attr.value, table::TableAttrValue::Flag);
326 }
327
328 #[test]
331 fn test_parse_column_attr_str() {
332 let tokens: proc_macro2::TokenStream = syn::parse_quote! { header = "Name" };
333 let attr: table::ColumnAttr = syn::parse2(tokens).unwrap();
334 assert_eq!(attr.key, "header");
335 match attr.value {
336 table::ColumnAttrValue::Str(s) => assert_eq!(s.value(), "Name"),
337 _ => panic!("expected Str"),
338 }
339 }
340
341 #[test]
342 fn test_parse_column_attr_int() {
343 let tokens: proc_macro2::TokenStream = syn::parse_quote! { width = 42 };
344 let attr: table::ColumnAttr = syn::parse2(tokens).unwrap();
345 assert_eq!(attr.key, "width");
346 match attr.value {
347 table::ColumnAttrValue::Int(i) => assert_eq!(i.base10_parse::<usize>().unwrap(), 42),
348 _ => panic!("expected Int"),
349 }
350 }
351
352 #[test]
353 fn test_parse_column_attr_bool() {
354 let tokens: proc_macro2::TokenStream = syn::parse_quote! { no_wrap = true };
355 let attr: table::ColumnAttr = syn::parse2(tokens).unwrap();
356 assert_eq!(attr.key, "no_wrap");
357 match attr.value {
358 table::ColumnAttrValue::Bool(b) => assert!(b.value),
359 _ => panic!("expected Bool"),
360 }
361 }
362
363 #[test]
364 fn test_parse_column_attr_flag() {
365 let tokens: proc_macro2::TokenStream = syn::parse_quote! { skip };
366 let attr: table::ColumnAttr = syn::parse2(tokens).unwrap();
367 assert_eq!(attr.key, "skip");
368 matches!(attr.value, table::ColumnAttrValue::Flag);
369 }
370
371 #[test]
374 fn test_expect_str_ok() {
375 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = "hello" };
376 let attr: table::TableAttr = syn::parse2(tokens).unwrap();
377 let result = table::expect_str(&attr, "title");
378 assert!(result.is_ok());
379 assert_eq!(result.unwrap().value(), "hello");
380 }
381
382 #[test]
383 fn test_expect_str_wrong_type() {
384 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = true };
385 let attr: table::TableAttr = syn::parse2(tokens).unwrap();
386 let result = table::expect_str(&attr, "title");
387 assert!(result.is_err());
388 }
389
390 #[test]
391 fn test_expect_bool_ok() {
392 let tokens: proc_macro2::TokenStream = syn::parse_quote! { expand = false };
393 let attr: table::TableAttr = syn::parse2(tokens).unwrap();
394 let result = table::expect_bool(&attr, "expand");
395 assert!(result.is_ok());
396 assert!(!result.unwrap().value);
397 }
398
399 #[test]
400 fn test_expect_bool_flag() {
401 let tokens: proc_macro2::TokenStream = syn::parse_quote! { expand };
402 let attr: table::TableAttr = syn::parse2(tokens).unwrap();
403 let result = table::expect_bool(&attr, "expand");
404 assert!(result.is_ok());
405 assert!(result.unwrap().value);
406 }
407
408 #[test]
409 fn test_col_expect_int_ok() {
410 let tokens: proc_macro2::TokenStream = syn::parse_quote! { width = 10 };
411 let attr: table::ColumnAttr = syn::parse2(tokens).unwrap();
412 let result = table::col_expect_int(&attr, "width");
413 assert!(result.is_ok());
414 assert_eq!(result.unwrap().base10_parse::<usize>().unwrap(), 10);
415 }
416
417 #[test]
418 fn test_col_expect_int_wrong_type() {
419 let tokens: proc_macro2::TokenStream = syn::parse_quote! { width = "ten" };
420 let attr: table::ColumnAttr = syn::parse2(tokens).unwrap();
421 let result = table::col_expect_int(&attr, "width");
422 assert!(result.is_err());
423 }
424
425 #[test]
428 fn test_derive_basic_struct() {
429 let input: DeriveInput = syn::parse_quote! {
430 struct Employee {
431 name: String,
432 age: u32,
433 }
434 };
435 let result = table::derive_table_impl(&input);
436 assert!(
437 result.is_ok(),
438 "derive_table_impl failed: {:?}",
439 result.err()
440 );
441 let tokens = result.unwrap().to_string();
442 assert!(tokens.contains("to_table"));
443 assert!(tokens.contains("\"Name\""));
444 assert!(tokens.contains("\"Age\""));
445 }
446
447 #[test]
448 fn test_derive_with_skip() {
449 let input: DeriveInput = syn::parse_quote! {
450 struct Data {
451 visible: String,
452 #[column(skip)]
453 hidden: u64,
454 also_visible: i32,
455 }
456 };
457 let result = table::derive_table_impl(&input);
458 assert!(result.is_ok());
459 let tokens = result.unwrap().to_string();
460 assert!(tokens.contains("\"Visible\""));
461 assert!(tokens.contains("\"Also Visible\""));
462 assert!(!tokens.contains("\"Hidden\""));
464 }
465
466 #[test]
467 fn test_derive_with_custom_header() {
468 let input: DeriveInput = syn::parse_quote! {
469 struct Rec {
470 #[column(header = "Full Name")]
471 name: String,
472 }
473 };
474 let result = table::derive_table_impl(&input);
475 assert!(result.is_ok());
476 let tokens = result.unwrap().to_string();
477 assert!(tokens.contains("\"Full Name\""));
478 }
479
480 #[test]
481 fn test_derive_with_table_attrs() {
482 let input: DeriveInput = syn::parse_quote! {
483 #[table(title = "My Table", box_style = "ROUNDED", show_lines = true)]
484 struct Rec {
485 a: String,
486 }
487 };
488 let result = table::derive_table_impl(&input);
489 assert!(result.is_ok());
490 let tokens = result.unwrap().to_string();
491 assert!(tokens.contains("\"My Table\""));
492 assert!(tokens.contains("ROUNDED"));
493 assert!(tokens.contains("show_lines"));
494 }
495
496 #[test]
497 fn test_derive_with_column_justify() {
498 let input: DeriveInput = syn::parse_quote! {
499 struct Rec {
500 #[column(justify = "right")]
501 amount: f64,
502 }
503 };
504 let result = table::derive_table_impl(&input);
505 assert!(result.is_ok());
506 let tokens = result.unwrap().to_string();
507 assert!(tokens.contains("JustifyMethod"));
508 assert!(tokens.contains("Right"));
509 }
510
511 #[test]
512 fn test_derive_rejects_enum() {
513 let input: DeriveInput = syn::parse_quote! {
514 enum Foo { A, B }
515 };
516 let result = table::derive_table_impl(&input);
517 assert!(result.is_err());
518 assert!(result
519 .unwrap_err()
520 .to_string()
521 .contains("does not support enums"));
522 }
523
524 #[test]
525 fn test_derive_rejects_unknown_table_attr() {
526 let input: DeriveInput = syn::parse_quote! {
527 #[table(nonexistent = "value")]
528 struct Rec {
529 a: String,
530 }
531 };
532 let result = table::derive_table_impl(&input);
533 assert!(result.is_err());
534 assert!(result
535 .unwrap_err()
536 .to_string()
537 .contains("unknown table attribute"),);
538 }
539
540 #[test]
541 fn test_derive_rejects_unknown_column_attr() {
542 let input: DeriveInput = syn::parse_quote! {
543 struct Rec {
544 #[column(nonexistent = "value")]
545 a: String,
546 }
547 };
548 let result = table::derive_table_impl(&input);
549 assert!(result.is_err());
550 assert!(result
551 .unwrap_err()
552 .to_string()
553 .contains("unknown column attribute"),);
554 }
555
556 #[test]
557 fn test_derive_rejects_invalid_justify() {
558 let input: DeriveInput = syn::parse_quote! {
559 struct Rec {
560 #[column(justify = "middle")]
561 a: String,
562 }
563 };
564 let result = table::derive_table_impl(&input);
565 assert!(result.is_err());
566 assert!(result.unwrap_err().to_string().contains("unknown justify"));
567 }
568
569 #[test]
570 fn test_derive_rejects_invalid_box_style() {
571 let input: DeriveInput = syn::parse_quote! {
572 #[table(box_style = "FANCY")]
573 struct Rec {
574 a: String,
575 }
576 };
577 let result = table::derive_table_impl(&input);
578 assert!(result.is_err());
579 assert!(result
580 .unwrap_err()
581 .to_string()
582 .contains("unknown box_style"));
583 }
584
585 #[test]
586 fn test_derive_row_styles() {
587 let input: DeriveInput = syn::parse_quote! {
588 #[table(row_styles = "bold, dim")]
589 struct Rec {
590 a: String,
591 }
592 };
593 let result = table::derive_table_impl(&input);
594 assert!(result.is_ok());
595 let tokens = result.unwrap().to_string();
596 assert!(tokens.contains("row_styles"));
597 assert!(tokens.contains("\"bold\""));
598 assert!(tokens.contains("\"dim\""));
599 }
600
601 #[test]
602 fn test_derive_column_width_attrs() {
603 let input: DeriveInput = syn::parse_quote! {
604 struct Rec {
605 #[column(width = 20, min_width = 5, max_width = 50, ratio = 2)]
606 a: String,
607 }
608 };
609 let result = table::derive_table_impl(&input);
610 assert!(result.is_ok());
611 let tokens = result.unwrap().to_string();
612 assert!(tokens.contains("width"));
613 assert!(tokens.contains("min_width"));
614 assert!(tokens.contains("max_width"));
615 assert!(tokens.contains("ratio"));
616 }
617
618 #[test]
619 fn test_derive_expand_flag() {
620 let input: DeriveInput = syn::parse_quote! {
621 #[table(expand)]
622 struct Rec {
623 a: String,
624 }
625 };
626 let result = table::derive_table_impl(&input);
627 assert!(result.is_ok());
628 let tokens = result.unwrap().to_string();
629 assert!(tokens.contains("set_expand"));
630 }
631
632 #[test]
635 fn test_derive_panel_basic() {
636 let input: DeriveInput = syn::parse_quote! {
637 struct Server {
638 name: String,
639 cpu: f32,
640 }
641 };
642 let result = panel::derive_panel_impl(&input);
643 assert!(
644 result.is_ok(),
645 "derive_panel_impl failed: {:?}",
646 result.err()
647 );
648 let tokens = result.unwrap().to_string();
649 assert!(
650 tokens.contains("to_panel"),
651 "should generate to_panel method"
652 );
653 assert!(
654 tokens.contains("\"Name\""),
655 "should contain default label 'Name'"
656 );
657 assert!(
658 tokens.contains("\"Cpu\""),
659 "should contain default label 'Cpu'"
660 );
661 assert!(tokens.contains("Panel"), "should reference Panel type");
662 assert!(
663 tokens.contains("from_markup"),
664 "should use from_markup for content"
665 );
666 assert!(
668 tokens.contains("\"Server\""),
669 "default title should be struct name"
670 );
671 }
672
673 #[test]
674 fn test_derive_panel_with_attrs() {
675 let input: DeriveInput = syn::parse_quote! {
676 #[panel(
677 title = "Server Status",
678 subtitle = "Last updated",
679 box_style = "HEAVY",
680 border_style = "blue",
681 style = "white",
682 expand = false,
683 highlight = true
684 )]
685 struct Server {
686 #[field(label = "Host", style = "bold cyan")]
687 name: String,
688 cpu: f32,
689 }
690 };
691 let result = panel::derive_panel_impl(&input);
692 assert!(
693 result.is_ok(),
694 "derive_panel_impl failed: {:?}",
695 result.err()
696 );
697 let tokens = result.unwrap().to_string();
698 assert!(
699 tokens.contains("\"Server Status\""),
700 "should contain custom title"
701 );
702 assert!(
703 tokens.contains("\"Last updated\""),
704 "should contain subtitle"
705 );
706 assert!(tokens.contains("HEAVY"), "should reference HEAVY box style");
707 assert!(tokens.contains("\"blue\""), "should contain border_style");
708 assert!(tokens.contains("\"white\""), "should contain style");
709 assert!(tokens.contains("expand"), "should set expand");
710 assert!(tokens.contains("highlight"), "should set highlight");
711 assert!(tokens.contains("\"Host\""), "should use custom label");
712 assert!(
713 tokens.contains("bold cyan"),
714 "should contain field style markup"
715 );
716 }
717
718 #[test]
719 fn test_derive_panel_skip_field() {
720 let input: DeriveInput = syn::parse_quote! {
721 struct Data {
722 visible: String,
723 #[field(skip)]
724 hidden: u64,
725 also_visible: i32,
726 }
727 };
728 let result = panel::derive_panel_impl(&input);
729 assert!(result.is_ok());
730 let tokens = result.unwrap().to_string();
731 assert!(
732 tokens.contains("\"Visible\""),
733 "should include visible field"
734 );
735 assert!(
736 tokens.contains("\"Also Visible\""),
737 "should include also_visible field"
738 );
739 assert!(!tokens.contains("\"Hidden\""), "should skip hidden field");
741 assert!(
743 !tokens.contains("hidden"),
744 "hidden field ident should not appear"
745 );
746 }
747
748 #[test]
749 fn test_derive_panel_custom_labels() {
750 let input: DeriveInput = syn::parse_quote! {
751 struct Status {
752 #[field(label = "Host Name")]
753 server_name: String,
754 #[field(label = "CPU %", style = "yellow")]
755 cpu_usage: f32,
756 #[field(label = "Mem (GB)")]
757 memory_gb: f64,
758 }
759 };
760 let result = panel::derive_panel_impl(&input);
761 assert!(result.is_ok());
762 let tokens = result.unwrap().to_string();
763 assert!(
764 tokens.contains("\"Host Name\""),
765 "should use custom label 'Host Name'"
766 );
767 assert!(
768 tokens.contains("\"CPU %\""),
769 "should use custom label 'CPU %'"
770 );
771 assert!(
772 tokens.contains("\"Mem (GB)\""),
773 "should use custom label 'Mem (GB)'"
774 );
775 assert!(
777 !tokens.contains("\"Server Name\""),
778 "should not use default label"
779 );
780 assert!(
781 !tokens.contains("\"Cpu Usage\""),
782 "should not use default label"
783 );
784 assert!(
785 !tokens.contains("\"Memory Gb\""),
786 "should not use default label"
787 );
788 }
789
790 #[test]
791 fn test_derive_panel_rejects_enum() {
792 let input: DeriveInput = syn::parse_quote! {
793 enum Foo { A, B }
794 };
795 let result = panel::derive_panel_impl(&input);
796 assert!(result.is_err());
797 assert!(result
798 .unwrap_err()
799 .to_string()
800 .contains("does not support enums"));
801 }
802
803 #[test]
804 fn test_derive_panel_rejects_unknown_panel_attr() {
805 let input: DeriveInput = syn::parse_quote! {
806 #[panel(nonexistent = "value")]
807 struct Rec {
808 a: String,
809 }
810 };
811 let result = panel::derive_panel_impl(&input);
812 assert!(result.is_err());
813 assert!(result
814 .unwrap_err()
815 .to_string()
816 .contains("unknown panel attribute"),);
817 }
818
819 #[test]
820 fn test_derive_panel_rejects_unknown_field_attr() {
821 let input: DeriveInput = syn::parse_quote! {
822 struct Rec {
823 #[field(nonexistent = "value")]
824 a: String,
825 }
826 };
827 let result = panel::derive_panel_impl(&input);
828 assert!(result.is_err());
829 assert!(result
830 .unwrap_err()
831 .to_string()
832 .contains("unknown field attribute"),);
833 }
834
835 #[test]
836 fn test_derive_panel_rejects_invalid_box_style() {
837 let input: DeriveInput = syn::parse_quote! {
838 #[panel(box_style = "FANCY")]
839 struct Rec {
840 a: String,
841 }
842 };
843 let result = panel::derive_panel_impl(&input);
844 assert!(result.is_err());
845 assert!(result
846 .unwrap_err()
847 .to_string()
848 .contains("unknown box_style"));
849 }
850
851 #[test]
852 fn test_derive_panel_title_style() {
853 let input: DeriveInput = syn::parse_quote! {
854 #[panel(title = "Info", title_style = "bold cyan")]
855 struct Info {
856 a: String,
857 }
858 };
859 let result = panel::derive_panel_impl(&input);
860 assert!(result.is_ok());
861 let tokens = result.unwrap().to_string();
862 assert!(
863 tokens.contains("bold cyan"),
864 "should apply title_style as markup"
865 );
866 assert!(tokens.contains("\"Info\""), "should contain title text");
867 }
868
869 #[test]
872 fn test_parse_panel_attr_str() {
873 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = "My Panel" };
874 let attr: panel::PanelAttr = syn::parse2(tokens).unwrap();
875 assert_eq!(attr.key, "title");
876 match attr.value {
877 panel::PanelAttrValue::Str(s) => assert_eq!(s.value(), "My Panel"),
878 _ => panic!("expected Str"),
879 }
880 }
881
882 #[test]
883 fn test_parse_panel_attr_bool() {
884 let tokens: proc_macro2::TokenStream = syn::parse_quote! { expand = false };
885 let attr: panel::PanelAttr = syn::parse2(tokens).unwrap();
886 assert_eq!(attr.key, "expand");
887 match attr.value {
888 panel::PanelAttrValue::Bool(b) => assert!(!b.value),
889 _ => panic!("expected Bool"),
890 }
891 }
892
893 #[test]
894 fn test_parse_panel_attr_flag() {
895 let tokens: proc_macro2::TokenStream = syn::parse_quote! { highlight };
896 let attr: panel::PanelAttr = syn::parse2(tokens).unwrap();
897 assert_eq!(attr.key, "highlight");
898 matches!(attr.value, panel::PanelAttrValue::Flag);
899 }
900
901 #[test]
904 fn test_parse_field_attr_str() {
905 let tokens: proc_macro2::TokenStream = syn::parse_quote! { label = "Host" };
906 let attr: panel::FieldAttr = syn::parse2(tokens).unwrap();
907 assert_eq!(attr.key, "label");
908 match attr.value {
909 panel::FieldAttrValue::Str(s) => assert_eq!(s.value(), "Host"),
910 _ => panic!("expected Str"),
911 }
912 }
913
914 #[test]
915 fn test_parse_field_attr_flag() {
916 let tokens: proc_macro2::TokenStream = syn::parse_quote! { skip };
917 let attr: panel::FieldAttr = syn::parse2(tokens).unwrap();
918 assert_eq!(attr.key, "skip");
919 matches!(attr.value, panel::FieldAttrValue::Flag);
920 }
921
922 #[test]
923 fn test_panel_expect_str_ok() {
924 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = "hello" };
925 let attr: panel::PanelAttr = syn::parse2(tokens).unwrap();
926 let result = panel::panel_expect_str(&attr, "title");
927 assert!(result.is_ok());
928 assert_eq!(result.unwrap().value(), "hello");
929 }
930
931 #[test]
932 fn test_panel_expect_str_wrong_type() {
933 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = true };
934 let attr: panel::PanelAttr = syn::parse2(tokens).unwrap();
935 let result = panel::panel_expect_str(&attr, "title");
936 assert!(result.is_err());
937 }
938
939 #[test]
940 fn test_panel_expect_bool_flag() {
941 let tokens: proc_macro2::TokenStream = syn::parse_quote! { expand };
942 let attr: panel::PanelAttr = syn::parse2(tokens).unwrap();
943 let result = panel::panel_expect_bool(&attr, "expand");
944 assert!(result.is_ok());
945 assert!(result.unwrap().value);
946 }
947
948 #[test]
949 fn test_field_expect_str_ok() {
950 let tokens: proc_macro2::TokenStream = syn::parse_quote! { label = "Name" };
951 let attr: panel::FieldAttr = syn::parse2(tokens).unwrap();
952 let result = panel::field_expect_str(&attr, "label");
953 assert!(result.is_ok());
954 assert_eq!(result.unwrap().value(), "Name");
955 }
956
957 #[test]
958 fn test_field_expect_bool_flag() {
959 let tokens: proc_macro2::TokenStream = syn::parse_quote! { skip };
960 let attr: panel::FieldAttr = syn::parse2(tokens).unwrap();
961 let result = panel::field_expect_bool(&attr, "skip");
962 assert!(result.is_ok());
963 assert!(result.unwrap().value);
964 }
965
966 #[test]
969 fn test_derive_tree_basic() {
970 let input: DeriveInput = syn::parse_quote! {
971 struct FileEntry {
972 #[tree(label)]
973 name: String,
974 #[tree(children)]
975 entries: Vec<FileEntry>,
976 }
977 };
978 let result = tree::derive_tree_impl(&input);
979 assert!(
980 result.is_ok(),
981 "derive_tree_impl failed: {:?}",
982 result.err()
983 );
984 let tokens = result.unwrap().to_string();
985 assert!(tokens.contains("to_tree"), "should generate to_tree method");
986 assert!(tokens.contains("Tree"), "should reference Tree type");
987 assert!(tokens.contains("name"), "should use label field 'name'");
988 assert!(
989 tokens.contains("entries"),
990 "should use children field 'entries'"
991 );
992 assert!(tokens.contains("children"), "should push to children vec");
993 }
994
995 #[test]
996 fn test_derive_tree_with_style() {
997 let input: DeriveInput = syn::parse_quote! {
998 #[tree(style = "bold", guide_style = "dim cyan")]
999 struct FileEntry {
1000 #[tree(label)]
1001 name: String,
1002 #[tree(children)]
1003 entries: Vec<FileEntry>,
1004 }
1005 };
1006 let result = tree::derive_tree_impl(&input);
1007 assert!(
1008 result.is_ok(),
1009 "derive_tree_impl failed: {:?}",
1010 result.err()
1011 );
1012 let tokens = result.unwrap().to_string();
1013 assert!(
1014 tokens.contains("\"bold\""),
1015 "should contain style string 'bold'"
1016 );
1017 assert!(
1018 tokens.contains("\"dim cyan\""),
1019 "should contain guide_style string 'dim cyan'"
1020 );
1021 assert!(
1022 tokens.contains("Style :: parse"),
1023 "should call Style::parse"
1024 );
1025 }
1026
1027 #[test]
1028 fn test_derive_tree_with_leaf() {
1029 let input: DeriveInput = syn::parse_quote! {
1030 struct FileEntry {
1031 #[tree(label)]
1032 name: String,
1033 #[tree(children)]
1034 entries: Vec<FileEntry>,
1035 #[tree(leaf)]
1036 size: u64,
1037 #[tree(leaf)]
1038 permissions: String,
1039 }
1040 };
1041 let result = tree::derive_tree_impl(&input);
1042 assert!(
1043 result.is_ok(),
1044 "derive_tree_impl failed: {:?}",
1045 result.err()
1046 );
1047 let tokens = result.unwrap().to_string();
1048 assert!(tokens.contains("to_tree"), "should generate to_tree method");
1049 assert!(
1050 tokens.contains("\"Size\""),
1051 "should contain leaf label 'Size'"
1052 );
1053 assert!(
1054 tokens.contains("\"Permissions\""),
1055 "should contain leaf label 'Permissions'"
1056 );
1057 assert!(
1058 tokens.contains("self . size"),
1059 "should reference leaf field 'size'"
1060 );
1061 assert!(
1062 tokens.contains("self . permissions"),
1063 "should reference leaf field 'permissions'"
1064 );
1065 }
1066
1067 #[test]
1068 fn test_derive_tree_missing_label() {
1069 let input: DeriveInput = syn::parse_quote! {
1070 struct FileEntry {
1071 name: String,
1072 #[tree(children)]
1073 entries: Vec<FileEntry>,
1074 }
1075 };
1076 let result = tree::derive_tree_impl(&input);
1077 assert!(result.is_err(), "should error when no #[tree(label)] field");
1078 let err_msg = result.unwrap_err().to_string();
1079 assert!(
1080 err_msg.contains("tree(label)"),
1081 "error should mention tree(label), got: {}",
1082 err_msg
1083 );
1084 }
1085
1086 #[test]
1087 fn test_derive_tree_missing_children() {
1088 let input: DeriveInput = syn::parse_quote! {
1089 struct FileEntry {
1090 #[tree(label)]
1091 name: String,
1092 entries: Vec<FileEntry>,
1093 }
1094 };
1095 let result = tree::derive_tree_impl(&input);
1096 assert!(
1097 result.is_err(),
1098 "should error when no #[tree(children)] field"
1099 );
1100 let err_msg = result.unwrap_err().to_string();
1101 assert!(
1102 err_msg.contains("tree(children)"),
1103 "error should mention tree(children), got: {}",
1104 err_msg
1105 );
1106 }
1107
1108 #[test]
1109 fn test_derive_tree_rejects_enum() {
1110 let input: DeriveInput = syn::parse_quote! {
1111 enum Foo { A, B }
1112 };
1113 let result = tree::derive_tree_impl(&input);
1114 assert!(result.is_err());
1115 assert!(result
1116 .unwrap_err()
1117 .to_string()
1118 .contains("does not support enums"));
1119 }
1120
1121 #[test]
1122 fn test_derive_tree_rejects_unknown_tree_attr() {
1123 let input: DeriveInput = syn::parse_quote! {
1124 #[tree(nonexistent = "value")]
1125 struct FileEntry {
1126 #[tree(label)]
1127 name: String,
1128 #[tree(children)]
1129 entries: Vec<FileEntry>,
1130 }
1131 };
1132 let result = tree::derive_tree_impl(&input);
1133 assert!(result.is_err());
1134 assert!(result
1135 .unwrap_err()
1136 .to_string()
1137 .contains("unknown tree attribute"),);
1138 }
1139
1140 #[test]
1141 fn test_derive_tree_rejects_unknown_field_role() {
1142 let input: DeriveInput = syn::parse_quote! {
1143 struct FileEntry {
1144 #[tree(label)]
1145 name: String,
1146 #[tree(children)]
1147 entries: Vec<FileEntry>,
1148 #[tree(bogus)]
1149 size: u64,
1150 }
1151 };
1152 let result = tree::derive_tree_impl(&input);
1153 assert!(result.is_err());
1154 assert!(result
1155 .unwrap_err()
1156 .to_string()
1157 .contains("unknown tree field attribute"),);
1158 }
1159
1160 #[test]
1161 fn test_derive_tree_duplicate_label() {
1162 let input: DeriveInput = syn::parse_quote! {
1163 struct FileEntry {
1164 #[tree(label)]
1165 name: String,
1166 #[tree(label)]
1167 title: String,
1168 #[tree(children)]
1169 entries: Vec<FileEntry>,
1170 }
1171 };
1172 let result = tree::derive_tree_impl(&input);
1173 assert!(result.is_err());
1174 assert!(result.unwrap_err().to_string().contains("only one field"),);
1175 }
1176
1177 #[test]
1178 fn test_derive_tree_ignores_unannotated_fields() {
1179 let input: DeriveInput = syn::parse_quote! {
1180 struct FileEntry {
1181 #[tree(label)]
1182 name: String,
1183 #[tree(children)]
1184 entries: Vec<FileEntry>,
1185 ignored_field: u64,
1186 another_ignored: String,
1187 }
1188 };
1189 let result = tree::derive_tree_impl(&input);
1190 assert!(
1191 result.is_ok(),
1192 "unannotated fields should be silently ignored"
1193 );
1194 let tokens = result.unwrap().to_string();
1195 assert!(
1197 !tokens.contains("ignored_field"),
1198 "ignored_field should not appear"
1199 );
1200 assert!(
1201 !tokens.contains("another_ignored"),
1202 "another_ignored should not appear"
1203 );
1204 }
1205
1206 #[test]
1209 fn test_parse_tree_attr_str() {
1210 let tokens: proc_macro2::TokenStream = syn::parse_quote! { style = "bold" };
1211 let attr: tree::TreeAttr = syn::parse2(tokens).unwrap();
1212 assert_eq!(attr.key, "style");
1213 match attr.value {
1214 tree::TreeAttrValue::Str(s) => assert_eq!(s.value(), "bold"),
1215 _ => panic!("expected Str"),
1216 }
1217 }
1218
1219 #[test]
1220 fn test_parse_tree_attr_flag() {
1221 let tokens: proc_macro2::TokenStream = syn::parse_quote! { style };
1222 let attr: tree::TreeAttr = syn::parse2(tokens).unwrap();
1223 assert_eq!(attr.key, "style");
1224 matches!(attr.value, tree::TreeAttrValue::Flag);
1225 }
1226
1227 #[test]
1228 fn test_tree_expect_str_ok() {
1229 let tokens: proc_macro2::TokenStream = syn::parse_quote! { guide_style = "dim cyan" };
1230 let attr: tree::TreeAttr = syn::parse2(tokens).unwrap();
1231 let result = tree::tree_expect_str(&attr, "guide_style");
1232 assert!(result.is_ok());
1233 assert_eq!(result.unwrap().value(), "dim cyan");
1234 }
1235
1236 #[test]
1237 fn test_tree_expect_str_wrong_type() {
1238 let tokens: proc_macro2::TokenStream = syn::parse_quote! { style };
1239 let attr: tree::TreeAttr = syn::parse2(tokens).unwrap();
1240 let result = tree::tree_expect_str(&attr, "style");
1241 assert!(result.is_err());
1242 }
1243
1244 #[test]
1247 fn test_derive_renderable_via_panel() {
1248 let input: DeriveInput = syn::parse_quote! {
1249 #[renderable(via = "panel")]
1250 struct Config {
1251 host: String,
1252 port: u16,
1253 }
1254 };
1255 let result = renderable::derive_renderable_impl(&input);
1256 assert!(
1257 result.is_ok(),
1258 "derive_renderable_impl failed: {:?}",
1259 result.err()
1260 );
1261 let tokens = result.unwrap().to_string();
1262 assert!(
1263 tokens.contains("Renderable"),
1264 "should implement Renderable trait"
1265 );
1266 assert!(
1267 tokens.contains("gilt_console"),
1268 "should generate gilt_console method"
1269 );
1270 assert!(tokens.contains("to_panel"), "should delegate to to_panel()");
1271 assert!(
1272 !tokens.contains("to_tree"),
1273 "should not reference to_tree()"
1274 );
1275 }
1276
1277 #[test]
1278 fn test_derive_renderable_via_tree() {
1279 let input: DeriveInput = syn::parse_quote! {
1280 #[renderable(via = "tree")]
1281 struct FileEntry {
1282 name: String,
1283 entries: Vec<String>,
1284 }
1285 };
1286 let result = renderable::derive_renderable_impl(&input);
1287 assert!(
1288 result.is_ok(),
1289 "derive_renderable_impl failed: {:?}",
1290 result.err()
1291 );
1292 let tokens = result.unwrap().to_string();
1293 assert!(
1294 tokens.contains("Renderable"),
1295 "should implement Renderable trait"
1296 );
1297 assert!(
1298 tokens.contains("gilt_console"),
1299 "should generate gilt_console method"
1300 );
1301 assert!(tokens.contains("to_tree"), "should delegate to to_tree()");
1302 assert!(
1303 !tokens.contains("to_panel"),
1304 "should not reference to_panel()"
1305 );
1306 }
1307
1308 #[test]
1309 fn test_derive_renderable_default() {
1310 let input: DeriveInput = syn::parse_quote! {
1312 struct Simple {
1313 name: String,
1314 value: u32,
1315 }
1316 };
1317 let result = renderable::derive_renderable_impl(&input);
1318 assert!(
1319 result.is_ok(),
1320 "derive_renderable_impl should succeed with no attrs"
1321 );
1322 let tokens = result.unwrap().to_string();
1323 assert!(
1324 tokens.contains("Renderable"),
1325 "should implement Renderable trait"
1326 );
1327 assert!(
1328 tokens.contains("to_panel"),
1329 "default delegation should be to_panel()"
1330 );
1331 }
1332
1333 #[test]
1334 fn test_derive_renderable_rejects_unknown_via() {
1335 let input: DeriveInput = syn::parse_quote! {
1336 #[renderable(via = "table")]
1337 struct Rec {
1338 a: String,
1339 }
1340 };
1341 let result = renderable::derive_renderable_impl(&input);
1342 assert!(result.is_err(), "should reject unknown via value");
1343 let err_msg = result.unwrap_err().to_string();
1344 assert!(
1345 err_msg.contains("unknown renderable via"),
1346 "error should mention unknown via, got: {}",
1347 err_msg
1348 );
1349 }
1350
1351 #[test]
1352 fn test_derive_renderable_rejects_enum() {
1353 let input: DeriveInput = syn::parse_quote! {
1354 enum Foo { A, B }
1355 };
1356 let result = renderable::derive_renderable_impl(&input);
1357 assert!(result.is_err());
1358 assert!(result
1359 .unwrap_err()
1360 .to_string()
1361 .contains("does not support enums"));
1362 }
1363
1364 #[test]
1365 fn test_derive_renderable_rejects_unknown_attr() {
1366 let input: DeriveInput = syn::parse_quote! {
1367 #[renderable(nonexistent = "value")]
1368 struct Rec {
1369 a: String,
1370 }
1371 };
1372 let result = renderable::derive_renderable_impl(&input);
1373 assert!(result.is_err());
1374 assert!(result
1375 .unwrap_err()
1376 .to_string()
1377 .contains("unknown renderable attribute"),);
1378 }
1379
1380 #[test]
1383 fn test_parse_renderable_attr_str() {
1384 let tokens: proc_macro2::TokenStream = syn::parse_quote! { via = "panel" };
1385 let attr: renderable::RenderableAttr = syn::parse2(tokens).unwrap();
1386 assert_eq!(attr.key, "via");
1387 match attr.value {
1388 renderable::RenderableAttrValue::Str(s) => assert_eq!(s.value(), "panel"),
1389 }
1390 }
1391
1392 #[test]
1393 fn test_renderable_expect_str_ok() {
1394 let tokens: proc_macro2::TokenStream = syn::parse_quote! { via = "tree" };
1395 let attr: renderable::RenderableAttr = syn::parse2(tokens).unwrap();
1396 let result = renderable::renderable_expect_str(&attr, "via");
1397 assert!(result.is_ok());
1398 assert_eq!(result.unwrap().value(), "tree");
1399 }
1400
1401 #[test]
1404 fn test_derive_columns_basic() {
1405 let input: DeriveInput = syn::parse_quote! {
1406 struct ProjectCard {
1407 name: String,
1408 status: String,
1409 }
1410 };
1411 let result = columns::derive_columns_impl(&input);
1412 assert!(
1413 result.is_ok(),
1414 "derive_columns_impl failed: {:?}",
1415 result.err()
1416 );
1417 let tokens = result.unwrap().to_string();
1418 assert!(tokens.contains("to_card"), "should generate to_card method");
1419 assert!(
1420 tokens.contains("to_columns"),
1421 "should generate to_columns method"
1422 );
1423 assert!(
1424 tokens.contains("\"Name\""),
1425 "should contain default label 'Name'"
1426 );
1427 assert!(
1428 tokens.contains("\"Status\""),
1429 "should contain default label 'Status'"
1430 );
1431 assert!(tokens.contains("Panel"), "should reference Panel type");
1432 assert!(tokens.contains("Columns"), "should reference Columns type");
1433 assert!(
1435 tokens.contains("\"ProjectCard\""),
1436 "default card title should be struct name"
1437 );
1438 }
1439
1440 #[test]
1441 fn test_derive_columns_with_attrs() {
1442 let input: DeriveInput = syn::parse_quote! {
1443 #[columns(
1444 column_count = 3,
1445 equal = true,
1446 expand = true,
1447 padding = 2,
1448 title = "My Projects"
1449 )]
1450 struct ProjectCard {
1451 #[field(label = "Project", style = "bold cyan")]
1452 name: String,
1453 #[field(label = "Status")]
1454 status: String,
1455 }
1456 };
1457 let result = columns::derive_columns_impl(&input);
1458 assert!(
1459 result.is_ok(),
1460 "derive_columns_impl failed: {:?}",
1461 result.err()
1462 );
1463 let tokens = result.unwrap().to_string();
1464 assert!(tokens.contains("to_card"), "should generate to_card method");
1465 assert!(
1466 tokens.contains("to_columns"),
1467 "should generate to_columns method"
1468 );
1469 assert!(
1470 tokens.contains("\"Project\""),
1471 "should use custom label 'Project'"
1472 );
1473 assert!(
1474 tokens.contains("bold cyan"),
1475 "should contain field style markup"
1476 );
1477 assert!(
1478 tokens.contains("\"Status\""),
1479 "should use custom label 'Status'"
1480 );
1481 assert!(tokens.contains("equal"), "should set equal");
1482 assert!(tokens.contains("expand"), "should set expand");
1483 assert!(
1484 tokens.contains("width"),
1485 "should set width from column_count"
1486 );
1487 assert!(tokens.contains("\"My Projects\""), "should contain title");
1488 }
1489
1490 #[test]
1491 fn test_derive_columns_skip_field() {
1492 let input: DeriveInput = syn::parse_quote! {
1493 struct Card {
1494 visible: String,
1495 #[field(skip)]
1496 hidden: u64,
1497 also_visible: i32,
1498 }
1499 };
1500 let result = columns::derive_columns_impl(&input);
1501 assert!(result.is_ok());
1502 let tokens = result.unwrap().to_string();
1503 assert!(
1504 tokens.contains("\"Visible\""),
1505 "should include visible field"
1506 );
1507 assert!(
1508 tokens.contains("\"Also Visible\""),
1509 "should include also_visible field"
1510 );
1511 assert!(!tokens.contains("\"Hidden\""), "should skip hidden field");
1513 assert!(
1514 !tokens.contains("hidden"),
1515 "hidden field ident should not appear"
1516 );
1517 }
1518
1519 #[test]
1520 fn test_derive_columns_rejects_enum() {
1521 let input: DeriveInput = syn::parse_quote! {
1522 enum Foo { A, B }
1523 };
1524 let result = columns::derive_columns_impl(&input);
1525 assert!(result.is_err());
1526 assert!(result
1527 .unwrap_err()
1528 .to_string()
1529 .contains("does not support enums"));
1530 }
1531
1532 #[test]
1535 fn test_parse_columns_attr_str() {
1536 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = "My Cols" };
1537 let attr: columns::ColumnsAttr = syn::parse2(tokens).unwrap();
1538 assert_eq!(attr.key, "title");
1539 match attr.value {
1540 columns::ColumnsAttrValue::Str(s) => assert_eq!(s.value(), "My Cols"),
1541 _ => panic!("expected Str"),
1542 }
1543 }
1544
1545 #[test]
1546 fn test_parse_columns_attr_bool() {
1547 let tokens: proc_macro2::TokenStream = syn::parse_quote! { equal = true };
1548 let attr: columns::ColumnsAttr = syn::parse2(tokens).unwrap();
1549 assert_eq!(attr.key, "equal");
1550 match attr.value {
1551 columns::ColumnsAttrValue::Bool(b) => assert!(b.value),
1552 _ => panic!("expected Bool"),
1553 }
1554 }
1555
1556 #[test]
1557 fn test_parse_columns_attr_int() {
1558 let tokens: proc_macro2::TokenStream = syn::parse_quote! { column_count = 4 };
1559 let attr: columns::ColumnsAttr = syn::parse2(tokens).unwrap();
1560 assert_eq!(attr.key, "column_count");
1561 match attr.value {
1562 columns::ColumnsAttrValue::Int(i) => assert_eq!(i.base10_parse::<usize>().unwrap(), 4),
1563 _ => panic!("expected Int"),
1564 }
1565 }
1566
1567 #[test]
1568 fn test_parse_columns_attr_flag() {
1569 let tokens: proc_macro2::TokenStream = syn::parse_quote! { expand };
1570 let attr: columns::ColumnsAttr = syn::parse2(tokens).unwrap();
1571 assert_eq!(attr.key, "expand");
1572 matches!(attr.value, columns::ColumnsAttrValue::Flag);
1573 }
1574
1575 #[test]
1576 fn test_columns_expect_str_ok() {
1577 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = "hello" };
1578 let attr: columns::ColumnsAttr = syn::parse2(tokens).unwrap();
1579 let result = columns::columns_expect_str(&attr, "title");
1580 assert!(result.is_ok());
1581 assert_eq!(result.unwrap().value(), "hello");
1582 }
1583
1584 #[test]
1585 fn test_columns_expect_bool_flag() {
1586 let tokens: proc_macro2::TokenStream = syn::parse_quote! { expand };
1587 let attr: columns::ColumnsAttr = syn::parse2(tokens).unwrap();
1588 let result = columns::columns_expect_bool(&attr, "expand");
1589 assert!(result.is_ok());
1590 assert!(result.unwrap().value);
1591 }
1592
1593 #[test]
1594 fn test_columns_expect_int_ok() {
1595 let tokens: proc_macro2::TokenStream = syn::parse_quote! { padding = 3 };
1596 let attr: columns::ColumnsAttr = syn::parse2(tokens).unwrap();
1597 let result = columns::columns_expect_int(&attr, "padding");
1598 assert!(result.is_ok());
1599 assert_eq!(result.unwrap().base10_parse::<usize>().unwrap(), 3);
1600 }
1601
1602 #[test]
1603 fn test_derive_columns_rejects_unknown_columns_attr() {
1604 let input: DeriveInput = syn::parse_quote! {
1605 #[columns(nonexistent = "value")]
1606 struct Rec {
1607 a: String,
1608 }
1609 };
1610 let result = columns::derive_columns_impl(&input);
1611 assert!(result.is_err());
1612 assert!(result
1613 .unwrap_err()
1614 .to_string()
1615 .contains("unknown columns attribute"),);
1616 }
1617
1618 #[test]
1619 fn test_derive_columns_rejects_unknown_field_attr() {
1620 let input: DeriveInput = syn::parse_quote! {
1621 struct Rec {
1622 #[field(nonexistent = "value")]
1623 a: String,
1624 }
1625 };
1626 let result = columns::derive_columns_impl(&input);
1627 assert!(result.is_err());
1628 assert!(result
1629 .unwrap_err()
1630 .to_string()
1631 .contains("unknown field attribute"),);
1632 }
1633
1634 #[test]
1637 fn test_derive_rule_basic() {
1638 let input: DeriveInput = syn::parse_quote! {
1639 struct Section {
1640 heading: String,
1641 }
1642 };
1643 let result = rule::derive_rule_impl(&input);
1644 assert!(
1645 result.is_ok(),
1646 "derive_rule_impl failed: {:?}",
1647 result.err()
1648 );
1649 let tokens = result.unwrap().to_string();
1650 assert!(tokens.contains("to_rule"), "should generate to_rule method");
1651 assert!(tokens.contains("Rule"), "should reference Rule type");
1652 assert!(
1653 tokens.contains("with_title"),
1654 "should use with_title constructor"
1655 );
1656 assert!(
1658 tokens.contains("\"Section\""),
1659 "default title should be struct name"
1660 );
1661 }
1662
1663 #[test]
1664 fn test_derive_rule_with_style() {
1665 let input: DeriveInput = syn::parse_quote! {
1666 #[rule(style = "bold blue")]
1667 struct Divider {
1668 label: String,
1669 }
1670 };
1671 let result = rule::derive_rule_impl(&input);
1672 assert!(
1673 result.is_ok(),
1674 "derive_rule_impl failed: {:?}",
1675 result.err()
1676 );
1677 let tokens = result.unwrap().to_string();
1678 assert!(tokens.contains("to_rule"), "should generate to_rule method");
1679 assert!(
1680 tokens.contains("Style :: parse"),
1681 "should parse style string"
1682 );
1683 assert!(
1684 tokens.contains("\"bold blue\""),
1685 "should contain style value"
1686 );
1687 }
1688
1689 #[test]
1690 fn test_derive_rule_with_characters() {
1691 let input: DeriveInput = syn::parse_quote! {
1692 #[rule(characters = "=")]
1693 struct Break {
1694 text: String,
1695 }
1696 };
1697 let result = rule::derive_rule_impl(&input);
1698 assert!(
1699 result.is_ok(),
1700 "derive_rule_impl failed: {:?}",
1701 result.err()
1702 );
1703 let tokens = result.unwrap().to_string();
1704 assert!(
1705 tokens.contains("characters"),
1706 "should call characters method"
1707 );
1708 assert!(tokens.contains("\"=\""), "should contain custom character");
1709 }
1710
1711 #[test]
1712 fn test_derive_rule_with_align() {
1713 let input: DeriveInput = syn::parse_quote! {
1714 #[rule(align = "left")]
1715 struct Header {
1716 text: String,
1717 }
1718 };
1719 let result = rule::derive_rule_impl(&input);
1720 assert!(
1721 result.is_ok(),
1722 "derive_rule_impl failed: {:?}",
1723 result.err()
1724 );
1725 let tokens = result.unwrap().to_string();
1726 assert!(tokens.contains("align"), "should call align method");
1727 assert!(tokens.contains("Left"), "should contain Left variant");
1728 }
1729
1730 #[test]
1731 fn test_derive_rule_title_field() {
1732 let input: DeriveInput = syn::parse_quote! {
1733 struct Section {
1734 #[rule(title)]
1735 heading: String,
1736 extra: u32,
1737 }
1738 };
1739 let result = rule::derive_rule_impl(&input);
1740 assert!(
1741 result.is_ok(),
1742 "derive_rule_impl failed: {:?}",
1743 result.err()
1744 );
1745 let tokens = result.unwrap().to_string();
1746 assert!(tokens.contains("to_rule"), "should generate to_rule method");
1747 assert!(tokens.contains("heading"), "should reference heading field");
1749 assert!(
1750 tokens.contains("to_string"),
1751 "should call to_string on field"
1752 );
1753 assert!(
1755 !tokens.contains("\"Section\""),
1756 "should not use struct name as title"
1757 );
1758 }
1759
1760 #[test]
1761 fn test_derive_rule_rejects_enum() {
1762 let input: DeriveInput = syn::parse_quote! {
1763 enum Foo { A, B }
1764 };
1765 let result = rule::derive_rule_impl(&input);
1766 assert!(result.is_err());
1767 assert!(result
1768 .unwrap_err()
1769 .to_string()
1770 .contains("does not support enums"));
1771 }
1772
1773 #[test]
1776 fn test_parse_rule_attr_str() {
1777 let tokens: proc_macro2::TokenStream = syn::parse_quote! { style = "bold red" };
1778 let attr: rule::RuleAttr = syn::parse2(tokens).unwrap();
1779 assert_eq!(attr.key, "style");
1780 match attr.value {
1781 rule::RuleAttrValue::Str(s) => assert_eq!(s.value(), "bold red"),
1782 }
1783 }
1784
1785 #[test]
1786 fn test_rule_expect_str_ok() {
1787 let tokens: proc_macro2::TokenStream = syn::parse_quote! { characters = "─" };
1788 let attr: rule::RuleAttr = syn::parse2(tokens).unwrap();
1789 let result = rule::rule_expect_str(&attr, "characters");
1790 assert!(result.is_ok());
1791 assert_eq!(result.unwrap().value(), "─");
1792 }
1793
1794 #[test]
1797 fn test_derive_inspect_basic() {
1798 let input: DeriveInput = syn::parse_quote! {
1799 struct Config {
1800 host: String,
1801 port: u16,
1802 }
1803 };
1804 let result = inspect::derive_inspect_impl(&input);
1805 assert!(
1806 result.is_ok(),
1807 "derive_inspect_impl failed: {:?}",
1808 result.err()
1809 );
1810 let tokens = result.unwrap().to_string();
1811 assert!(
1812 tokens.contains("to_inspect"),
1813 "should generate to_inspect method"
1814 );
1815 assert!(tokens.contains("Inspect"), "should reference Inspect type");
1816 assert!(tokens.contains("Debug"), "should have Debug bound");
1817 }
1818
1819 #[test]
1820 fn test_derive_inspect_with_title() {
1821 let input: DeriveInput = syn::parse_quote! {
1822 #[inspect(title = "Server Info")]
1823 struct Config {
1824 host: String,
1825 port: u16,
1826 }
1827 };
1828 let result = inspect::derive_inspect_impl(&input);
1829 assert!(
1830 result.is_ok(),
1831 "derive_inspect_impl failed: {:?}",
1832 result.err()
1833 );
1834 let tokens = result.unwrap().to_string();
1835 assert!(
1836 tokens.contains("\"Server Info\""),
1837 "should contain custom title"
1838 );
1839 assert!(tokens.contains("with_title"), "should call with_title");
1840 }
1841
1842 #[test]
1843 fn test_derive_inspect_with_all_attrs() {
1844 let input: DeriveInput = syn::parse_quote! {
1845 #[inspect(title = "My Widget", label = "web-01", doc = "A web server", pretty = false)]
1846 struct Server {
1847 host: String,
1848 cpu: f32,
1849 }
1850 };
1851 let result = inspect::derive_inspect_impl(&input);
1852 assert!(
1853 result.is_ok(),
1854 "derive_inspect_impl failed: {:?}",
1855 result.err()
1856 );
1857 let tokens = result.unwrap().to_string();
1858 assert!(tokens.contains("with_title"), "should call with_title");
1859 assert!(tokens.contains("with_label"), "should call with_label");
1860 assert!(tokens.contains("with_doc"), "should call with_doc");
1861 assert!(tokens.contains("with_pretty"), "should call with_pretty");
1862 }
1863
1864 #[test]
1865 fn test_derive_inspect_pretty_false() {
1866 let input: DeriveInput = syn::parse_quote! {
1867 #[inspect(pretty = false)]
1868 struct Config {
1869 host: String,
1870 }
1871 };
1872 let result = inspect::derive_inspect_impl(&input);
1873 assert!(result.is_ok());
1874 let tokens = result.unwrap().to_string();
1875 assert!(tokens.contains("with_pretty"), "should call with_pretty");
1876 }
1877
1878 #[test]
1879 fn test_derive_inspect_rejects_enum() {
1880 let input: DeriveInput = syn::parse_quote! {
1881 enum Foo { A, B }
1882 };
1883 let result = inspect::derive_inspect_impl(&input);
1884 assert!(result.is_err());
1885 assert!(result
1886 .unwrap_err()
1887 .to_string()
1888 .contains("does not support enums"));
1889 }
1890
1891 #[test]
1892 fn test_derive_inspect_rejects_unknown_attr() {
1893 let input: DeriveInput = syn::parse_quote! {
1894 #[inspect(nonexistent = "value")]
1895 struct Rec {
1896 a: String,
1897 }
1898 };
1899 let result = inspect::derive_inspect_impl(&input);
1900 assert!(result.is_err());
1901 assert!(result
1902 .unwrap_err()
1903 .to_string()
1904 .contains("unknown inspect attribute"),);
1905 }
1906
1907 #[test]
1910 fn test_parse_inspect_attr_str() {
1911 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = "My Inspect" };
1912 let attr: inspect::InspectAttr = syn::parse2(tokens).unwrap();
1913 assert_eq!(attr.key, "title");
1914 match attr.value {
1915 inspect::InspectAttrValue::Str(s) => assert_eq!(s.value(), "My Inspect"),
1916 _ => panic!("expected Str"),
1917 }
1918 }
1919
1920 #[test]
1921 fn test_parse_inspect_attr_bool() {
1922 let tokens: proc_macro2::TokenStream = syn::parse_quote! { pretty = false };
1923 let attr: inspect::InspectAttr = syn::parse2(tokens).unwrap();
1924 assert_eq!(attr.key, "pretty");
1925 match attr.value {
1926 inspect::InspectAttrValue::Bool(b) => assert!(!b.value),
1927 _ => panic!("expected Bool"),
1928 }
1929 }
1930
1931 #[test]
1932 fn test_inspect_expect_str_ok() {
1933 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = "hello" };
1934 let attr: inspect::InspectAttr = syn::parse2(tokens).unwrap();
1935 let result = inspect::inspect_expect_str(&attr, "title");
1936 assert!(result.is_ok());
1937 assert_eq!(result.unwrap().value(), "hello");
1938 }
1939
1940 #[test]
1941 fn test_inspect_expect_str_wrong_type() {
1942 let tokens: proc_macro2::TokenStream = syn::parse_quote! { title = true };
1943 let attr: inspect::InspectAttr = syn::parse2(tokens).unwrap();
1944 let result = inspect::inspect_expect_str(&attr, "title");
1945 assert!(result.is_err());
1946 }
1947
1948 #[test]
1949 fn test_inspect_expect_bool_flag() {
1950 let tokens: proc_macro2::TokenStream = syn::parse_quote! { pretty };
1951 let attr: inspect::InspectAttr = syn::parse2(tokens).unwrap();
1952 let result = inspect::inspect_expect_bool(&attr, "pretty");
1953 assert!(result.is_ok());
1954 assert!(result.unwrap().value);
1955 }
1956
1957 #[test]
1958 fn test_inspect_expect_bool_ok() {
1959 let tokens: proc_macro2::TokenStream = syn::parse_quote! { pretty = false };
1960 let attr: inspect::InspectAttr = syn::parse2(tokens).unwrap();
1961 let result = inspect::inspect_expect_bool(&attr, "pretty");
1962 assert!(result.is_ok());
1963 assert!(!result.unwrap().value);
1964 }
1965
1966 fn render_tokens(ts: &proc_macro2::TokenStream) -> String {
1982 match syn::parse2::<syn::File>(ts.clone()) {
1986 Ok(file) => prettyplease_or_raw(&file, ts),
1987 Err(_) => ts.to_string(),
1988 }
1989 }
1990
1991 fn prettyplease_or_raw(file: &syn::File, ts: &proc_macro2::TokenStream) -> String {
1992 let _ = file;
1996 ts.to_string()
1997 }
1998
1999 #[test]
2000 fn expand_table_minimal() {
2001 let input: DeriveInput = syn::parse_quote! {
2002 struct Row {
2003 id: u32,
2004 name: String,
2005 }
2006 };
2007 let ts =
2008 table::derive_table_impl(&input).expect("Table derive must succeed on minimal input");
2009 insta::assert_snapshot!(render_tokens(&ts));
2010 }
2011
2012 #[test]
2013 fn expand_panel_minimal() {
2014 let input: DeriveInput = syn::parse_quote! {
2015 struct Server {
2016 host: String,
2017 port: u16,
2018 }
2019 };
2020 let ts = panel::derive_panel_impl(&input).expect("Panel derive must succeed");
2021 insta::assert_snapshot!(render_tokens(&ts));
2022 }
2023
2024 #[test]
2025 fn expand_tree_minimal() {
2026 let input: DeriveInput = syn::parse_quote! {
2027 struct Node {
2028 #[tree(label)]
2029 label: String,
2030 #[tree(children)]
2031 children: Vec<Node>,
2032 }
2033 };
2034 let ts = tree::derive_tree_impl(&input).expect("Tree derive must succeed");
2035 insta::assert_snapshot!(render_tokens(&ts));
2036 }
2037
2038 #[test]
2039 fn expand_columns_minimal() {
2040 let input: DeriveInput = syn::parse_quote! {
2041 struct Item {
2042 title: String,
2043 }
2044 };
2045 let ts = columns::derive_columns_impl(&input).expect("Columns derive must succeed");
2046 insta::assert_snapshot!(render_tokens(&ts));
2047 }
2048
2049 #[test]
2050 fn expand_rule_minimal() {
2051 let input: DeriveInput = syn::parse_quote! {
2052 struct Section {
2053 #[rule(title)]
2054 heading: String,
2055 }
2056 };
2057 let ts = rule::derive_rule_impl(&input).expect("Rule derive must succeed");
2058 insta::assert_snapshot!(render_tokens(&ts));
2059 }
2060
2061 #[test]
2062 fn expand_inspect_minimal() {
2063 let input: DeriveInput = syn::parse_quote! {
2064 struct Status {
2065 cpu: f64,
2066 memory: f64,
2067 }
2068 };
2069 let ts = inspect::derive_inspect_impl(&input).expect("Inspect derive must succeed");
2070 insta::assert_snapshot!(render_tokens(&ts));
2071 }
2072
2073 #[test]
2074 fn expand_renderable_minimal() {
2075 let input: DeriveInput = syn::parse_quote! {
2076 struct Card {
2077 title: String,
2078 }
2079 };
2080 let ts =
2081 renderable::derive_renderable_impl(&input).expect("Renderable derive must succeed");
2082 insta::assert_snapshot!(render_tokens(&ts));
2083 }
2084}