1use std::sync::atomic::{AtomicUsize, Ordering};
39
40use super::formatter::{CellValue, OwnedCellValue, TabularFormatter};
41use super::traits::{Tabular, TabularRow};
42use super::types::{FlatDataSpec, TabularSpec};
43use super::util::display_width;
44
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
47pub enum BorderStyle {
48 #[default]
50 None,
51 Ascii,
53 Light,
55 Heavy,
57 Double,
59 Rounded,
61}
62
63impl BorderStyle {
64 fn chars(&self) -> BorderChars {
69 match self {
70 BorderStyle::None => BorderChars::empty(),
71 BorderStyle::Ascii => BorderChars {
72 horizontal: '-',
73 vertical: '|',
74 top_left: '+',
75 top_right: '+',
76 bottom_left: '+',
77 bottom_right: '+',
78 left_t: '+',
79 cross: '+',
80 right_t: '+',
81 top_t: '+',
82 bottom_t: '+',
83 },
84 BorderStyle::Light => BorderChars {
85 horizontal: '─',
86 vertical: '│',
87 top_left: '┌',
88 top_right: '┐',
89 bottom_left: '└',
90 bottom_right: '┘',
91 left_t: '├',
92 cross: '┼',
93 right_t: '┤',
94 top_t: '┬',
95 bottom_t: '┴',
96 },
97 BorderStyle::Heavy => BorderChars {
98 horizontal: '━',
99 vertical: '┃',
100 top_left: '┏',
101 top_right: '┓',
102 bottom_left: '┗',
103 bottom_right: '┛',
104 left_t: '┣',
105 cross: '╋',
106 right_t: '┫',
107 top_t: '┳',
108 bottom_t: '┻',
109 },
110 BorderStyle::Double => BorderChars {
111 horizontal: '═',
112 vertical: '║',
113 top_left: '╔',
114 top_right: '╗',
115 bottom_left: '╚',
116 bottom_right: '╝',
117 left_t: '╠',
118 cross: '╬',
119 right_t: '╣',
120 top_t: '╦',
121 bottom_t: '╩',
122 },
123 BorderStyle::Rounded => BorderChars {
124 horizontal: '─',
125 vertical: '│',
126 top_left: '╭',
127 top_right: '╮',
128 bottom_left: '╰',
129 bottom_right: '╯',
130 left_t: '├',
131 cross: '┼',
132 right_t: '┤',
133 top_t: '┬',
134 bottom_t: '┴',
135 },
136 }
137 }
138}
139
140#[derive(Clone, Copy, Debug)]
142struct BorderChars {
143 horizontal: char,
144 vertical: char,
145 top_left: char,
146 top_right: char,
147 bottom_left: char,
148 bottom_right: char,
149 left_t: char,
150 cross: char,
151 right_t: char,
152 top_t: char,
153 bottom_t: char,
154}
155
156impl BorderChars {
157 fn empty() -> Self {
158 BorderChars {
159 horizontal: ' ',
160 vertical: ' ',
161 top_left: ' ',
162 top_right: ' ',
163 bottom_left: ' ',
164 bottom_right: ' ',
165 left_t: ' ',
166 cross: ' ',
167 right_t: ' ',
168 top_t: ' ',
169 bottom_t: ' ',
170 }
171 }
172}
173
174pub struct Table {
180 formatter: TabularFormatter,
182 headers: Option<Vec<String>>,
184 border: BorderStyle,
186 header_style: Option<String>,
188 row_separator: bool,
190 row_styles: Option<(String, String)>,
193 row_counter: AtomicUsize,
195}
196
197impl Clone for Table {
198 fn clone(&self) -> Self {
199 Self {
200 formatter: self.formatter.clone(),
201 headers: self.headers.clone(),
202 border: self.border,
203 header_style: self.header_style.clone(),
204 row_separator: self.row_separator,
205 row_styles: self.row_styles.clone(),
206 row_counter: AtomicUsize::new(self.row_counter.load(Ordering::Relaxed)),
207 }
208 }
209}
210
211impl std::fmt::Debug for Table {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.debug_struct("Table")
214 .field("formatter", &self.formatter)
215 .field("headers", &self.headers)
216 .field("border", &self.border)
217 .field("header_style", &self.header_style)
218 .field("row_separator", &self.row_separator)
219 .field("row_styles", &self.row_styles)
220 .field("row_counter", &self.row_counter.load(Ordering::Relaxed))
221 .finish()
222 }
223}
224
225impl Table {
226 pub fn new(spec: TabularSpec, total_width: usize) -> Self {
228 let formatter = TabularFormatter::new(&spec, total_width);
229 Table {
230 formatter,
231 headers: None,
232 border: BorderStyle::None,
233 header_style: None,
234 row_separator: false,
235 row_styles: None,
236 row_counter: AtomicUsize::new(0),
237 }
238 }
239
240 pub fn from_spec(spec: &FlatDataSpec, total_width: usize) -> Self {
242 let formatter = TabularFormatter::new(spec, total_width);
243 Table {
244 formatter,
245 headers: None,
246 border: BorderStyle::None,
247 header_style: None,
248 row_separator: false,
249 row_styles: None,
250 row_counter: AtomicUsize::new(0),
251 }
252 }
253
254 pub fn from_type<T: Tabular>(total_width: usize) -> Self {
279 let spec = T::tabular_spec();
280 Self::new(spec, total_width)
281 }
282
283 pub fn border(mut self, border: BorderStyle) -> Self {
285 self.border = border;
286 self
287 }
288
289 pub fn header<S: Into<String>, I: IntoIterator<Item = S>>(mut self, headers: I) -> Self {
291 self.headers = Some(headers.into_iter().map(|s| s.into()).collect());
292 self
293 }
294
295 pub fn header_from_columns(mut self) -> Self {
317 self.headers = Some(self.formatter.extract_headers());
318 self
319 }
320
321 pub fn header_style(mut self, style: impl Into<String>) -> Self {
323 self.header_style = Some(style.into());
324 self
325 }
326
327 pub fn row_separator(mut self, enable: bool) -> Self {
329 self.row_separator = enable;
330 self
331 }
332
333 pub fn row_styles(
346 mut self,
347 even_style: impl Into<String>,
348 odd_style: impl Into<String>,
349 ) -> Self {
350 self.row_styles = Some((odd_style.into(), even_style.into()));
351 self
352 }
353
354 pub fn get_border(&self) -> BorderStyle {
356 self.border
357 }
358
359 pub fn num_columns(&self) -> usize {
361 self.formatter.num_columns()
362 }
363
364 pub fn row<S: AsRef<str>>(&self, values: &[S]) -> String {
366 let content = self.formatter.format_row(values);
367 self.wrap_data_row(&content)
368 }
369
370 pub fn row_cells(&self, values: &[CellValue<'_>]) -> String {
375 let content = self.formatter.format_row_cells(values);
376 self.wrap_data_row(&content)
377 }
378
379 pub fn row_from<T: serde::Serialize>(&self, value: &T) -> String {
397 let content = self.formatter.row_from(value);
398 self.wrap_data_row(&content)
399 }
400
401 pub fn row_from_trait<T: TabularRow>(&self, value: &T) -> String {
429 let content = self.formatter.row_from_trait(value);
430 self.wrap_data_row(&content)
431 }
432
433 pub fn header_row(&self) -> String {
435 match &self.headers {
436 Some(headers) => {
437 let content = self.formatter.format_row(headers);
439
440 let styled_content = if let Some(style) = &self.header_style {
442 format!("[{}]{}[/{}]", style, content, style)
443 } else {
444 content
445 };
446
447 self.wrap_row(&styled_content)
448 }
449 None => String::new(),
450 }
451 }
452
453 pub fn separator_row(&self) -> String {
455 self.horizontal_line(LineType::Middle)
456 }
457
458 pub fn top_border(&self) -> String {
460 self.horizontal_line(LineType::Top)
461 }
462
463 pub fn bottom_border(&self) -> String {
465 self.horizontal_line(LineType::Bottom)
466 }
467
468 fn wrap_data_row(&self, content: &str) -> String {
470 let bordered = self.wrap_row(content);
471 if let Some((odd_style, even_style)) = &self.row_styles {
472 let index = self.row_counter.fetch_add(1, Ordering::Relaxed);
473 let style = if index.is_multiple_of(2) {
474 even_style
475 } else {
476 odd_style
477 };
478 format!("[{}]{}[/{}]", style, bordered, style)
479 } else {
480 bordered
481 }
482 }
483
484 fn wrap_row(&self, content: &str) -> String {
486 if self.border == BorderStyle::None {
487 return content.to_string();
488 }
489
490 let chars = self.border.chars();
491 format!("{}{}{}", chars.vertical, content, chars.vertical)
492 }
493
494 fn horizontal_line(&self, line_type: LineType) -> String {
496 if self.border == BorderStyle::None {
497 return String::new();
498 }
499
500 let chars = self.border.chars();
501 let widths = self.formatter.widths();
502
503 let content_width: usize = widths.iter().sum();
505 let sep_width = display_width(&self.formatter_separator());
506 let num_seps = widths.len().saturating_sub(1);
507 let total_content = content_width + (num_seps * sep_width);
508
509 let (left, _joint, right) = match line_type {
510 LineType::Top => (chars.top_left, chars.top_t, chars.top_right),
511 LineType::Middle => (chars.left_t, chars.cross, chars.right_t),
512 LineType::Bottom => (chars.bottom_left, chars.bottom_t, chars.bottom_right),
513 };
514
515 let mut line = String::new();
516 line.push(left);
517
518 for (i, &width) in widths.iter().enumerate() {
519 if i > 0 {
520 for _ in 0..sep_width {
522 line.push(chars.horizontal);
523 }
524 }
527 for _ in 0..width {
528 line.push(chars.horizontal);
529 }
530 }
531
532 line = format!(
536 "{}{}{}",
537 left,
538 std::iter::repeat_n(chars.horizontal, total_content).collect::<String>(),
539 right
540 );
541
542 line
543 }
544
545 fn formatter_separator(&self) -> String {
547 use minijinja::value::{Object, Value};
549 use std::sync::Arc;
550 let arc_formatter = Arc::new(self.formatter.clone());
551 arc_formatter
552 .get_value(&Value::from("separator"))
553 .map(|v| v.to_string())
554 .unwrap_or_default()
555 }
556
557 pub fn render<S: AsRef<str>>(&self, rows: &[Vec<S>]) -> String {
561 self.row_counter.store(0, Ordering::Relaxed);
562 let mut output = Vec::new();
563
564 let top = self.top_border();
566 if !top.is_empty() {
567 output.push(top);
568 }
569
570 let header = self.header_row();
572 if !header.is_empty() {
573 output.push(header);
574
575 let sep = self.separator_row();
577 if !sep.is_empty() {
578 output.push(sep);
579 }
580 }
581
582 let separator = if self.row_separator {
584 let sep = self.separator_row();
585 if sep.is_empty() {
586 None
587 } else {
588 Some(sep)
589 }
590 } else {
591 None
592 };
593
594 for (i, row) in rows.iter().enumerate() {
595 if i > 0 {
596 if let Some(ref sep) = separator {
597 output.push(sep.clone());
598 }
599 }
600 output.push(self.row(row));
601 }
602
603 let bottom = self.bottom_border();
605 if !bottom.is_empty() {
606 output.push(bottom);
607 }
608
609 output.join("\n")
610 }
611}
612
613#[derive(Clone, Copy, Debug, PartialEq, Eq)]
615enum LineType {
616 Top,
617 Middle,
618 Bottom,
619}
620
621impl minijinja::value::Object for Table {
626 fn get_value(self: &std::sync::Arc<Self>, key: &minijinja::Value) -> Option<minijinja::Value> {
627 match key.as_str()? {
628 "num_columns" => Some(minijinja::Value::from(self.num_columns())),
629 "border" => Some(minijinja::Value::from(format!("{:?}", self.get_border()))),
630 _ => None,
631 }
632 }
633
634 fn enumerate(self: &std::sync::Arc<Self>) -> minijinja::value::Enumerator {
635 minijinja::value::Enumerator::Str(&["num_columns", "border"])
636 }
637
638 fn call_method(
639 self: &std::sync::Arc<Self>,
640 _state: &minijinja::State,
641 name: &str,
642 args: &[minijinja::Value],
643 ) -> Result<minijinja::Value, minijinja::Error> {
644 match name {
645 "row" => {
646 if args.is_empty() {
648 return Err(minijinja::Error::new(
649 minijinja::ErrorKind::MissingArgument,
650 "row() requires an array of values",
651 ));
652 }
653
654 let values_arg = &args[0];
655
656 if self.formatter.has_sub_columns() {
657 let outer_iter = match values_arg.try_iter() {
659 Ok(iter) => iter,
660 Err(_) => {
661 let values = vec![values_arg.to_string()];
662 return Ok(minijinja::Value::from(self.row(&values)));
663 }
664 };
665
666 let mut owned_values: Vec<OwnedCellValue> = Vec::new();
667 for (i, v) in outer_iter.enumerate() {
668 let is_sub_col = self
669 .formatter
670 .columns()
671 .get(i)
672 .and_then(|c| c.sub_columns.as_ref())
673 .is_some();
674
675 if is_sub_col {
676 if let Ok(inner_iter) = v.try_iter() {
677 let sub_vals: Vec<String> =
678 inner_iter.map(|iv| iv.to_string()).collect();
679 owned_values.push(OwnedCellValue::Sub(sub_vals));
680 } else {
681 owned_values.push(OwnedCellValue::Single(v.to_string()));
682 }
683 } else {
684 owned_values.push(OwnedCellValue::Single(v.to_string()));
685 }
686 }
687
688 let cell_values: Vec<CellValue<'_>> = owned_values
689 .iter()
690 .map(|ov| match ov {
691 OwnedCellValue::Single(s) => CellValue::Single(s.as_str()),
692 OwnedCellValue::Sub(v) => {
693 CellValue::Sub(v.iter().map(|s| s.as_str()).collect())
694 }
695 })
696 .collect();
697
698 let formatted = self.row_cells(&cell_values);
699 Ok(minijinja::Value::from(formatted))
700 } else {
701 let values: Vec<String> = match values_arg.try_iter() {
702 Ok(iter) => iter.map(|v| v.to_string()).collect(),
703 Err(_) => vec![values_arg.to_string()],
704 };
705
706 let formatted = self.row(&values);
707 Ok(minijinja::Value::from(formatted))
708 }
709 }
710 "row_from" => {
711 if args.is_empty() {
713 return Err(minijinja::Error::new(
714 minijinja::ErrorKind::MissingArgument,
715 "row_from() requires an object argument",
716 ));
717 }
718
719 let json_value = minijinja::value::Value::from_serialize(&args[0]);
721 let formatted = self.formatter.row_from(&json_value);
722 Ok(minijinja::Value::from(self.wrap_data_row(&formatted)))
723 }
724 "header_row" => {
725 Ok(minijinja::Value::from(self.header_row()))
727 }
728 "separator_row" => {
729 Ok(minijinja::Value::from(self.separator_row()))
731 }
732 "top_border" => {
733 Ok(minijinja::Value::from(self.top_border()))
735 }
736 "bottom_border" => {
737 Ok(minijinja::Value::from(self.bottom_border()))
739 }
740 "render_all" => {
741 if args.is_empty() {
743 return Err(minijinja::Error::new(
744 minijinja::ErrorKind::MissingArgument,
745 "render_all() requires an array of rows",
746 ));
747 }
748
749 let rows_iter = args[0].try_iter().map_err(|_| {
750 minijinja::Error::new(
751 minijinja::ErrorKind::InvalidOperation,
752 "render_all() requires an array of rows",
753 )
754 })?;
755
756 let rows: Vec<Vec<String>> = rows_iter
757 .map(|row| {
758 row.try_iter()
759 .map(|iter| iter.map(|v| v.to_string()).collect())
760 .unwrap_or_else(|_| vec![row.to_string()])
761 })
762 .collect();
763
764 let formatted = Table::render(self, &rows);
765 Ok(minijinja::Value::from(formatted))
766 }
767 _ => Err(minijinja::Error::new(
768 minijinja::ErrorKind::UnknownMethod,
769 format!("Table has no method '{}'", name),
770 )),
771 }
772 }
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778 use crate::tabular::Col;
779
780 fn simple_spec() -> TabularSpec {
781 TabularSpec::builder()
782 .column(Col::fixed(10))
783 .column(Col::fixed(8))
784 .separator(" ")
785 .build()
786 }
787
788 #[test]
789 fn table_no_border() {
790 let table = Table::new(simple_spec(), 80);
791 let row = table.row(&["Hello", "World"]);
792 assert!(!row.contains('│'));
794 assert!(row.contains("Hello"));
795 }
796
797 #[test]
798 fn table_with_ascii_border() {
799 let table = Table::new(simple_spec(), 80).border(BorderStyle::Ascii);
800 let row = table.row(&["Hello", "World"]);
801 assert!(row.starts_with('|'));
803 assert!(row.ends_with('|'));
804 }
805
806 #[test]
807 fn table_with_light_border() {
808 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
809 let row = table.row(&["Hello", "World"]);
810 assert!(row.starts_with('│'));
812 assert!(row.ends_with('│'));
813 }
814
815 #[test]
816 fn table_with_heavy_border() {
817 let table = Table::new(simple_spec(), 80).border(BorderStyle::Heavy);
818 let row = table.row(&["Hello", "World"]);
819 assert!(row.starts_with('┃'));
820 assert!(row.ends_with('┃'));
821 }
822
823 #[test]
824 fn table_with_double_border() {
825 let table = Table::new(simple_spec(), 80).border(BorderStyle::Double);
826 let row = table.row(&["Hello", "World"]);
827 assert!(row.starts_with('║'));
828 assert!(row.ends_with('║'));
829 }
830
831 #[test]
832 fn table_with_rounded_border() {
833 let table = Table::new(simple_spec(), 80).border(BorderStyle::Rounded);
834 let row = table.row(&["Hello", "World"]);
835 assert!(row.starts_with('│'));
836 assert!(row.ends_with('│'));
837 }
838
839 #[test]
840 fn table_header_row() {
841 let table = Table::new(simple_spec(), 80)
842 .border(BorderStyle::Light)
843 .header(vec!["Name", "Status"]);
844
845 let header = table.header_row();
846 assert!(header.contains("Name"));
847 assert!(header.contains("Status"));
848 assert!(header.starts_with('│'));
849 }
850
851 #[test]
852 fn table_header_with_style() {
853 let table = Table::new(simple_spec(), 80)
854 .header(vec!["Name", "Status"])
855 .header_style("header");
856
857 let header = table.header_row();
858 assert!(header.contains("[header]"));
859 assert!(header.contains("[/header]"));
860 }
861
862 #[test]
863 fn table_no_header() {
864 let table = Table::new(simple_spec(), 80);
865 let header = table.header_row();
866 assert!(header.is_empty());
867 }
868
869 #[test]
870 fn table_separator_row() {
871 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
872 let sep = table.separator_row();
873 assert!(sep.contains('─'));
874 assert!(sep.starts_with('├'));
875 assert!(sep.ends_with('┤'));
876 }
877
878 #[test]
879 fn table_top_border() {
880 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
881 let top = table.top_border();
882 assert!(top.contains('─'));
883 assert!(top.starts_with('┌'));
884 assert!(top.ends_with('┐'));
885 }
886
887 #[test]
888 fn table_bottom_border() {
889 let table = Table::new(simple_spec(), 80).border(BorderStyle::Light);
890 let bottom = table.bottom_border();
891 assert!(bottom.contains('─'));
892 assert!(bottom.starts_with('└'));
893 assert!(bottom.ends_with('┘'));
894 }
895
896 #[test]
897 fn table_render_full() {
898 let table = Table::new(simple_spec(), 80)
899 .border(BorderStyle::Light)
900 .header(vec!["Name", "Value"]);
901
902 let data = vec![vec!["Alice", "100"], vec!["Bob", "200"]];
903
904 let output = table.render(&data);
905 let lines: Vec<&str> = output.lines().collect();
906
907 assert!(lines.len() >= 5);
909
910 assert!(lines[0].starts_with('┌'));
912 assert!(lines[1].contains("Name"));
914 assert!(lines[2].starts_with('├'));
916 assert!(lines[3].contains("Alice"));
918 assert!(lines[4].contains("Bob"));
919 assert!(lines[5].starts_with('└'));
921 }
922
923 #[test]
924 fn table_render_no_border() {
925 let table = Table::new(simple_spec(), 80).header(vec!["Name", "Value"]);
926
927 let data = vec![vec!["Alice", "100"]];
928
929 let output = table.render(&data);
930 let lines: Vec<&str> = output.lines().collect();
931
932 assert!(lines.len() >= 2);
934 assert!(lines[0].contains("Name"));
935 assert!(lines[1].contains("Alice"));
936 }
937
938 #[test]
939 fn border_style_default() {
940 assert_eq!(BorderStyle::default(), BorderStyle::None);
941 }
942
943 #[test]
944 fn table_accessors() {
945 let table = Table::new(simple_spec(), 80).border(BorderStyle::Ascii);
946
947 assert_eq!(table.get_border(), BorderStyle::Ascii);
948 assert_eq!(table.num_columns(), 2);
949 }
950
951 #[test]
952 fn table_row_from() {
953 use serde::Serialize;
954
955 #[derive(Serialize)]
956 struct Record {
957 name: String,
958 status: String,
959 }
960
961 let spec = TabularSpec::builder()
962 .column(Col::fixed(10).key("name"))
963 .column(Col::fixed(8).key("status"))
964 .separator(" ")
965 .build();
966
967 let table = Table::new(spec, 80);
968 let record = Record {
969 name: "Alice".to_string(),
970 status: "active".to_string(),
971 };
972
973 let row = table.row_from(&record);
974 assert!(row.contains("Alice"));
975 assert!(row.contains("active"));
976 }
977
978 #[test]
979 fn table_row_from_with_border() {
980 use serde::Serialize;
981
982 #[derive(Serialize)]
983 struct Item {
984 id: u32,
985 value: String,
986 }
987
988 let spec = TabularSpec::builder()
989 .column(Col::fixed(5).key("id"))
990 .column(Col::fixed(10).key("value"))
991 .build();
992
993 let table = Table::new(spec, 80).border(BorderStyle::Light);
994 let item = Item {
995 id: 42,
996 value: "test".to_string(),
997 };
998
999 let row = table.row_from(&item);
1000 assert!(row.starts_with('│'));
1001 assert!(row.ends_with('│'));
1002 assert!(row.contains("42"));
1003 assert!(row.contains("test"));
1004 }
1005
1006 #[test]
1007 fn table_row_separator_option() {
1008 let spec = TabularSpec::builder()
1009 .column(Col::fixed(10))
1010 .column(Col::fixed(8))
1011 .build();
1012
1013 let table = Table::new(spec, 80)
1014 .border(BorderStyle::Light)
1015 .row_separator(true);
1016
1017 let data = vec![vec!["A", "1"], vec!["B", "2"], vec!["C", "3"]];
1018 let output = table.render(&data);
1019 let lines: Vec<&str> = output.lines().collect();
1020
1021 let sep_count = lines.iter().filter(|l| l.starts_with('├')).count();
1024 assert_eq!(sep_count, 2, "Expected 2 separators between 3 rows");
1025 }
1026
1027 #[test]
1028 fn table_row_separator_disabled_by_default() {
1029 let spec = TabularSpec::builder()
1030 .column(Col::fixed(10))
1031 .column(Col::fixed(8))
1032 .build();
1033
1034 let table = Table::new(spec, 80).border(BorderStyle::Light);
1035
1036 let data = vec![vec!["A", "1"], vec!["B", "2"]];
1037 let output = table.render(&data);
1038 let lines: Vec<&str> = output.lines().collect();
1039
1040 assert_eq!(lines.len(), 4);
1043 }
1044
1045 #[test]
1046 fn table_header_from_columns_with_header_field() {
1047 let spec = TabularSpec::builder()
1048 .column(Col::fixed(10).header("Name"))
1049 .column(Col::fixed(8).header("Status"))
1050 .separator(" ")
1051 .build();
1052
1053 let table = Table::new(spec, 80)
1054 .header_from_columns()
1055 .border(BorderStyle::Light);
1056
1057 let header = table.header_row();
1058 assert!(header.contains("Name"));
1059 assert!(header.contains("Status"));
1060 }
1061
1062 #[test]
1063 fn table_header_from_columns_fallback_to_key() {
1064 let spec = TabularSpec::builder()
1065 .column(Col::fixed(10).key("user_name"))
1066 .column(Col::fixed(8).key("status"))
1067 .separator(" ")
1068 .build();
1069
1070 let table = Table::new(spec, 80).header_from_columns();
1071
1072 let header = table.header_row();
1073 assert!(header.contains("user_name"));
1074 assert!(header.contains("status"));
1075 }
1076
1077 #[test]
1078 fn table_header_from_columns_fallback_to_name() {
1079 let spec = TabularSpec::builder()
1080 .column(Col::fixed(10).named("column1"))
1081 .column(Col::fixed(8).named("column2"))
1082 .separator(" ")
1083 .build();
1084
1085 let table = Table::new(spec, 80).header_from_columns();
1086
1087 let header = table.header_row();
1088 assert!(header.contains("column1"));
1089 assert!(header.contains("column2"));
1090 }
1091
1092 #[test]
1093 fn table_header_from_columns_priority_order() {
1094 let spec = TabularSpec::builder()
1096 .column(Col::fixed(10).header("Header").key("key").named("name"))
1097 .column(Col::fixed(10).key("key_only").named("name_only"))
1098 .column(Col::fixed(10).named("name_only2"))
1099 .separator(" ")
1100 .build();
1101
1102 let table = Table::new(spec, 80).header_from_columns();
1103
1104 let header = table.header_row();
1105 assert!(header.contains("Header")); assert!(header.contains("key_only")); assert!(header.contains("name_only2")); }
1109
1110 #[test]
1111 fn table_header_from_columns_in_render() {
1112 let spec = TabularSpec::builder()
1113 .column(Col::fixed(10).header("Name"))
1114 .column(Col::fixed(8).header("Value"))
1115 .separator(" ")
1116 .build();
1117
1118 let table = Table::new(spec, 80)
1119 .header_from_columns()
1120 .border(BorderStyle::Light);
1121
1122 let data = vec![vec!["Alice", "100"]];
1123 let output = table.render(&data);
1124
1125 assert!(output.contains("Name"));
1127 assert!(output.contains("Value"));
1128 assert!(output.contains("Alice"));
1129 assert!(output.contains("100"));
1130 }
1131}