1use super::formatter::{CellValue, OwnedCellValue, 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_cells(&self, values: &[CellValue<'_>]) -> String {
312 let content = self.formatter.format_row_cells(values);
313 self.wrap_row(&content)
314 }
315
316 pub fn row_from<T: serde::Serialize>(&self, value: &T) -> String {
334 let content = self.formatter.row_from(value);
335 self.wrap_row(&content)
336 }
337
338 pub fn row_from_trait<T: TabularRow>(&self, value: &T) -> String {
366 let content = self.formatter.row_from_trait(value);
367 self.wrap_row(&content)
368 }
369
370 pub fn header_row(&self) -> String {
372 match &self.headers {
373 Some(headers) => {
374 let content = self.formatter.format_row(headers);
376
377 let styled_content = if let Some(style) = &self.header_style {
379 format!("[{}]{}[/{}]", style, content, style)
380 } else {
381 content
382 };
383
384 self.wrap_row(&styled_content)
385 }
386 None => String::new(),
387 }
388 }
389
390 pub fn separator_row(&self) -> String {
392 self.horizontal_line(LineType::Middle)
393 }
394
395 pub fn top_border(&self) -> String {
397 self.horizontal_line(LineType::Top)
398 }
399
400 pub fn bottom_border(&self) -> String {
402 self.horizontal_line(LineType::Bottom)
403 }
404
405 fn wrap_row(&self, content: &str) -> String {
407 if self.border == BorderStyle::None {
408 return content.to_string();
409 }
410
411 let chars = self.border.chars();
412 format!("{}{}{}", chars.vertical, content, chars.vertical)
413 }
414
415 fn horizontal_line(&self, line_type: LineType) -> String {
417 if self.border == BorderStyle::None {
418 return String::new();
419 }
420
421 let chars = self.border.chars();
422 let widths = self.formatter.widths();
423
424 let content_width: usize = widths.iter().sum();
426 let sep_width = display_width(&self.formatter_separator());
427 let num_seps = widths.len().saturating_sub(1);
428 let total_content = content_width + (num_seps * sep_width);
429
430 let (left, _joint, right) = match line_type {
431 LineType::Top => (chars.top_left, chars.top_t, chars.top_right),
432 LineType::Middle => (chars.left_t, chars.cross, chars.right_t),
433 LineType::Bottom => (chars.bottom_left, chars.bottom_t, chars.bottom_right),
434 };
435
436 let mut line = String::new();
437 line.push(left);
438
439 for (i, &width) in widths.iter().enumerate() {
440 if i > 0 {
441 for _ in 0..sep_width {
443 line.push(chars.horizontal);
444 }
445 }
448 for _ in 0..width {
449 line.push(chars.horizontal);
450 }
451 }
452
453 line = format!(
457 "{}{}{}",
458 left,
459 std::iter::repeat_n(chars.horizontal, total_content).collect::<String>(),
460 right
461 );
462
463 line
464 }
465
466 fn formatter_separator(&self) -> String {
468 use minijinja::value::{Object, Value};
470 use std::sync::Arc;
471 let arc_formatter = Arc::new(self.formatter.clone());
472 arc_formatter
473 .get_value(&Value::from("separator"))
474 .map(|v| v.to_string())
475 .unwrap_or_default()
476 }
477
478 pub fn render<S: AsRef<str>>(&self, rows: &[Vec<S>]) -> String {
482 let mut output = Vec::new();
483
484 let top = self.top_border();
486 if !top.is_empty() {
487 output.push(top);
488 }
489
490 let header = self.header_row();
492 if !header.is_empty() {
493 output.push(header);
494
495 let sep = self.separator_row();
497 if !sep.is_empty() {
498 output.push(sep);
499 }
500 }
501
502 let separator = if self.row_separator {
504 let sep = self.separator_row();
505 if sep.is_empty() {
506 None
507 } else {
508 Some(sep)
509 }
510 } else {
511 None
512 };
513
514 for (i, row) in rows.iter().enumerate() {
515 if i > 0 {
516 if let Some(ref sep) = separator {
517 output.push(sep.clone());
518 }
519 }
520 output.push(self.row(row));
521 }
522
523 let bottom = self.bottom_border();
525 if !bottom.is_empty() {
526 output.push(bottom);
527 }
528
529 output.join("\n")
530 }
531}
532
533#[derive(Clone, Copy, Debug, PartialEq, Eq)]
535enum LineType {
536 Top,
537 Middle,
538 Bottom,
539}
540
541impl minijinja::value::Object for Table {
546 fn get_value(self: &std::sync::Arc<Self>, key: &minijinja::Value) -> Option<minijinja::Value> {
547 match key.as_str()? {
548 "num_columns" => Some(minijinja::Value::from(self.num_columns())),
549 "border" => Some(minijinja::Value::from(format!("{:?}", self.get_border()))),
550 _ => None,
551 }
552 }
553
554 fn enumerate(self: &std::sync::Arc<Self>) -> minijinja::value::Enumerator {
555 minijinja::value::Enumerator::Str(&["num_columns", "border"])
556 }
557
558 fn call_method(
559 self: &std::sync::Arc<Self>,
560 _state: &minijinja::State,
561 name: &str,
562 args: &[minijinja::Value],
563 ) -> Result<minijinja::Value, minijinja::Error> {
564 match name {
565 "row" => {
566 if args.is_empty() {
568 return Err(minijinja::Error::new(
569 minijinja::ErrorKind::MissingArgument,
570 "row() requires an array of values",
571 ));
572 }
573
574 let values_arg = &args[0];
575
576 if self.formatter.has_sub_columns() {
577 let outer_iter = match values_arg.try_iter() {
579 Ok(iter) => iter,
580 Err(_) => {
581 let values = vec![values_arg.to_string()];
582 return Ok(minijinja::Value::from(self.row(&values)));
583 }
584 };
585
586 let mut owned_values: Vec<OwnedCellValue> = Vec::new();
587 for (i, v) in outer_iter.enumerate() {
588 let is_sub_col = self
589 .formatter
590 .columns()
591 .get(i)
592 .and_then(|c| c.sub_columns.as_ref())
593 .is_some();
594
595 if is_sub_col {
596 if let Ok(inner_iter) = v.try_iter() {
597 let sub_vals: Vec<String> =
598 inner_iter.map(|iv| iv.to_string()).collect();
599 owned_values.push(OwnedCellValue::Sub(sub_vals));
600 } else {
601 owned_values.push(OwnedCellValue::Single(v.to_string()));
602 }
603 } else {
604 owned_values.push(OwnedCellValue::Single(v.to_string()));
605 }
606 }
607
608 let cell_values: Vec<CellValue<'_>> = owned_values
609 .iter()
610 .map(|ov| match ov {
611 OwnedCellValue::Single(s) => CellValue::Single(s.as_str()),
612 OwnedCellValue::Sub(v) => {
613 CellValue::Sub(v.iter().map(|s| s.as_str()).collect())
614 }
615 })
616 .collect();
617
618 let formatted = self.row_cells(&cell_values);
619 Ok(minijinja::Value::from(formatted))
620 } else {
621 let values: Vec<String> = match values_arg.try_iter() {
622 Ok(iter) => iter.map(|v| v.to_string()).collect(),
623 Err(_) => vec![values_arg.to_string()],
624 };
625
626 let formatted = self.row(&values);
627 Ok(minijinja::Value::from(formatted))
628 }
629 }
630 "row_from" => {
631 if args.is_empty() {
633 return Err(minijinja::Error::new(
634 minijinja::ErrorKind::MissingArgument,
635 "row_from() requires an object argument",
636 ));
637 }
638
639 let json_value = minijinja::value::Value::from_serialize(&args[0]);
641 let formatted = self.formatter.row_from(&json_value);
642 Ok(minijinja::Value::from(self.wrap_row(&formatted)))
643 }
644 "header_row" => {
645 Ok(minijinja::Value::from(self.header_row()))
647 }
648 "separator_row" => {
649 Ok(minijinja::Value::from(self.separator_row()))
651 }
652 "top_border" => {
653 Ok(minijinja::Value::from(self.top_border()))
655 }
656 "bottom_border" => {
657 Ok(minijinja::Value::from(self.bottom_border()))
659 }
660 "render_all" => {
661 if args.is_empty() {
663 return Err(minijinja::Error::new(
664 minijinja::ErrorKind::MissingArgument,
665 "render_all() requires an array of rows",
666 ));
667 }
668
669 let rows_iter = args[0].try_iter().map_err(|_| {
670 minijinja::Error::new(
671 minijinja::ErrorKind::InvalidOperation,
672 "render_all() requires an array of rows",
673 )
674 })?;
675
676 let rows: Vec<Vec<String>> = rows_iter
677 .map(|row| {
678 row.try_iter()
679 .map(|iter| iter.map(|v| v.to_string()).collect())
680 .unwrap_or_else(|_| vec![row.to_string()])
681 })
682 .collect();
683
684 let formatted = Table::render(self, &rows);
685 Ok(minijinja::Value::from(formatted))
686 }
687 _ => Err(minijinja::Error::new(
688 minijinja::ErrorKind::UnknownMethod,
689 format!("Table has no method '{}'", name),
690 )),
691 }
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698 use crate::tabular::Col;
699
700 fn simple_spec() -> TabularSpec {
701 TabularSpec::builder()
702 .column(Col::fixed(10))
703 .column(Col::fixed(8))
704 .separator(" ")
705 .build()
706 }
707
708 #[test]
709 fn table_no_border() {
710 let table = Table::new(simple_spec(), 80);
711 let row = table.row(&["Hello", "World"]);
712 assert!(!row.contains('│'));
714 assert!(row.contains("Hello"));
715 }
716
717 #[test]
718 fn table_with_ascii_border() {
719 let table = Table::new(simple_spec(), 80).border(BorderStyle::Ascii);
720 let row = table.row(&["Hello", "World"]);
721 assert!(row.starts_with('|'));
723 assert!(row.ends_with('|'));
724 }
725
726 #[test]
727 fn table_with_light_border() {
728 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
729 let row = table.row(&["Hello", "World"]);
730 assert!(row.starts_with('│'));
732 assert!(row.ends_with('│'));
733 }
734
735 #[test]
736 fn table_with_heavy_border() {
737 let table = Table::new(simple_spec(), 80).border(BorderStyle::Heavy);
738 let row = table.row(&["Hello", "World"]);
739 assert!(row.starts_with('┃'));
740 assert!(row.ends_with('┃'));
741 }
742
743 #[test]
744 fn table_with_double_border() {
745 let table = Table::new(simple_spec(), 80).border(BorderStyle::Double);
746 let row = table.row(&["Hello", "World"]);
747 assert!(row.starts_with('║'));
748 assert!(row.ends_with('║'));
749 }
750
751 #[test]
752 fn table_with_rounded_border() {
753 let table = Table::new(simple_spec(), 80).border(BorderStyle::Rounded);
754 let row = table.row(&["Hello", "World"]);
755 assert!(row.starts_with('│'));
756 assert!(row.ends_with('│'));
757 }
758
759 #[test]
760 fn table_header_row() {
761 let table = Table::new(simple_spec(), 80)
762 .border(BorderStyle::Light)
763 .header(vec!["Name", "Status"]);
764
765 let header = table.header_row();
766 assert!(header.contains("Name"));
767 assert!(header.contains("Status"));
768 assert!(header.starts_with('│'));
769 }
770
771 #[test]
772 fn table_header_with_style() {
773 let table = Table::new(simple_spec(), 80)
774 .header(vec!["Name", "Status"])
775 .header_style("header");
776
777 let header = table.header_row();
778 assert!(header.contains("[header]"));
779 assert!(header.contains("[/header]"));
780 }
781
782 #[test]
783 fn table_no_header() {
784 let table = Table::new(simple_spec(), 80);
785 let header = table.header_row();
786 assert!(header.is_empty());
787 }
788
789 #[test]
790 fn table_separator_row() {
791 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
792 let sep = table.separator_row();
793 assert!(sep.contains('─'));
794 assert!(sep.starts_with('├'));
795 assert!(sep.ends_with('┤'));
796 }
797
798 #[test]
799 fn table_top_border() {
800 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
801 let top = table.top_border();
802 assert!(top.contains('─'));
803 assert!(top.starts_with('┌'));
804 assert!(top.ends_with('┐'));
805 }
806
807 #[test]
808 fn table_bottom_border() {
809 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
810 let bottom = table.bottom_border();
811 assert!(bottom.contains('─'));
812 assert!(bottom.starts_with('└'));
813 assert!(bottom.ends_with('┘'));
814 }
815
816 #[test]
817 fn table_render_full() {
818 let table = Table::new(simple_spec(), 80)
819 .border(BorderStyle::Light)
820 .header(vec!["Name", "Value"]);
821
822 let data = vec![vec!["Alice", "100"], vec!["Bob", "200"]];
823
824 let output = table.render(&data);
825 let lines: Vec<&str> = output.lines().collect();
826
827 assert!(lines.len() >= 5);
829
830 assert!(lines[0].starts_with('┌'));
832 assert!(lines[1].contains("Name"));
834 assert!(lines[2].starts_with('├'));
836 assert!(lines[3].contains("Alice"));
838 assert!(lines[4].contains("Bob"));
839 assert!(lines[5].starts_with('└'));
841 }
842
843 #[test]
844 fn table_render_no_border() {
845 let table = Table::new(simple_spec(), 80).header(vec!["Name", "Value"]);
846
847 let data = vec![vec!["Alice", "100"]];
848
849 let output = table.render(&data);
850 let lines: Vec<&str> = output.lines().collect();
851
852 assert!(lines.len() >= 2);
854 assert!(lines[0].contains("Name"));
855 assert!(lines[1].contains("Alice"));
856 }
857
858 #[test]
859 fn border_style_default() {
860 assert_eq!(BorderStyle::default(), BorderStyle::None);
861 }
862
863 #[test]
864 fn table_accessors() {
865 let table = Table::new(simple_spec(), 80).border(BorderStyle::Ascii);
866
867 assert_eq!(table.get_border(), BorderStyle::Ascii);
868 assert_eq!(table.num_columns(), 2);
869 }
870
871 #[test]
872 fn table_row_from() {
873 use serde::Serialize;
874
875 #[derive(Serialize)]
876 struct Record {
877 name: String,
878 status: String,
879 }
880
881 let spec = TabularSpec::builder()
882 .column(Col::fixed(10).key("name"))
883 .column(Col::fixed(8).key("status"))
884 .separator(" ")
885 .build();
886
887 let table = Table::new(spec, 80);
888 let record = Record {
889 name: "Alice".to_string(),
890 status: "active".to_string(),
891 };
892
893 let row = table.row_from(&record);
894 assert!(row.contains("Alice"));
895 assert!(row.contains("active"));
896 }
897
898 #[test]
899 fn table_row_from_with_border() {
900 use serde::Serialize;
901
902 #[derive(Serialize)]
903 struct Item {
904 id: u32,
905 value: String,
906 }
907
908 let spec = TabularSpec::builder()
909 .column(Col::fixed(5).key("id"))
910 .column(Col::fixed(10).key("value"))
911 .build();
912
913 let table = Table::new(spec, 80).border(BorderStyle::Light);
914 let item = Item {
915 id: 42,
916 value: "test".to_string(),
917 };
918
919 let row = table.row_from(&item);
920 assert!(row.starts_with('│'));
921 assert!(row.ends_with('│'));
922 assert!(row.contains("42"));
923 assert!(row.contains("test"));
924 }
925
926 #[test]
927 fn table_row_separator_option() {
928 let spec = TabularSpec::builder()
929 .column(Col::fixed(10))
930 .column(Col::fixed(8))
931 .build();
932
933 let table = Table::new(spec, 80)
934 .border(BorderStyle::Light)
935 .row_separator(true);
936
937 let data = vec![vec!["A", "1"], vec!["B", "2"], vec!["C", "3"]];
938 let output = table.render(&data);
939 let lines: Vec<&str> = output.lines().collect();
940
941 let sep_count = lines.iter().filter(|l| l.starts_with('├')).count();
944 assert_eq!(sep_count, 2, "Expected 2 separators between 3 rows");
945 }
946
947 #[test]
948 fn table_row_separator_disabled_by_default() {
949 let spec = TabularSpec::builder()
950 .column(Col::fixed(10))
951 .column(Col::fixed(8))
952 .build();
953
954 let table = Table::new(spec, 80).border(BorderStyle::Light);
955
956 let data = vec![vec!["A", "1"], vec!["B", "2"]];
957 let output = table.render(&data);
958 let lines: Vec<&str> = output.lines().collect();
959
960 assert_eq!(lines.len(), 4);
963 }
964
965 #[test]
966 fn table_header_from_columns_with_header_field() {
967 let spec = TabularSpec::builder()
968 .column(Col::fixed(10).header("Name"))
969 .column(Col::fixed(8).header("Status"))
970 .separator(" ")
971 .build();
972
973 let table = Table::new(spec, 80)
974 .header_from_columns()
975 .border(BorderStyle::Light);
976
977 let header = table.header_row();
978 assert!(header.contains("Name"));
979 assert!(header.contains("Status"));
980 }
981
982 #[test]
983 fn table_header_from_columns_fallback_to_key() {
984 let spec = TabularSpec::builder()
985 .column(Col::fixed(10).key("user_name"))
986 .column(Col::fixed(8).key("status"))
987 .separator(" ")
988 .build();
989
990 let table = Table::new(spec, 80).header_from_columns();
991
992 let header = table.header_row();
993 assert!(header.contains("user_name"));
994 assert!(header.contains("status"));
995 }
996
997 #[test]
998 fn table_header_from_columns_fallback_to_name() {
999 let spec = TabularSpec::builder()
1000 .column(Col::fixed(10).named("column1"))
1001 .column(Col::fixed(8).named("column2"))
1002 .separator(" ")
1003 .build();
1004
1005 let table = Table::new(spec, 80).header_from_columns();
1006
1007 let header = table.header_row();
1008 assert!(header.contains("column1"));
1009 assert!(header.contains("column2"));
1010 }
1011
1012 #[test]
1013 fn table_header_from_columns_priority_order() {
1014 let spec = TabularSpec::builder()
1016 .column(Col::fixed(10).header("Header").key("key").named("name"))
1017 .column(Col::fixed(10).key("key_only").named("name_only"))
1018 .column(Col::fixed(10).named("name_only2"))
1019 .separator(" ")
1020 .build();
1021
1022 let table = Table::new(spec, 80).header_from_columns();
1023
1024 let header = table.header_row();
1025 assert!(header.contains("Header")); assert!(header.contains("key_only")); assert!(header.contains("name_only2")); }
1029
1030 #[test]
1031 fn table_header_from_columns_in_render() {
1032 let spec = TabularSpec::builder()
1033 .column(Col::fixed(10).header("Name"))
1034 .column(Col::fixed(8).header("Value"))
1035 .separator(" ")
1036 .build();
1037
1038 let table = Table::new(spec, 80)
1039 .header_from_columns()
1040 .border(BorderStyle::Light);
1041
1042 let data = vec![vec!["Alice", "100"]];
1043 let output = table.render(&data);
1044
1045 assert!(output.contains("Name"));
1047 assert!(output.contains("Value"));
1048 assert!(output.contains("Alice"));
1049 assert!(output.contains("100"));
1050 }
1051}