1use super::formatter::TabularFormatter;
39use super::traits::{Tabular, TabularRow};
40use super::types::{FlatDataSpec, TabularSpec};
41use super::util::display_width;
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub enum BorderStyle {
46 #[default]
48 None,
49 Ascii,
51 Light,
53 Heavy,
55 Double,
57 Rounded,
59}
60
61impl BorderStyle {
62 fn chars(&self) -> BorderChars {
67 match self {
68 BorderStyle::None => BorderChars::empty(),
69 BorderStyle::Ascii => BorderChars {
70 horizontal: '-',
71 vertical: '|',
72 top_left: '+',
73 top_right: '+',
74 bottom_left: '+',
75 bottom_right: '+',
76 left_t: '+',
77 cross: '+',
78 right_t: '+',
79 top_t: '+',
80 bottom_t: '+',
81 },
82 BorderStyle::Light => BorderChars {
83 horizontal: '─',
84 vertical: '│',
85 top_left: '┌',
86 top_right: '┐',
87 bottom_left: '└',
88 bottom_right: '┘',
89 left_t: '├',
90 cross: '┼',
91 right_t: '┤',
92 top_t: '┬',
93 bottom_t: '┴',
94 },
95 BorderStyle::Heavy => BorderChars {
96 horizontal: '━',
97 vertical: '┃',
98 top_left: '┏',
99 top_right: '┓',
100 bottom_left: '┗',
101 bottom_right: '┛',
102 left_t: '┣',
103 cross: '╋',
104 right_t: '┫',
105 top_t: '┳',
106 bottom_t: '┻',
107 },
108 BorderStyle::Double => BorderChars {
109 horizontal: '═',
110 vertical: '║',
111 top_left: '╔',
112 top_right: '╗',
113 bottom_left: '╚',
114 bottom_right: '╝',
115 left_t: '╠',
116 cross: '╬',
117 right_t: '╣',
118 top_t: '╦',
119 bottom_t: '╩',
120 },
121 BorderStyle::Rounded => BorderChars {
122 horizontal: '─',
123 vertical: '│',
124 top_left: '╭',
125 top_right: '╮',
126 bottom_left: '╰',
127 bottom_right: '╯',
128 left_t: '├',
129 cross: '┼',
130 right_t: '┤',
131 top_t: '┬',
132 bottom_t: '┴',
133 },
134 }
135 }
136}
137
138#[derive(Clone, Copy, Debug)]
140struct BorderChars {
141 horizontal: char,
142 vertical: char,
143 top_left: char,
144 top_right: char,
145 bottom_left: char,
146 bottom_right: char,
147 left_t: char,
148 cross: char,
149 right_t: char,
150 top_t: char,
151 bottom_t: char,
152}
153
154impl BorderChars {
155 fn empty() -> Self {
156 BorderChars {
157 horizontal: ' ',
158 vertical: ' ',
159 top_left: ' ',
160 top_right: ' ',
161 bottom_left: ' ',
162 bottom_right: ' ',
163 left_t: ' ',
164 cross: ' ',
165 right_t: ' ',
166 top_t: ' ',
167 bottom_t: ' ',
168 }
169 }
170}
171
172#[derive(Clone, Debug)]
174pub struct Table {
175 formatter: TabularFormatter,
177 headers: Option<Vec<String>>,
179 border: BorderStyle,
181 header_style: Option<String>,
183 row_separator: bool,
185}
186
187impl Table {
188 pub fn new(spec: TabularSpec, total_width: usize) -> Self {
190 let formatter = TabularFormatter::new(&spec, total_width);
191 Table {
192 formatter,
193 headers: None,
194 border: BorderStyle::None,
195 header_style: None,
196 row_separator: false,
197 }
198 }
199
200 pub fn from_spec(spec: &FlatDataSpec, total_width: usize) -> Self {
202 let formatter = TabularFormatter::new(spec, total_width);
203 Table {
204 formatter,
205 headers: None,
206 border: BorderStyle::None,
207 header_style: None,
208 row_separator: false,
209 }
210 }
211
212 pub fn from_type<T: Tabular>(total_width: usize) -> Self {
237 let spec = T::tabular_spec();
238 Self::new(spec, total_width)
239 }
240
241 pub fn border(mut self, border: BorderStyle) -> Self {
243 self.border = border;
244 self
245 }
246
247 pub fn header<S: Into<String>, I: IntoIterator<Item = S>>(mut self, headers: I) -> Self {
249 self.headers = Some(headers.into_iter().map(|s| s.into()).collect());
250 self
251 }
252
253 pub fn header_from_columns(mut self) -> Self {
275 self.headers = Some(self.formatter.extract_headers());
276 self
277 }
278
279 pub fn header_style(mut self, style: impl Into<String>) -> Self {
281 self.header_style = Some(style.into());
282 self
283 }
284
285 pub fn row_separator(mut self, enable: bool) -> Self {
287 self.row_separator = enable;
288 self
289 }
290
291 pub fn get_border(&self) -> BorderStyle {
293 self.border
294 }
295
296 pub fn num_columns(&self) -> usize {
298 self.formatter.num_columns()
299 }
300
301 pub fn row<S: AsRef<str>>(&self, values: &[S]) -> String {
303 let content = self.formatter.format_row(values);
304 self.wrap_row(&content)
305 }
306
307 pub fn row_from<T: serde::Serialize>(&self, value: &T) -> String {
325 let content = self.formatter.row_from(value);
326 self.wrap_row(&content)
327 }
328
329 pub fn row_from_trait<T: TabularRow>(&self, value: &T) -> String {
357 let content = self.formatter.row_from_trait(value);
358 self.wrap_row(&content)
359 }
360
361 pub fn header_row(&self) -> String {
363 match &self.headers {
364 Some(headers) => {
365 let content = self.formatter.format_row(headers);
367
368 let styled_content = if let Some(style) = &self.header_style {
370 format!("[{}]{}[/{}]", style, content, style)
371 } else {
372 content
373 };
374
375 self.wrap_row(&styled_content)
376 }
377 None => String::new(),
378 }
379 }
380
381 pub fn separator_row(&self) -> String {
383 self.horizontal_line(LineType::Middle)
384 }
385
386 pub fn top_border(&self) -> String {
388 self.horizontal_line(LineType::Top)
389 }
390
391 pub fn bottom_border(&self) -> String {
393 self.horizontal_line(LineType::Bottom)
394 }
395
396 fn wrap_row(&self, content: &str) -> String {
398 if self.border == BorderStyle::None {
399 return content.to_string();
400 }
401
402 let chars = self.border.chars();
403 format!("{}{}{}", chars.vertical, content, chars.vertical)
404 }
405
406 fn horizontal_line(&self, line_type: LineType) -> String {
408 if self.border == BorderStyle::None {
409 return String::new();
410 }
411
412 let chars = self.border.chars();
413 let widths = self.formatter.widths();
414
415 let content_width: usize = widths.iter().sum();
417 let sep_width = display_width(&self.formatter_separator());
418 let num_seps = widths.len().saturating_sub(1);
419 let total_content = content_width + (num_seps * sep_width);
420
421 let (left, _joint, right) = match line_type {
422 LineType::Top => (chars.top_left, chars.top_t, chars.top_right),
423 LineType::Middle => (chars.left_t, chars.cross, chars.right_t),
424 LineType::Bottom => (chars.bottom_left, chars.bottom_t, chars.bottom_right),
425 };
426
427 let mut line = String::new();
428 line.push(left);
429
430 for (i, &width) in widths.iter().enumerate() {
431 if i > 0 {
432 for _ in 0..sep_width {
434 line.push(chars.horizontal);
435 }
436 }
439 for _ in 0..width {
440 line.push(chars.horizontal);
441 }
442 }
443
444 line = format!(
448 "{}{}{}",
449 left,
450 std::iter::repeat_n(chars.horizontal, total_content).collect::<String>(),
451 right
452 );
453
454 line
455 }
456
457 fn formatter_separator(&self) -> String {
459 use minijinja::value::{Object, Value};
461 use std::sync::Arc;
462 let arc_formatter = Arc::new(self.formatter.clone());
463 arc_formatter
464 .get_value(&Value::from("separator"))
465 .map(|v| v.to_string())
466 .unwrap_or_default()
467 }
468
469 pub fn render<S: AsRef<str>>(&self, rows: &[Vec<S>]) -> String {
473 let mut output = Vec::new();
474
475 let top = self.top_border();
477 if !top.is_empty() {
478 output.push(top);
479 }
480
481 let header = self.header_row();
483 if !header.is_empty() {
484 output.push(header);
485
486 let sep = self.separator_row();
488 if !sep.is_empty() {
489 output.push(sep);
490 }
491 }
492
493 let separator = if self.row_separator {
495 let sep = self.separator_row();
496 if sep.is_empty() {
497 None
498 } else {
499 Some(sep)
500 }
501 } else {
502 None
503 };
504
505 for (i, row) in rows.iter().enumerate() {
506 if i > 0 {
507 if let Some(ref sep) = separator {
508 output.push(sep.clone());
509 }
510 }
511 output.push(self.row(row));
512 }
513
514 let bottom = self.bottom_border();
516 if !bottom.is_empty() {
517 output.push(bottom);
518 }
519
520 output.join("\n")
521 }
522}
523
524#[derive(Clone, Copy, Debug, PartialEq, Eq)]
526enum LineType {
527 Top,
528 Middle,
529 Bottom,
530}
531
532impl minijinja::value::Object for Table {
537 fn get_value(self: &std::sync::Arc<Self>, key: &minijinja::Value) -> Option<minijinja::Value> {
538 match key.as_str()? {
539 "num_columns" => Some(minijinja::Value::from(self.num_columns())),
540 "border" => Some(minijinja::Value::from(format!("{:?}", self.get_border()))),
541 _ => None,
542 }
543 }
544
545 fn enumerate(self: &std::sync::Arc<Self>) -> minijinja::value::Enumerator {
546 minijinja::value::Enumerator::Str(&["num_columns", "border"])
547 }
548
549 fn call_method(
550 self: &std::sync::Arc<Self>,
551 _state: &minijinja::State,
552 name: &str,
553 args: &[minijinja::Value],
554 ) -> Result<minijinja::Value, minijinja::Error> {
555 match name {
556 "row" => {
557 if args.is_empty() {
559 return Err(minijinja::Error::new(
560 minijinja::ErrorKind::MissingArgument,
561 "row() requires an array of values",
562 ));
563 }
564
565 let values: Vec<String> = match args[0].try_iter() {
566 Ok(iter) => iter.map(|v| v.to_string()).collect(),
567 Err(_) => vec![args[0].to_string()],
568 };
569
570 let formatted = self.row(&values);
571 Ok(minijinja::Value::from(formatted))
572 }
573 "row_from" => {
574 if args.is_empty() {
576 return Err(minijinja::Error::new(
577 minijinja::ErrorKind::MissingArgument,
578 "row_from() requires an object argument",
579 ));
580 }
581
582 let json_value = minijinja::value::Value::from_serialize(&args[0]);
584 let formatted = self.formatter.row_from(&json_value);
585 Ok(minijinja::Value::from(self.wrap_row(&formatted)))
586 }
587 "header_row" => {
588 Ok(minijinja::Value::from(self.header_row()))
590 }
591 "separator_row" => {
592 Ok(minijinja::Value::from(self.separator_row()))
594 }
595 "top_border" => {
596 Ok(minijinja::Value::from(self.top_border()))
598 }
599 "bottom_border" => {
600 Ok(minijinja::Value::from(self.bottom_border()))
602 }
603 "render_all" => {
604 if args.is_empty() {
606 return Err(minijinja::Error::new(
607 minijinja::ErrorKind::MissingArgument,
608 "render_all() requires an array of rows",
609 ));
610 }
611
612 let rows_iter = args[0].try_iter().map_err(|_| {
613 minijinja::Error::new(
614 minijinja::ErrorKind::InvalidOperation,
615 "render_all() requires an array of rows",
616 )
617 })?;
618
619 let rows: Vec<Vec<String>> = rows_iter
620 .map(|row| {
621 row.try_iter()
622 .map(|iter| iter.map(|v| v.to_string()).collect())
623 .unwrap_or_else(|_| vec![row.to_string()])
624 })
625 .collect();
626
627 let formatted = Table::render(self, &rows);
628 Ok(minijinja::Value::from(formatted))
629 }
630 _ => Err(minijinja::Error::new(
631 minijinja::ErrorKind::UnknownMethod,
632 format!("Table has no method '{}'", name),
633 )),
634 }
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use crate::tabular::Col;
642
643 fn simple_spec() -> TabularSpec {
644 TabularSpec::builder()
645 .column(Col::fixed(10))
646 .column(Col::fixed(8))
647 .separator(" ")
648 .build()
649 }
650
651 #[test]
652 fn table_no_border() {
653 let table = Table::new(simple_spec(), 80);
654 let row = table.row(&["Hello", "World"]);
655 assert!(!row.contains('│'));
657 assert!(row.contains("Hello"));
658 }
659
660 #[test]
661 fn table_with_ascii_border() {
662 let table = Table::new(simple_spec(), 80).border(BorderStyle::Ascii);
663 let row = table.row(&["Hello", "World"]);
664 assert!(row.starts_with('|'));
666 assert!(row.ends_with('|'));
667 }
668
669 #[test]
670 fn table_with_light_border() {
671 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
672 let row = table.row(&["Hello", "World"]);
673 assert!(row.starts_with('│'));
675 assert!(row.ends_with('│'));
676 }
677
678 #[test]
679 fn table_with_heavy_border() {
680 let table = Table::new(simple_spec(), 80).border(BorderStyle::Heavy);
681 let row = table.row(&["Hello", "World"]);
682 assert!(row.starts_with('┃'));
683 assert!(row.ends_with('┃'));
684 }
685
686 #[test]
687 fn table_with_double_border() {
688 let table = Table::new(simple_spec(), 80).border(BorderStyle::Double);
689 let row = table.row(&["Hello", "World"]);
690 assert!(row.starts_with('║'));
691 assert!(row.ends_with('║'));
692 }
693
694 #[test]
695 fn table_with_rounded_border() {
696 let table = Table::new(simple_spec(), 80).border(BorderStyle::Rounded);
697 let row = table.row(&["Hello", "World"]);
698 assert!(row.starts_with('│'));
699 assert!(row.ends_with('│'));
700 }
701
702 #[test]
703 fn table_header_row() {
704 let table = Table::new(simple_spec(), 80)
705 .border(BorderStyle::Light)
706 .header(vec!["Name", "Status"]);
707
708 let header = table.header_row();
709 assert!(header.contains("Name"));
710 assert!(header.contains("Status"));
711 assert!(header.starts_with('│'));
712 }
713
714 #[test]
715 fn table_header_with_style() {
716 let table = Table::new(simple_spec(), 80)
717 .header(vec!["Name", "Status"])
718 .header_style("header");
719
720 let header = table.header_row();
721 assert!(header.contains("[header]"));
722 assert!(header.contains("[/header]"));
723 }
724
725 #[test]
726 fn table_no_header() {
727 let table = Table::new(simple_spec(), 80);
728 let header = table.header_row();
729 assert!(header.is_empty());
730 }
731
732 #[test]
733 fn table_separator_row() {
734 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
735 let sep = table.separator_row();
736 assert!(sep.contains('─'));
737 assert!(sep.starts_with('├'));
738 assert!(sep.ends_with('┤'));
739 }
740
741 #[test]
742 fn table_top_border() {
743 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
744 let top = table.top_border();
745 assert!(top.contains('─'));
746 assert!(top.starts_with('┌'));
747 assert!(top.ends_with('┐'));
748 }
749
750 #[test]
751 fn table_bottom_border() {
752 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
753 let bottom = table.bottom_border();
754 assert!(bottom.contains('─'));
755 assert!(bottom.starts_with('└'));
756 assert!(bottom.ends_with('┘'));
757 }
758
759 #[test]
760 fn table_render_full() {
761 let table = Table::new(simple_spec(), 80)
762 .border(BorderStyle::Light)
763 .header(vec!["Name", "Value"]);
764
765 let data = vec![vec!["Alice", "100"], vec!["Bob", "200"]];
766
767 let output = table.render(&data);
768 let lines: Vec<&str> = output.lines().collect();
769
770 assert!(lines.len() >= 5);
772
773 assert!(lines[0].starts_with('┌'));
775 assert!(lines[1].contains("Name"));
777 assert!(lines[2].starts_with('├'));
779 assert!(lines[3].contains("Alice"));
781 assert!(lines[4].contains("Bob"));
782 assert!(lines[5].starts_with('└'));
784 }
785
786 #[test]
787 fn table_render_no_border() {
788 let table = Table::new(simple_spec(), 80).header(vec!["Name", "Value"]);
789
790 let data = vec![vec!["Alice", "100"]];
791
792 let output = table.render(&data);
793 let lines: Vec<&str> = output.lines().collect();
794
795 assert!(lines.len() >= 2);
797 assert!(lines[0].contains("Name"));
798 assert!(lines[1].contains("Alice"));
799 }
800
801 #[test]
802 fn border_style_default() {
803 assert_eq!(BorderStyle::default(), BorderStyle::None);
804 }
805
806 #[test]
807 fn table_accessors() {
808 let table = Table::new(simple_spec(), 80).border(BorderStyle::Ascii);
809
810 assert_eq!(table.get_border(), BorderStyle::Ascii);
811 assert_eq!(table.num_columns(), 2);
812 }
813
814 #[test]
815 fn table_row_from() {
816 use serde::Serialize;
817
818 #[derive(Serialize)]
819 struct Record {
820 name: String,
821 status: String,
822 }
823
824 let spec = TabularSpec::builder()
825 .column(Col::fixed(10).key("name"))
826 .column(Col::fixed(8).key("status"))
827 .separator(" ")
828 .build();
829
830 let table = Table::new(spec, 80);
831 let record = Record {
832 name: "Alice".to_string(),
833 status: "active".to_string(),
834 };
835
836 let row = table.row_from(&record);
837 assert!(row.contains("Alice"));
838 assert!(row.contains("active"));
839 }
840
841 #[test]
842 fn table_row_from_with_border() {
843 use serde::Serialize;
844
845 #[derive(Serialize)]
846 struct Item {
847 id: u32,
848 value: String,
849 }
850
851 let spec = TabularSpec::builder()
852 .column(Col::fixed(5).key("id"))
853 .column(Col::fixed(10).key("value"))
854 .build();
855
856 let table = Table::new(spec, 80).border(BorderStyle::Light);
857 let item = Item {
858 id: 42,
859 value: "test".to_string(),
860 };
861
862 let row = table.row_from(&item);
863 assert!(row.starts_with('│'));
864 assert!(row.ends_with('│'));
865 assert!(row.contains("42"));
866 assert!(row.contains("test"));
867 }
868
869 #[test]
870 fn table_row_separator_option() {
871 let spec = TabularSpec::builder()
872 .column(Col::fixed(10))
873 .column(Col::fixed(8))
874 .build();
875
876 let table = Table::new(spec, 80)
877 .border(BorderStyle::Light)
878 .row_separator(true);
879
880 let data = vec![vec!["A", "1"], vec!["B", "2"], vec!["C", "3"]];
881 let output = table.render(&data);
882 let lines: Vec<&str> = output.lines().collect();
883
884 let sep_count = lines.iter().filter(|l| l.starts_with('├')).count();
887 assert_eq!(sep_count, 2, "Expected 2 separators between 3 rows");
888 }
889
890 #[test]
891 fn table_row_separator_disabled_by_default() {
892 let spec = TabularSpec::builder()
893 .column(Col::fixed(10))
894 .column(Col::fixed(8))
895 .build();
896
897 let table = Table::new(spec, 80).border(BorderStyle::Light);
898
899 let data = vec![vec!["A", "1"], vec!["B", "2"]];
900 let output = table.render(&data);
901 let lines: Vec<&str> = output.lines().collect();
902
903 assert_eq!(lines.len(), 4);
906 }
907
908 #[test]
909 fn table_header_from_columns_with_header_field() {
910 let spec = TabularSpec::builder()
911 .column(Col::fixed(10).header("Name"))
912 .column(Col::fixed(8).header("Status"))
913 .separator(" ")
914 .build();
915
916 let table = Table::new(spec, 80)
917 .header_from_columns()
918 .border(BorderStyle::Light);
919
920 let header = table.header_row();
921 assert!(header.contains("Name"));
922 assert!(header.contains("Status"));
923 }
924
925 #[test]
926 fn table_header_from_columns_fallback_to_key() {
927 let spec = TabularSpec::builder()
928 .column(Col::fixed(10).key("user_name"))
929 .column(Col::fixed(8).key("status"))
930 .separator(" ")
931 .build();
932
933 let table = Table::new(spec, 80).header_from_columns();
934
935 let header = table.header_row();
936 assert!(header.contains("user_name"));
937 assert!(header.contains("status"));
938 }
939
940 #[test]
941 fn table_header_from_columns_fallback_to_name() {
942 let spec = TabularSpec::builder()
943 .column(Col::fixed(10).named("column1"))
944 .column(Col::fixed(8).named("column2"))
945 .separator(" ")
946 .build();
947
948 let table = Table::new(spec, 80).header_from_columns();
949
950 let header = table.header_row();
951 assert!(header.contains("column1"));
952 assert!(header.contains("column2"));
953 }
954
955 #[test]
956 fn table_header_from_columns_priority_order() {
957 let spec = TabularSpec::builder()
959 .column(Col::fixed(10).header("Header").key("key").named("name"))
960 .column(Col::fixed(10).key("key_only").named("name_only"))
961 .column(Col::fixed(10).named("name_only2"))
962 .separator(" ")
963 .build();
964
965 let table = Table::new(spec, 80).header_from_columns();
966
967 let header = table.header_row();
968 assert!(header.contains("Header")); assert!(header.contains("key_only")); assert!(header.contains("name_only2")); }
972
973 #[test]
974 fn table_header_from_columns_in_render() {
975 let spec = TabularSpec::builder()
976 .column(Col::fixed(10).header("Name"))
977 .column(Col::fixed(8).header("Value"))
978 .separator(" ")
979 .build();
980
981 let table = Table::new(spec, 80)
982 .header_from_columns()
983 .border(BorderStyle::Light);
984
985 let data = vec![vec!["Alice", "100"]];
986 let output = table.render(&data);
987
988 assert!(output.contains("Name"));
990 assert!(output.contains("Value"));
991 assert!(output.contains("Alice"));
992 assert!(output.contains("100"));
993 }
994}