1use minijinja::value::{Enumerator, Object, Value};
41use serde::Serialize;
42use serde_json::Value as JsonValue;
43use std::sync::Arc;
44
45use super::resolve::ResolvedWidths;
46use super::traits::TabularRow;
47use super::types::{Align, Anchor, Column, FlatDataSpec, Overflow, TabularSpec, TruncateAt};
48use super::util::{
49 display_width, pad_center, pad_left, pad_right, truncate_end, truncate_middle, truncate_start,
50 wrap_indent,
51};
52
53#[derive(Clone, Debug)]
80pub struct TabularFormatter {
81 columns: Vec<Column>,
83 widths: Vec<usize>,
85 separator: String,
87 prefix: String,
89 suffix: String,
91 total_width: usize,
93}
94
95impl TabularFormatter {
96 pub fn new(spec: &FlatDataSpec, total_width: usize) -> Self {
103 let resolved = spec.resolve_widths(total_width);
104 Self::from_resolved_with_width(spec, resolved, total_width)
105 }
106
107 pub fn from_resolved(spec: &FlatDataSpec, resolved: ResolvedWidths) -> Self {
111 let content_width: usize = resolved.widths.iter().sum();
113 let overhead = spec.decorations.overhead(resolved.widths.len());
114 let total_width = content_width + overhead;
115 Self::from_resolved_with_width(spec, resolved, total_width)
116 }
117
118 pub fn from_resolved_with_width(
120 spec: &FlatDataSpec,
121 resolved: ResolvedWidths,
122 total_width: usize,
123 ) -> Self {
124 TabularFormatter {
125 columns: spec.columns.clone(),
126 widths: resolved.widths,
127 separator: spec.decorations.column_sep.clone(),
128 prefix: spec.decorations.row_prefix.clone(),
129 suffix: spec.decorations.row_suffix.clone(),
130 total_width,
131 }
132 }
133
134 pub fn with_widths(columns: Vec<Column>, widths: Vec<usize>) -> Self {
138 let total_width = widths.iter().sum();
139 TabularFormatter {
140 columns,
141 widths,
142 separator: String::new(),
143 prefix: String::new(),
144 suffix: String::new(),
145 total_width,
146 }
147 }
148
149 pub fn from_type<T: super::traits::Tabular>(total_width: usize) -> Self {
171 let spec: TabularSpec = T::tabular_spec();
172 Self::new(&spec, total_width)
173 }
174
175 pub fn total_width(mut self, width: usize) -> Self {
177 self.total_width = width;
178 self
179 }
180
181 pub fn separator(mut self, sep: impl Into<String>) -> Self {
183 self.separator = sep.into();
184 self
185 }
186
187 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
189 self.prefix = prefix.into();
190 self
191 }
192
193 pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
195 self.suffix = suffix.into();
196 self
197 }
198
199 pub fn format_row<S: AsRef<str>>(&self, values: &[S]) -> String {
224 let mut result = String::new();
225 result.push_str(&self.prefix);
226
227 let (anchor_gap, anchor_transition) = self.calculate_anchor_gap();
229
230 for (i, col) in self.columns.iter().enumerate() {
231 if i > 0 {
233 if anchor_gap > 0 && i == anchor_transition {
234 result.push_str(&" ".repeat(anchor_gap));
236 } else {
237 result.push_str(&self.separator);
238 }
239 }
240
241 let width = self.widths.get(i).copied().unwrap_or(0);
242 let value = values.get(i).map(|s| s.as_ref()).unwrap_or(&col.null_repr);
243
244 let formatted = format_cell(value, width, col);
245 result.push_str(&formatted);
246 }
247
248 result.push_str(&self.suffix);
249 result
250 }
251
252 fn calculate_anchor_gap(&self) -> (usize, usize) {
258 let transition = self
260 .columns
261 .iter()
262 .position(|c| c.anchor == Anchor::Right)
263 .unwrap_or(self.columns.len());
264
265 if transition == 0 || transition == self.columns.len() {
267 return (0, transition);
268 }
269
270 let prefix_width = display_width(&self.prefix);
272 let suffix_width = display_width(&self.suffix);
273 let sep_width = display_width(&self.separator);
274 let content_width: usize = self.widths.iter().sum();
275 let num_seps = self.columns.len().saturating_sub(1);
276 let current_total = prefix_width + content_width + (num_seps * sep_width) + suffix_width;
277
278 if current_total >= self.total_width {
280 (0, transition)
282 } else {
283 let extra = self.total_width - current_total;
285 (extra + sep_width, transition)
287 }
288 }
289
290 pub fn format_rows<S: AsRef<str>>(&self, rows: &[Vec<S>]) -> Vec<String> {
294 rows.iter().map(|row| self.format_row(row)).collect()
295 }
296
297 pub fn format_row_lines<S: AsRef<str>>(&self, values: &[S]) -> Vec<String> {
318 let cell_outputs: Vec<CellOutput> = self
320 .columns
321 .iter()
322 .enumerate()
323 .map(|(i, col)| {
324 let width = self.widths.get(i).copied().unwrap_or(0);
325 let value = values.get(i).map(|s| s.as_ref()).unwrap_or(&col.null_repr);
326 format_cell_lines(value, width, col)
327 })
328 .collect();
329
330 let max_lines = cell_outputs
332 .iter()
333 .map(|c| c.line_count())
334 .max()
335 .unwrap_or(1);
336
337 if max_lines == 1 {
339 return vec![self.format_row(values)];
340 }
341
342 let (anchor_gap, anchor_transition) = self.calculate_anchor_gap();
344 let mut output = Vec::with_capacity(max_lines);
345
346 for line_idx in 0..max_lines {
347 let mut row = String::new();
348 row.push_str(&self.prefix);
349
350 for (i, (cell, col)) in cell_outputs.iter().zip(self.columns.iter()).enumerate() {
351 if i > 0 {
352 if anchor_gap > 0 && i == anchor_transition {
353 row.push_str(&" ".repeat(anchor_gap));
354 } else {
355 row.push_str(&self.separator);
356 }
357 }
358
359 let width = self.widths.get(i).copied().unwrap_or(0);
360 let line = cell.line(line_idx, width, col.align);
361 row.push_str(&line);
362 }
363
364 row.push_str(&self.suffix);
365 output.push(row);
366 }
367
368 output
369 }
370
371 pub fn column_width(&self, index: usize) -> Option<usize> {
373 self.widths.get(index).copied()
374 }
375
376 pub fn widths(&self) -> &[usize] {
378 &self.widths
379 }
380
381 pub fn num_columns(&self) -> usize {
383 self.columns.len()
384 }
385
386 pub fn extract_headers(&self) -> Vec<String> {
396 self.columns
397 .iter()
398 .map(|col| {
399 col.header
400 .as_deref()
401 .or(col.key.as_deref())
402 .or(col.name.as_deref())
403 .unwrap_or("")
404 .to_string()
405 })
406 .collect()
407 }
408
409 pub fn row_from<T: Serialize>(&self, value: &T) -> String {
452 let values = self.extract_values(value);
453 let string_refs: Vec<&str> = values.iter().map(|s| s.as_str()).collect();
454 self.format_row(&string_refs)
455 }
456
457 pub fn row_lines_from<T: Serialize>(&self, value: &T) -> Vec<String> {
462 let values = self.extract_values(value);
463 let string_refs: Vec<&str> = values.iter().map(|s| s.as_str()).collect();
464 self.format_row_lines(&string_refs)
465 }
466
467 pub fn row_from_trait<T: TabularRow>(&self, value: &T) -> String {
492 let values = value.to_row();
493 self.format_row(&values)
494 }
495
496 pub fn row_lines_from_trait<T: TabularRow>(&self, value: &T) -> Vec<String> {
501 let values = value.to_row();
502 self.format_row_lines(&values)
503 }
504
505 fn extract_values<T: Serialize>(&self, value: &T) -> Vec<String> {
507 let json = match serde_json::to_value(value) {
509 Ok(v) => v,
510 Err(_) => return vec![String::new(); self.columns.len()],
511 };
512
513 self.columns
514 .iter()
515 .map(|col| {
516 let key = col.key.as_ref().or(col.name.as_ref());
518
519 match key {
520 Some(k) => extract_field(&json, k),
521 None => col.null_repr.clone(),
522 }
523 })
524 .collect()
525 }
526}
527
528fn extract_field(value: &JsonValue, path: &str) -> String {
532 let mut current = value;
533
534 for part in path.split('.') {
535 match current {
536 JsonValue::Object(map) => {
537 current = match map.get(part) {
538 Some(v) => v,
539 None => return String::new(),
540 };
541 }
542 JsonValue::Array(arr) => {
543 if let Ok(idx) = part.parse::<usize>() {
545 current = match arr.get(idx) {
546 Some(v) => v,
547 None => return String::new(),
548 };
549 } else {
550 return String::new();
551 }
552 }
553 _ => return String::new(),
554 }
555 }
556
557 match current {
559 JsonValue::String(s) => s.clone(),
560 JsonValue::Number(n) => n.to_string(),
561 JsonValue::Bool(b) => b.to_string(),
562 JsonValue::Null => String::new(),
563 _ => current.to_string(),
565 }
566}
567
568impl Object for TabularFormatter {
573 fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
574 match key.as_str()? {
575 "num_columns" => Some(Value::from(self.num_columns())),
576 "widths" => {
577 let widths: Vec<Value> = self.widths.iter().map(|&w| Value::from(w)).collect();
578 Some(Value::from(widths))
579 }
580 "separator" => Some(Value::from(self.separator.clone())),
581 _ => None,
582 }
583 }
584
585 fn enumerate(self: &Arc<Self>) -> Enumerator {
586 Enumerator::Str(&["num_columns", "widths", "separator"])
587 }
588
589 fn call_method(
590 self: &Arc<Self>,
591 _state: &minijinja::State,
592 name: &str,
593 args: &[Value],
594 ) -> Result<Value, minijinja::Error> {
595 match name {
596 "row" => {
597 if args.is_empty() {
599 return Err(minijinja::Error::new(
600 minijinja::ErrorKind::MissingArgument,
601 "row() requires an array of values",
602 ));
603 }
604
605 let values_arg = &args[0];
606
607 let values: Vec<String> = match values_arg.try_iter() {
609 Ok(iter) => iter.map(|v| v.to_string()).collect(),
610 Err(_) => {
611 vec![values_arg.to_string()]
613 }
614 };
615
616 let formatted = self.format_row(&values);
617 Ok(Value::from(formatted))
618 }
619 "column_width" => {
620 if args.is_empty() {
622 return Err(minijinja::Error::new(
623 minijinja::ErrorKind::MissingArgument,
624 "column_width() requires an index argument",
625 ));
626 }
627
628 let index = args[0].as_usize().ok_or_else(|| {
629 minijinja::Error::new(
630 minijinja::ErrorKind::InvalidOperation,
631 "column_width() index must be a number",
632 )
633 })?;
634
635 match self.column_width(index) {
636 Some(w) => Ok(Value::from(w)),
637 None => Ok(Value::from(())),
638 }
639 }
640 _ => Err(minijinja::Error::new(
641 minijinja::ErrorKind::UnknownMethod,
642 format!("TabularFormatter has no method '{}'", name),
643 )),
644 }
645 }
646}
647
648fn format_cell(value: &str, width: usize, col: &Column) -> String {
650 let style_override = if col.style_from_value {
652 Some(value)
653 } else {
654 None
655 };
656 format_cell_styled(value, width, col, style_override)
657}
658
659fn format_cell_styled(
664 value: &str,
665 width: usize,
666 col: &Column,
667 style_override: Option<&str>,
668) -> String {
669 if width == 0 {
670 return String::new();
671 }
672
673 let current_width = display_width(value);
674
675 let processed = if current_width > width {
677 match &col.overflow {
678 Overflow::Truncate { at, marker } => match at {
679 TruncateAt::End => truncate_end(value, width, marker),
680 TruncateAt::Start => truncate_start(value, width, marker),
681 TruncateAt::Middle => truncate_middle(value, width, marker),
682 },
683 Overflow::Clip => {
684 truncate_end(value, width, "")
686 }
687 Overflow::Expand => {
688 value.to_string()
690 }
691 Overflow::Wrap { .. } => {
692 truncate_end(value, width, "…")
695 }
696 }
697 } else {
698 value.to_string()
699 };
700
701 let padded = if matches!(col.overflow, Overflow::Expand) && current_width > width {
703 processed
704 } else {
705 match col.align {
706 Align::Left => pad_right(&processed, width),
707 Align::Right => pad_left(&processed, width),
708 Align::Center => pad_center(&processed, width),
709 }
710 };
711
712 let style = style_override.or(col.style.as_deref());
714 match style {
715 Some(s) if !s.is_empty() => format!("[{}]{}[/{}]", s, padded, s),
716 _ => padded,
717 }
718}
719
720#[derive(Clone, Debug, PartialEq, Eq)]
722pub enum CellOutput {
723 Single(String),
725 Multi(Vec<String>),
727}
728
729impl CellOutput {
730 pub fn is_single(&self) -> bool {
732 matches!(self, CellOutput::Single(_))
733 }
734
735 pub fn line_count(&self) -> usize {
737 match self {
738 CellOutput::Single(_) => 1,
739 CellOutput::Multi(lines) => lines.len().max(1),
740 }
741 }
742
743 pub fn line(&self, index: usize, width: usize, align: Align) -> String {
745 let content = match self {
746 CellOutput::Single(s) if index == 0 => s.as_str(),
747 CellOutput::Multi(lines) => lines.get(index).map(|s| s.as_str()).unwrap_or(""),
748 _ => "",
749 };
750
751 match align {
753 Align::Left => pad_right(content, width),
754 Align::Right => pad_left(content, width),
755 Align::Center => pad_center(content, width),
756 }
757 }
758
759 pub fn to_single(&self) -> String {
761 match self {
762 CellOutput::Single(s) => s.clone(),
763 CellOutput::Multi(lines) => lines.first().cloned().unwrap_or_default(),
764 }
765 }
766}
767
768fn apply_style(content: &str, style: Option<&str>) -> String {
770 match style {
771 Some(s) if !s.is_empty() => format!("[{}]{}[/{}]", s, content, s),
772 _ => content.to_string(),
773 }
774}
775
776fn format_cell_lines(value: &str, width: usize, col: &Column) -> CellOutput {
778 if width == 0 {
779 return CellOutput::Single(String::new());
780 }
781
782 let current_width = display_width(value);
783
784 let style = if col.style_from_value {
786 Some(value)
787 } else {
788 col.style.as_deref()
789 };
790
791 match &col.overflow {
792 Overflow::Wrap { indent } => {
793 if current_width <= width {
794 let padded = match col.align {
796 Align::Left => pad_right(value, width),
797 Align::Right => pad_left(value, width),
798 Align::Center => pad_center(value, width),
799 };
800 CellOutput::Single(apply_style(&padded, style))
801 } else {
802 let wrapped = wrap_indent(value, width, *indent);
804 let padded: Vec<String> = wrapped
805 .into_iter()
806 .map(|line| {
807 let padded_line = match col.align {
808 Align::Left => pad_right(&line, width),
809 Align::Right => pad_left(&line, width),
810 Align::Center => pad_center(&line, width),
811 };
812 apply_style(&padded_line, style)
813 })
814 .collect();
815 if padded.len() == 1 {
816 CellOutput::Single(padded.into_iter().next().unwrap())
817 } else {
818 CellOutput::Multi(padded)
819 }
820 }
821 }
822 _ => CellOutput::Single(format_cell(value, width, col)),
824 }
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830 use crate::tabular::{TabularSpec, Width};
831
832 fn simple_spec() -> FlatDataSpec {
833 FlatDataSpec::builder()
834 .column(Column::new(Width::Fixed(10)))
835 .column(Column::new(Width::Fixed(8)))
836 .separator(" | ")
837 .build()
838 }
839
840 #[test]
841 fn format_basic_row() {
842 let formatter = TabularFormatter::new(&simple_spec(), 80);
843 let output = formatter.format_row(&["Hello", "World"]);
844 assert_eq!(output, "Hello | World ");
845 }
846
847 #[test]
848 fn format_row_with_truncation() {
849 let spec = FlatDataSpec::builder()
850 .column(Column::new(Width::Fixed(8)))
851 .build();
852 let formatter = TabularFormatter::new(&spec, 80);
853
854 let output = formatter.format_row(&["Hello World"]);
855 assert_eq!(output, "Hello W…");
856 }
857
858 #[test]
859 fn format_row_right_align() {
860 let spec = FlatDataSpec::builder()
861 .column(Column::new(Width::Fixed(10)).align(Align::Right))
862 .build();
863 let formatter = TabularFormatter::new(&spec, 80);
864
865 let output = formatter.format_row(&["42"]);
866 assert_eq!(output, " 42");
867 }
868
869 #[test]
870 fn format_row_center_align() {
871 let spec = FlatDataSpec::builder()
872 .column(Column::new(Width::Fixed(10)).align(Align::Center))
873 .build();
874 let formatter = TabularFormatter::new(&spec, 80);
875
876 let output = formatter.format_row(&["hi"]);
877 assert_eq!(output, " hi ");
878 }
879
880 #[test]
881 fn format_row_truncate_start() {
882 let spec = FlatDataSpec::builder()
883 .column(Column::new(Width::Fixed(10)).truncate(TruncateAt::Start))
884 .build();
885 let formatter = TabularFormatter::new(&spec, 80);
886
887 let output = formatter.format_row(&["/path/to/file.rs"]);
888 assert_eq!(display_width(&output), 10);
889 assert!(output.starts_with("…"));
890 }
891
892 #[test]
893 fn format_row_truncate_middle() {
894 let spec = FlatDataSpec::builder()
895 .column(Column::new(Width::Fixed(10)).truncate(TruncateAt::Middle))
896 .build();
897 let formatter = TabularFormatter::new(&spec, 80);
898
899 let output = formatter.format_row(&["abcdefghijklmno"]);
900 assert_eq!(display_width(&output), 10);
901 assert!(output.contains("…"));
902 }
903
904 #[test]
905 fn format_row_with_null() {
906 let spec = FlatDataSpec::builder()
907 .column(Column::new(Width::Fixed(10)))
908 .column(Column::new(Width::Fixed(8)).null_repr("N/A"))
909 .separator(" ")
910 .build();
911 let formatter = TabularFormatter::new(&spec, 80);
912
913 let output = formatter.format_row(&["value"]);
915 assert!(output.contains("N/A"));
916 }
917
918 #[test]
919 fn format_row_with_decorations() {
920 let spec = FlatDataSpec::builder()
921 .column(Column::new(Width::Fixed(10)))
922 .column(Column::new(Width::Fixed(8)))
923 .separator(" │ ")
924 .prefix("│ ")
925 .suffix(" │")
926 .build();
927 let formatter = TabularFormatter::new(&spec, 80);
928
929 let output = formatter.format_row(&["Hello", "World"]);
930 assert!(output.starts_with("│ "));
931 assert!(output.ends_with(" │"));
932 assert!(output.contains(" │ "));
933 }
934
935 #[test]
936 fn format_multiple_rows() {
937 let formatter = TabularFormatter::new(&simple_spec(), 80);
938 let rows = vec![vec!["a", "1"], vec!["b", "2"], vec!["c", "3"]];
939
940 let output = formatter.format_rows(&rows);
941 assert_eq!(output.len(), 3);
942 }
943
944 #[test]
945 fn format_row_fill_column() {
946 let spec = FlatDataSpec::builder()
947 .column(Column::new(Width::Fixed(5)))
948 .column(Column::new(Width::Fill))
949 .column(Column::new(Width::Fixed(5)))
950 .separator(" ")
951 .build();
952
953 let formatter = TabularFormatter::new(&spec, 30);
955 let _output = formatter.format_row(&["abc", "middle", "xyz"]);
956
957 assert_eq!(formatter.widths(), &[5, 16, 5]);
959 }
960
961 #[test]
962 fn formatter_accessors() {
963 let spec = FlatDataSpec::builder()
964 .column(Column::new(Width::Fixed(10)))
965 .column(Column::new(Width::Fixed(8)))
966 .build();
967 let formatter = TabularFormatter::new(&spec, 80);
968
969 assert_eq!(formatter.num_columns(), 2);
970 assert_eq!(formatter.column_width(0), Some(10));
971 assert_eq!(formatter.column_width(1), Some(8));
972 assert_eq!(formatter.column_width(2), None);
973 }
974
975 #[test]
976 fn format_empty_spec() {
977 let spec = FlatDataSpec::builder().build();
978 let formatter = TabularFormatter::new(&spec, 80);
979
980 let output = formatter.format_row::<&str>(&[]);
981 assert_eq!(output, "");
982 }
983
984 #[test]
985 fn format_with_ansi() {
986 let spec = FlatDataSpec::builder()
987 .column(Column::new(Width::Fixed(10)))
988 .build();
989 let formatter = TabularFormatter::new(&spec, 80);
990
991 let styled = "\x1b[31mred\x1b[0m";
992 let output = formatter.format_row(&[styled]);
993
994 assert!(output.contains("\x1b[31m"));
996 assert_eq!(display_width(&output), 10);
997 }
998
999 #[test]
1000 fn format_with_explicit_widths() {
1001 let columns = vec![Column::new(Width::Fixed(5)), Column::new(Width::Fixed(10))];
1002 let formatter = TabularFormatter::with_widths(columns, vec![5, 10]).separator(" - ");
1003
1004 let output = formatter.format_row(&["hi", "there"]);
1005 assert_eq!(output, "hi - there ");
1006 }
1007
1008 #[test]
1013 fn object_get_num_columns() {
1014 let formatter = Arc::new(TabularFormatter::new(&simple_spec(), 80));
1015 let value = formatter.get_value(&Value::from("num_columns"));
1016 assert_eq!(value, Some(Value::from(2)));
1017 }
1018
1019 #[test]
1020 fn object_get_widths() {
1021 let spec = TabularSpec::builder()
1022 .column(Column::new(Width::Fixed(10)))
1023 .column(Column::new(Width::Fixed(8)))
1024 .build();
1025 let formatter = Arc::new(TabularFormatter::new(&spec, 80));
1026
1027 let value = formatter.get_value(&Value::from("widths"));
1028 assert!(value.is_some());
1029 let widths = value.unwrap();
1030 assert!(widths.try_iter().is_ok());
1032 }
1033
1034 #[test]
1035 fn object_get_separator() {
1036 let spec = TabularSpec::builder()
1037 .column(Column::new(Width::Fixed(10)))
1038 .separator(" | ")
1039 .build();
1040 let formatter = Arc::new(TabularFormatter::new(&spec, 80));
1041
1042 let value = formatter.get_value(&Value::from("separator"));
1043 assert_eq!(value, Some(Value::from(" | ")));
1044 }
1045
1046 #[test]
1047 fn object_get_unknown_returns_none() {
1048 let formatter = Arc::new(TabularFormatter::new(&simple_spec(), 80));
1049 let value = formatter.get_value(&Value::from("unknown"));
1050 assert_eq!(value, None);
1051 }
1052
1053 #[test]
1054 fn object_row_method_via_template() {
1055 use minijinja::Environment;
1056
1057 let spec = TabularSpec::builder()
1058 .column(Column::new(Width::Fixed(10)))
1059 .column(Column::new(Width::Fixed(8)))
1060 .separator(" | ")
1061 .build();
1062 let formatter = TabularFormatter::new(&spec, 80);
1063
1064 let mut env = Environment::new();
1065 env.add_template("test", "{{ table.row(['Hello', 'World']) }}")
1066 .unwrap();
1067
1068 let tmpl = env.get_template("test").unwrap();
1069 let output = tmpl
1070 .render(minijinja::context! { table => Value::from_object(formatter) })
1071 .unwrap();
1072
1073 assert_eq!(output, "Hello | World ");
1074 }
1075
1076 #[test]
1077 fn object_row_method_in_loop() {
1078 use minijinja::Environment;
1079
1080 let spec = TabularSpec::builder()
1081 .column(Column::new(Width::Fixed(8)))
1082 .column(Column::new(Width::Fixed(6)))
1083 .separator(" ")
1084 .build();
1085 let formatter = TabularFormatter::new(&spec, 80);
1086
1087 let mut env = Environment::new();
1088 env.add_template(
1089 "test",
1090 "{% for item in items %}{{ table.row([item.name, item.value]) }}\n{% endfor %}",
1091 )
1092 .unwrap();
1093
1094 let tmpl = env.get_template("test").unwrap();
1095 let output = tmpl
1096 .render(minijinja::context! {
1097 table => Value::from_object(formatter),
1098 items => vec![
1099 minijinja::context! { name => "Alice", value => "100" },
1100 minijinja::context! { name => "Bob", value => "200" },
1101 ]
1102 })
1103 .unwrap();
1104
1105 assert!(output.contains("Alice"));
1106 assert!(output.contains("Bob"));
1107 }
1108
1109 #[test]
1110 fn object_column_width_method_via_template() {
1111 use minijinja::Environment;
1112
1113 let spec = TabularSpec::builder()
1114 .column(Column::new(Width::Fixed(10)))
1115 .column(Column::new(Width::Fixed(8)))
1116 .build();
1117 let formatter = TabularFormatter::new(&spec, 80);
1118
1119 let mut env = Environment::new();
1120 env.add_template(
1121 "test",
1122 "{{ table.column_width(0) }}-{{ table.column_width(1) }}",
1123 )
1124 .unwrap();
1125
1126 let tmpl = env.get_template("test").unwrap();
1127 let output = tmpl
1128 .render(minijinja::context! { table => Value::from_object(formatter) })
1129 .unwrap();
1130
1131 assert_eq!(output, "10-8");
1132 }
1133
1134 #[test]
1135 fn object_attribute_access_via_template() {
1136 use minijinja::Environment;
1137
1138 let spec = TabularSpec::builder()
1139 .column(Column::new(Width::Fixed(10)))
1140 .column(Column::new(Width::Fixed(8)))
1141 .separator(" | ")
1142 .build();
1143 let formatter = TabularFormatter::new(&spec, 80);
1144
1145 let mut env = Environment::new();
1146 env.add_template(
1147 "test",
1148 "cols={{ table.num_columns }}, sep='{{ table.separator }}'",
1149 )
1150 .unwrap();
1151
1152 let tmpl = env.get_template("test").unwrap();
1153 let output = tmpl
1154 .render(minijinja::context! { table => Value::from_object(formatter) })
1155 .unwrap();
1156
1157 assert_eq!(output, "cols=2, sep=' | '");
1158 }
1159
1160 #[test]
1165 fn format_cell_clip_no_marker() {
1166 let spec = FlatDataSpec::builder()
1167 .column(Column::new(Width::Fixed(5)).clip())
1168 .build();
1169 let formatter = TabularFormatter::new(&spec, 80);
1170
1171 let output = formatter.format_row(&["Hello World"]);
1172 assert_eq!(display_width(&output), 5);
1174 assert!(!output.contains("…"));
1175 assert!(output.starts_with("Hello"));
1176 }
1177
1178 #[test]
1179 fn format_cell_expand_overflows() {
1180 let col = Column::new(Width::Fixed(5)).overflow(Overflow::Expand);
1182 let output = format_cell("Hello World", 5, &col);
1183
1184 assert_eq!(output, "Hello World");
1186 assert_eq!(display_width(&output), 11); }
1188
1189 #[test]
1190 fn format_cell_expand_pads_when_short() {
1191 let col = Column::new(Width::Fixed(10)).overflow(Overflow::Expand);
1192 let output = format_cell("Hi", 10, &col);
1193
1194 assert_eq!(output, "Hi ");
1196 assert_eq!(display_width(&output), 10);
1197 }
1198
1199 #[test]
1200 fn format_cell_wrap_single_line() {
1201 let col = Column::new(Width::Fixed(20)).wrap();
1203 let output = format_cell_lines("Short text", 20, &col);
1204
1205 assert!(output.is_single());
1206 assert_eq!(output.line_count(), 1);
1207 assert_eq!(display_width(&output.to_single()), 20);
1208 }
1209
1210 #[test]
1211 fn format_cell_wrap_multi_line() {
1212 let col = Column::new(Width::Fixed(10)).wrap();
1213 let output = format_cell_lines("This is a longer text that wraps", 10, &col);
1214
1215 assert!(!output.is_single());
1216 assert!(output.line_count() > 1);
1217
1218 if let CellOutput::Multi(lines) = &output {
1220 for line in lines {
1221 assert_eq!(display_width(line), 10);
1222 }
1223 }
1224 }
1225
1226 #[test]
1227 fn format_cell_wrap_with_indent() {
1228 let col = Column::new(Width::Fixed(15)).overflow(Overflow::Wrap { indent: 2 });
1229 let output = format_cell_lines("First line then continuation", 15, &col);
1230
1231 if let CellOutput::Multi(lines) = output {
1232 assert!(lines[0].starts_with("First"));
1234 if lines.len() > 1 {
1236 let second_trimmed = lines[1].trim_start();
1238 assert!(lines[1].len() > second_trimmed.len()); }
1240 }
1241 }
1242
1243 #[test]
1244 fn format_row_lines_single_line() {
1245 let spec = FlatDataSpec::builder()
1246 .column(Column::new(Width::Fixed(10)))
1247 .column(Column::new(Width::Fixed(8)))
1248 .separator(" ")
1249 .build();
1250 let formatter = TabularFormatter::new(&spec, 80);
1251
1252 let lines = formatter.format_row_lines(&["Hello", "World"]);
1253 assert_eq!(lines.len(), 1);
1254 assert_eq!(lines[0], formatter.format_row(&["Hello", "World"]));
1255 }
1256
1257 #[test]
1258 fn format_row_lines_multi_line() {
1259 let spec = FlatDataSpec::builder()
1260 .column(Column::new(Width::Fixed(8)).wrap())
1261 .column(Column::new(Width::Fixed(6)))
1262 .separator(" ")
1263 .build();
1264 let formatter = TabularFormatter::new(&spec, 80);
1265
1266 let lines = formatter.format_row_lines(&["This is long", "Short"]);
1267
1268 assert!(!lines.is_empty());
1270
1271 let expected_width = display_width(&lines[0]);
1273 for line in &lines {
1274 assert_eq!(display_width(line), expected_width);
1275 }
1276 }
1277
1278 #[test]
1279 fn format_row_lines_mixed_columns() {
1280 let spec = FlatDataSpec::builder()
1282 .column(Column::new(Width::Fixed(6))) .column(Column::new(Width::Fixed(10)).wrap()) .column(Column::new(Width::Fixed(4))) .separator(" ")
1286 .build();
1287 let formatter = TabularFormatter::new(&spec, 80);
1288
1289 let lines = formatter.format_row_lines(&["aaaaa", "this text wraps here", "bbbb"]);
1290
1291 assert!(!lines.is_empty());
1293 }
1294
1295 #[test]
1300 fn cell_output_single_accessors() {
1301 let cell = CellOutput::Single("Hello".to_string());
1302
1303 assert!(cell.is_single());
1304 assert_eq!(cell.line_count(), 1);
1305 assert_eq!(cell.to_single(), "Hello");
1306 }
1307
1308 #[test]
1309 fn cell_output_multi_accessors() {
1310 let cell = CellOutput::Multi(vec!["Line 1".to_string(), "Line 2".to_string()]);
1311
1312 assert!(!cell.is_single());
1313 assert_eq!(cell.line_count(), 2);
1314 assert_eq!(cell.to_single(), "Line 1");
1315 }
1316
1317 #[test]
1318 fn cell_output_line_accessor() {
1319 let cell = CellOutput::Multi(vec!["First".to_string(), "Second".to_string()]);
1320
1321 let line0 = cell.line(0, 10, Align::Left);
1323 assert_eq!(line0, "First ");
1324 assert_eq!(display_width(&line0), 10);
1325
1326 let line1 = cell.line(1, 10, Align::Right);
1328 assert_eq!(line1, " Second");
1329
1330 let line2 = cell.line(2, 10, Align::Left);
1332 assert_eq!(line2, " ");
1333 }
1334
1335 #[test]
1340 fn format_row_all_left_anchor_no_gap() {
1341 let spec = FlatDataSpec::builder()
1343 .column(Column::new(Width::Fixed(5)))
1344 .column(Column::new(Width::Fixed(5)))
1345 .separator(" ")
1346 .build();
1347 let formatter = TabularFormatter::new(&spec, 50);
1348
1349 let output = formatter.format_row(&["A", "B"]);
1350 assert_eq!(output, "A B ");
1352 assert_eq!(display_width(&output), 11);
1353 }
1354
1355 #[test]
1356 fn format_row_with_right_anchor() {
1357 let spec = FlatDataSpec::builder()
1359 .column(Column::new(Width::Fixed(5))) .column(Column::new(Width::Fixed(5)).anchor_right()) .separator(" ")
1362 .build();
1363
1364 let formatter = TabularFormatter::new(&spec, 30);
1367
1368 let output = formatter.format_row(&["L", "R"]);
1369 assert_eq!(display_width(&output), 30);
1370 assert!(output.starts_with("L "));
1372 assert!(output.ends_with("R "));
1373 }
1374
1375 #[test]
1376 fn format_row_with_right_anchor_exact_fit() {
1377 let spec = FlatDataSpec::builder()
1379 .column(Column::new(Width::Fixed(10)))
1380 .column(Column::new(Width::Fixed(10)).anchor_right())
1381 .separator(" ")
1382 .build();
1383
1384 let formatter = TabularFormatter::new(&spec, 22);
1386
1387 let output = formatter.format_row(&["Left", "Right"]);
1388 assert_eq!(display_width(&output), 22);
1389 assert!(output.contains(" ")); }
1392
1393 #[test]
1394 fn format_row_all_right_anchor_no_gap() {
1395 let spec = FlatDataSpec::builder()
1397 .column(Column::new(Width::Fixed(5)).anchor_right())
1398 .column(Column::new(Width::Fixed(5)).anchor_right())
1399 .separator(" ")
1400 .build();
1401 let formatter = TabularFormatter::new(&spec, 50);
1402
1403 let output = formatter.format_row(&["A", "B"]);
1404 assert_eq!(output, "A B ");
1406 }
1407
1408 #[test]
1409 fn format_row_multiple_anchors() {
1410 let spec = FlatDataSpec::builder()
1412 .column(Column::new(Width::Fixed(4))) .column(Column::new(Width::Fixed(4))) .column(Column::new(Width::Fixed(4)).anchor_right()) .column(Column::new(Width::Fixed(4)).anchor_right()) .separator(" ")
1417 .build();
1418
1419 let formatter = TabularFormatter::new(&spec, 40);
1422
1423 let output = formatter.format_row(&["A", "B", "C", "D"]);
1424 assert_eq!(display_width(&output), 40);
1425 assert!(output.starts_with("A B "));
1427 }
1428
1429 #[test]
1430 fn calculate_anchor_gap_no_transition() {
1431 let spec = FlatDataSpec::builder()
1432 .column(Column::new(Width::Fixed(10)))
1433 .column(Column::new(Width::Fixed(10)))
1434 .build();
1435 let formatter = TabularFormatter::new(&spec, 50);
1436
1437 let (gap, transition) = formatter.calculate_anchor_gap();
1438 assert_eq!(transition, 2); assert_eq!(gap, 0);
1440 }
1441
1442 #[test]
1443 fn calculate_anchor_gap_with_transition() {
1444 let spec = FlatDataSpec::builder()
1445 .column(Column::new(Width::Fixed(10)))
1446 .column(Column::new(Width::Fixed(10)).anchor_right())
1447 .separator(" ")
1448 .build();
1449 let formatter = TabularFormatter::new(&spec, 50);
1450
1451 let (gap, transition) = formatter.calculate_anchor_gap();
1452 assert_eq!(transition, 1); assert!(gap > 0);
1454 }
1455
1456 #[test]
1457 fn format_row_lines_with_anchor() {
1458 let spec = FlatDataSpec::builder()
1460 .column(Column::new(Width::Fixed(8)).wrap())
1461 .column(Column::new(Width::Fixed(6)).anchor_right())
1462 .separator(" ")
1463 .build();
1464 let formatter = TabularFormatter::new(&spec, 40);
1465
1466 let lines = formatter.format_row_lines(&["This is text", "Right"]);
1467
1468 for line in &lines {
1470 assert_eq!(display_width(line), 40);
1471 }
1472 }
1473
1474 #[test]
1479 fn row_from_simple_struct() {
1480 #[derive(Serialize)]
1481 struct Record {
1482 name: String,
1483 value: i32,
1484 }
1485
1486 let spec = FlatDataSpec::builder()
1487 .column(Column::new(Width::Fixed(10)).key("name"))
1488 .column(Column::new(Width::Fixed(5)).key("value"))
1489 .separator(" ")
1490 .build();
1491 let formatter = TabularFormatter::new(&spec, 80);
1492
1493 let record = Record {
1494 name: "Test".to_string(),
1495 value: 42,
1496 };
1497
1498 let row = formatter.row_from(&record);
1499 assert!(row.contains("Test"));
1500 assert!(row.contains("42"));
1501 }
1502
1503 #[test]
1504 fn row_from_uses_name_as_fallback() {
1505 #[derive(Serialize)]
1506 struct Item {
1507 title: String,
1508 }
1509
1510 let spec = FlatDataSpec::builder()
1511 .column(Column::new(Width::Fixed(15)).named("title"))
1512 .build();
1513 let formatter = TabularFormatter::new(&spec, 80);
1514
1515 let item = Item {
1516 title: "Hello".to_string(),
1517 };
1518
1519 let row = formatter.row_from(&item);
1520 assert!(row.contains("Hello"));
1521 }
1522
1523 #[test]
1524 fn row_from_nested_field() {
1525 #[derive(Serialize)]
1526 struct User {
1527 email: String,
1528 }
1529
1530 #[derive(Serialize)]
1531 struct Record {
1532 user: User,
1533 status: String,
1534 }
1535
1536 let spec = FlatDataSpec::builder()
1537 .column(Column::new(Width::Fixed(20)).key("user.email"))
1538 .column(Column::new(Width::Fixed(10)).key("status"))
1539 .separator(" ")
1540 .build();
1541 let formatter = TabularFormatter::new(&spec, 80);
1542
1543 let record = Record {
1544 user: User {
1545 email: "test@example.com".to_string(),
1546 },
1547 status: "active".to_string(),
1548 };
1549
1550 let row = formatter.row_from(&record);
1551 assert!(row.contains("test@example.com"));
1552 assert!(row.contains("active"));
1553 }
1554
1555 #[test]
1556 fn row_from_array_index() {
1557 #[derive(Serialize)]
1558 struct Record {
1559 items: Vec<String>,
1560 }
1561
1562 let spec = FlatDataSpec::builder()
1563 .column(Column::new(Width::Fixed(10)).key("items.0"))
1564 .column(Column::new(Width::Fixed(10)).key("items.1"))
1565 .build();
1566 let formatter = TabularFormatter::new(&spec, 80);
1567
1568 let record = Record {
1569 items: vec!["First".to_string(), "Second".to_string()],
1570 };
1571
1572 let row = formatter.row_from(&record);
1573 assert!(row.contains("First"));
1574 assert!(row.contains("Second"));
1575 }
1576
1577 #[test]
1578 fn row_from_missing_field_uses_null_repr() {
1579 #[derive(Serialize)]
1580 struct Record {
1581 present: String,
1582 }
1583
1584 let spec = FlatDataSpec::builder()
1585 .column(Column::new(Width::Fixed(10)).key("present"))
1586 .column(Column::new(Width::Fixed(10)).key("missing").null_repr("-"))
1587 .build();
1588 let formatter = TabularFormatter::new(&spec, 80);
1589
1590 let record = Record {
1591 present: "value".to_string(),
1592 };
1593
1594 let row = formatter.row_from(&record);
1595 assert!(row.contains("value"));
1596 }
1598
1599 #[test]
1600 fn row_from_no_key_uses_null_repr() {
1601 #[derive(Serialize)]
1602 struct Record {
1603 value: String,
1604 }
1605
1606 let spec = FlatDataSpec::builder()
1607 .column(Column::new(Width::Fixed(10)).null_repr("N/A"))
1608 .build();
1609 let formatter = TabularFormatter::new(&spec, 80);
1610
1611 let record = Record {
1612 value: "test".to_string(),
1613 };
1614
1615 let row = formatter.row_from(&record);
1616 assert!(row.contains("N/A"));
1617 }
1618
1619 #[test]
1620 fn row_from_various_types() {
1621 #[derive(Serialize)]
1622 struct Record {
1623 string_val: String,
1624 int_val: i64,
1625 float_val: f64,
1626 bool_val: bool,
1627 }
1628
1629 let spec = FlatDataSpec::builder()
1630 .column(Column::new(Width::Fixed(10)).key("string_val"))
1631 .column(Column::new(Width::Fixed(10)).key("int_val"))
1632 .column(Column::new(Width::Fixed(10)).key("float_val"))
1633 .column(Column::new(Width::Fixed(10)).key("bool_val"))
1634 .build();
1635 let formatter = TabularFormatter::new(&spec, 80);
1636
1637 let record = Record {
1638 string_val: "text".to_string(),
1639 int_val: 123,
1640 float_val: 9.87,
1641 bool_val: true,
1642 };
1643
1644 let row = formatter.row_from(&record);
1645 assert!(row.contains("text"));
1646 assert!(row.contains("123"));
1647 assert!(row.contains("9.87"));
1648 assert!(row.contains("true"));
1649 }
1650
1651 #[test]
1652 fn extract_field_simple() {
1653 let json = serde_json::json!({
1654 "name": "Alice",
1655 "age": 30
1656 });
1657
1658 assert_eq!(extract_field(&json, "name"), "Alice");
1659 assert_eq!(extract_field(&json, "age"), "30");
1660 assert_eq!(extract_field(&json, "missing"), "");
1661 }
1662
1663 #[test]
1664 fn extract_field_nested() {
1665 let json = serde_json::json!({
1666 "user": {
1667 "profile": {
1668 "email": "test@example.com"
1669 }
1670 }
1671 });
1672
1673 assert_eq!(
1674 extract_field(&json, "user.profile.email"),
1675 "test@example.com"
1676 );
1677 assert_eq!(extract_field(&json, "user.missing"), "");
1678 }
1679
1680 #[test]
1681 fn extract_field_array() {
1682 let json = serde_json::json!({
1683 "items": ["a", "b", "c"]
1684 });
1685
1686 assert_eq!(extract_field(&json, "items.0"), "a");
1687 assert_eq!(extract_field(&json, "items.1"), "b");
1688 assert_eq!(extract_field(&json, "items.10"), ""); }
1690
1691 #[test]
1692 fn row_lines_from_struct() {
1693 #[derive(Serialize)]
1694 struct Record {
1695 description: String,
1696 status: String,
1697 }
1698
1699 let spec = FlatDataSpec::builder()
1700 .column(Column::new(Width::Fixed(10)).key("description").wrap())
1701 .column(Column::new(Width::Fixed(6)).key("status"))
1702 .separator(" ")
1703 .build();
1704 let formatter = TabularFormatter::new(&spec, 80);
1705
1706 let record = Record {
1707 description: "A longer description that wraps".to_string(),
1708 status: "OK".to_string(),
1709 };
1710
1711 let lines = formatter.row_lines_from(&record);
1712 assert!(!lines.is_empty());
1714 }
1715
1716 #[test]
1721 fn format_cell_with_style() {
1722 let spec = FlatDataSpec::builder()
1723 .column(Column::new(Width::Fixed(10)).style("header"))
1724 .build();
1725 let formatter = TabularFormatter::new(&spec, 80);
1726
1727 let output = formatter.format_row(&["Hello"]);
1728 assert!(output.starts_with("[header]"));
1730 assert!(output.ends_with("[/header]"));
1731 assert!(output.contains("Hello"));
1732 }
1733
1734 #[test]
1735 fn format_cell_style_from_value() {
1736 let spec = FlatDataSpec::builder()
1737 .column(Column::new(Width::Fixed(10)).style_from_value())
1738 .build();
1739 let formatter = TabularFormatter::new(&spec, 80);
1740
1741 let output = formatter.format_row(&["error"]);
1742 assert!(output.contains("[error]"));
1744 assert!(output.contains("[/error]"));
1745 }
1746
1747 #[test]
1748 fn format_cell_no_style() {
1749 let spec = FlatDataSpec::builder()
1750 .column(Column::new(Width::Fixed(10)))
1751 .build();
1752 let formatter = TabularFormatter::new(&spec, 80);
1753
1754 let output = formatter.format_row(&["Hello"]);
1755 assert!(!output.contains("["));
1757 assert!(!output.contains("]"));
1758 assert!(output.contains("Hello"));
1759 }
1760
1761 #[test]
1762 fn format_cell_style_overrides_style_from_value() {
1763 let mut col = Column::new(Width::Fixed(10));
1765 col.style = Some("default".to_string());
1766 col.style_from_value = true;
1767
1768 let spec = FlatDataSpec::builder().column(col).build();
1769 let formatter = TabularFormatter::new(&spec, 80);
1770
1771 let output = formatter.format_row(&["custom"]);
1772 assert!(output.contains("[custom]"));
1774 assert!(output.contains("[/custom]"));
1775 }
1776
1777 #[test]
1778 fn format_row_multiple_styled_columns() {
1779 let spec = FlatDataSpec::builder()
1780 .column(Column::new(Width::Fixed(8)).style("name"))
1781 .column(Column::new(Width::Fixed(8)).style("status"))
1782 .separator(" ")
1783 .build();
1784 let formatter = TabularFormatter::new(&spec, 80);
1785
1786 let output = formatter.format_row(&["Alice", "Active"]);
1787 assert!(output.contains("[name]"));
1788 assert!(output.contains("[status]"));
1789 }
1790
1791 #[test]
1792 fn format_cell_lines_with_style() {
1793 let spec = FlatDataSpec::builder()
1794 .column(Column::new(Width::Fixed(10)).wrap().style("text"))
1795 .build();
1796 let formatter = TabularFormatter::new(&spec, 80);
1797
1798 let lines = formatter.format_row_lines(&["This is a long text that wraps"]);
1799
1800 for line in &lines {
1802 assert!(line.contains("[text]"));
1803 assert!(line.contains("[/text]"));
1804 }
1805 }
1806
1807 #[test]
1812 fn extract_headers_from_header_field() {
1813 let spec = FlatDataSpec::builder()
1814 .column(Column::new(Width::Fixed(10)).header("Name"))
1815 .column(Column::new(Width::Fixed(8)).header("Status"))
1816 .build();
1817 let formatter = TabularFormatter::new(&spec, 80);
1818
1819 let headers = formatter.extract_headers();
1820 assert_eq!(headers, vec!["Name", "Status"]);
1821 }
1822
1823 #[test]
1824 fn extract_headers_fallback_to_key() {
1825 let spec = FlatDataSpec::builder()
1826 .column(Column::new(Width::Fixed(10)).key("user_name"))
1827 .column(Column::new(Width::Fixed(8)).key("status"))
1828 .build();
1829 let formatter = TabularFormatter::new(&spec, 80);
1830
1831 let headers = formatter.extract_headers();
1832 assert_eq!(headers, vec!["user_name", "status"]);
1833 }
1834
1835 #[test]
1836 fn extract_headers_fallback_to_name() {
1837 let spec = FlatDataSpec::builder()
1838 .column(Column::new(Width::Fixed(10)).named("col1"))
1839 .column(Column::new(Width::Fixed(8)).named("col2"))
1840 .build();
1841 let formatter = TabularFormatter::new(&spec, 80);
1842
1843 let headers = formatter.extract_headers();
1844 assert_eq!(headers, vec!["col1", "col2"]);
1845 }
1846
1847 #[test]
1848 fn extract_headers_priority_order() {
1849 let spec = FlatDataSpec::builder()
1851 .column(
1852 Column::new(Width::Fixed(10))
1853 .header("Header")
1854 .key("key")
1855 .named("name"),
1856 )
1857 .column(
1858 Column::new(Width::Fixed(10))
1859 .key("key_only")
1860 .named("name_only"),
1861 )
1862 .column(Column::new(Width::Fixed(10)).named("name_only"))
1863 .column(Column::new(Width::Fixed(10))) .build();
1865 let formatter = TabularFormatter::new(&spec, 80);
1866
1867 let headers = formatter.extract_headers();
1868 assert_eq!(headers, vec!["Header", "key_only", "name_only", ""]);
1869 }
1870
1871 #[test]
1872 fn extract_headers_empty_spec() {
1873 let spec = FlatDataSpec::builder().build();
1874 let formatter = TabularFormatter::new(&spec, 80);
1875
1876 let headers = formatter.extract_headers();
1877 assert!(headers.is_empty());
1878 }
1879}