1use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum Align {
13 #[default]
15 Left,
16 Right,
18 Center,
20}
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum TruncateAt {
26 #[default]
29 End,
30 Start,
33 Middle,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub enum Overflow {
41 Truncate {
43 at: TruncateAt,
45 marker: String,
47 },
48 Wrap {
50 indent: usize,
52 },
53 Clip,
55 Expand,
57}
58
59impl Default for Overflow {
60 fn default() -> Self {
61 Overflow::Truncate {
62 at: TruncateAt::End,
63 marker: "…".to_string(),
64 }
65 }
66}
67
68impl Overflow {
69 pub fn truncate(at: TruncateAt) -> Self {
71 Overflow::Truncate {
72 at,
73 marker: "…".to_string(),
74 }
75 }
76
77 pub fn truncate_with_marker(at: TruncateAt, marker: impl Into<String>) -> Self {
79 Overflow::Truncate {
80 at,
81 marker: marker.into(),
82 }
83 }
84
85 pub fn wrap() -> Self {
87 Overflow::Wrap { indent: 0 }
88 }
89
90 pub fn wrap_with_indent(indent: usize) -> Self {
92 Overflow::Wrap { indent }
93 }
94}
95
96#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "lowercase")]
99pub enum Anchor {
100 #[default]
102 Left,
103 Right,
105}
106
107#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(try_from = "WidthRaw", into = "WidthRaw")]
110pub enum Width {
111 Fixed(usize),
113 Bounded {
115 min: Option<usize>,
117 max: Option<usize>,
119 },
120 Fill,
123 Fraction(usize),
126}
127
128#[derive(Serialize, Deserialize)]
129#[serde(untagged)]
130enum WidthRaw {
131 Fixed(usize),
132 Bounded {
133 #[serde(default)]
134 min: Option<usize>,
135 #[serde(default)]
136 max: Option<usize>,
137 },
138 StringVariant(String),
139}
140
141impl From<Width> for WidthRaw {
142 fn from(width: Width) -> Self {
143 match width {
144 Width::Fixed(w) => WidthRaw::Fixed(w),
145 Width::Bounded { min, max } => WidthRaw::Bounded { min, max },
146 Width::Fill => WidthRaw::StringVariant("fill".to_string()),
147 Width::Fraction(n) => WidthRaw::StringVariant(format!("{}fr", n)),
148 }
149 }
150}
151
152impl TryFrom<WidthRaw> for Width {
153 type Error = String;
154
155 fn try_from(raw: WidthRaw) -> Result<Self, Self::Error> {
156 match raw {
157 WidthRaw::Fixed(w) => Ok(Width::Fixed(w)),
158 WidthRaw::Bounded { min, max } => Ok(Width::Bounded { min, max }),
159 WidthRaw::StringVariant(s) if s == "fill" => Ok(Width::Fill),
160 WidthRaw::StringVariant(s) if s.ends_with("fr") => {
161 let num_str = s.trim_end_matches("fr");
162 num_str
163 .parse::<usize>()
164 .map(Width::Fraction)
165 .map_err(|_| format!("Invalid fraction: '{}'. Expected format like '2fr'.", s))
166 }
167 WidthRaw::StringVariant(s) => Err(format!(
168 "Invalid width string: '{}'. Expected 'fill' or '<n>fr'.",
169 s
170 )),
171 }
172 }
173}
174
175impl Default for Width {
176 fn default() -> Self {
177 Width::Bounded {
178 min: None,
179 max: None,
180 }
181 }
182}
183
184impl Width {
185 pub fn fixed(width: usize) -> Self {
187 Width::Fixed(width)
188 }
189
190 pub fn bounded(min: usize, max: usize) -> Self {
192 Width::Bounded {
193 min: Some(min),
194 max: Some(max),
195 }
196 }
197
198 pub fn min(min: usize) -> Self {
200 Width::Bounded {
201 min: Some(min),
202 max: None,
203 }
204 }
205
206 pub fn max(max: usize) -> Self {
208 Width::Bounded {
209 min: None,
210 max: Some(max),
211 }
212 }
213
214 pub fn fill() -> Self {
216 Width::Fill
217 }
218
219 pub fn fraction(n: usize) -> Self {
222 Width::Fraction(n)
223 }
224}
225
226#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct Column {
229 pub name: Option<String>,
231 pub width: Width,
233 pub align: Align,
235 pub anchor: Anchor,
237 pub overflow: Overflow,
239 pub null_repr: String,
241 pub style: Option<String>,
243 pub style_from_value: bool,
245 pub key: Option<String>,
247 pub header: Option<String>,
249 pub sub_columns: Option<SubColumns>,
255}
256
257impl Default for Column {
258 fn default() -> Self {
259 Column {
260 name: None,
261 width: Width::default(),
262 align: Align::default(),
263 anchor: Anchor::default(),
264 overflow: Overflow::default(),
265 null_repr: "-".to_string(),
266 style: None,
267 style_from_value: false,
268 key: None,
269 header: None,
270 sub_columns: None,
271 }
272 }
273}
274
275impl Column {
276 pub fn new(width: Width) -> Self {
278 Column {
279 width,
280 ..Default::default()
281 }
282 }
283
284 pub fn builder() -> ColumnBuilder {
286 ColumnBuilder::default()
287 }
288
289 pub fn named(mut self, name: impl Into<String>) -> Self {
291 self.name = Some(name.into());
292 self
293 }
294
295 pub fn align(mut self, align: Align) -> Self {
297 self.align = align;
298 self
299 }
300
301 pub fn right(self) -> Self {
303 self.align(Align::Right)
304 }
305
306 pub fn center(self) -> Self {
308 self.align(Align::Center)
309 }
310
311 pub fn anchor(mut self, anchor: Anchor) -> Self {
313 self.anchor = anchor;
314 self
315 }
316
317 pub fn anchor_right(self) -> Self {
319 self.anchor(Anchor::Right)
320 }
321
322 pub fn overflow(mut self, overflow: Overflow) -> Self {
324 self.overflow = overflow;
325 self
326 }
327
328 pub fn wrap(self) -> Self {
330 self.overflow(Overflow::wrap())
331 }
332
333 pub fn wrap_indent(self, indent: usize) -> Self {
335 self.overflow(Overflow::wrap_with_indent(indent))
336 }
337
338 pub fn clip(self) -> Self {
340 self.overflow(Overflow::Clip)
341 }
342
343 pub fn truncate(mut self, at: TruncateAt) -> Self {
345 self.overflow = match self.overflow {
346 Overflow::Truncate { marker, .. } => Overflow::Truncate { at, marker },
347 _ => Overflow::truncate(at),
348 };
349 self
350 }
351
352 pub fn truncate_middle(self) -> Self {
354 self.truncate(TruncateAt::Middle)
355 }
356
357 pub fn truncate_start(self) -> Self {
359 self.truncate(TruncateAt::Start)
360 }
361
362 pub fn ellipsis(mut self, ellipsis: impl Into<String>) -> Self {
364 self.overflow = match self.overflow {
365 Overflow::Truncate { at, .. } => Overflow::Truncate {
366 at,
367 marker: ellipsis.into(),
368 },
369 _ => Overflow::truncate_with_marker(TruncateAt::End, ellipsis),
370 };
371 self
372 }
373
374 pub fn null_repr(mut self, null_repr: impl Into<String>) -> Self {
376 self.null_repr = null_repr.into();
377 self
378 }
379
380 pub fn style(mut self, style: impl Into<String>) -> Self {
382 self.style = Some(style.into());
383 self
384 }
385
386 pub fn style_from_value(mut self) -> Self {
391 self.style_from_value = true;
392 self
393 }
394
395 pub fn key(mut self, key: impl Into<String>) -> Self {
397 self.key = Some(key.into());
398 self
399 }
400
401 pub fn header(mut self, header: impl Into<String>) -> Self {
403 self.header = Some(header.into());
404 self
405 }
406
407 pub fn sub_columns(mut self, sub_cols: SubColumns) -> Self {
409 self.sub_columns = Some(sub_cols);
410 self
411 }
412}
413
414#[derive(Clone, Debug, Default)]
416pub struct ColumnBuilder {
417 name: Option<String>,
418 width: Option<Width>,
419 align: Option<Align>,
420 anchor: Option<Anchor>,
421 overflow: Option<Overflow>,
422 null_repr: Option<String>,
423 style: Option<String>,
424 style_from_value: bool,
425 key: Option<String>,
426 header: Option<String>,
427 sub_columns: Option<SubColumns>,
428}
429
430impl ColumnBuilder {
431 pub fn named(mut self, name: impl Into<String>) -> Self {
433 self.name = Some(name.into());
434 self
435 }
436
437 pub fn width(mut self, width: Width) -> Self {
439 self.width = Some(width);
440 self
441 }
442
443 pub fn fixed(mut self, width: usize) -> Self {
445 self.width = Some(Width::Fixed(width));
446 self
447 }
448
449 pub fn fill(mut self) -> Self {
451 self.width = Some(Width::Fill);
452 self
453 }
454
455 pub fn bounded(mut self, min: usize, max: usize) -> Self {
457 self.width = Some(Width::bounded(min, max));
458 self
459 }
460
461 pub fn fraction(mut self, n: usize) -> Self {
463 self.width = Some(Width::Fraction(n));
464 self
465 }
466
467 pub fn align(mut self, align: Align) -> Self {
469 self.align = Some(align);
470 self
471 }
472
473 pub fn right(self) -> Self {
475 self.align(Align::Right)
476 }
477
478 pub fn center(self) -> Self {
480 self.align(Align::Center)
481 }
482
483 pub fn anchor(mut self, anchor: Anchor) -> Self {
485 self.anchor = Some(anchor);
486 self
487 }
488
489 pub fn anchor_right(self) -> Self {
491 self.anchor(Anchor::Right)
492 }
493
494 pub fn overflow(mut self, overflow: Overflow) -> Self {
496 self.overflow = Some(overflow);
497 self
498 }
499
500 pub fn wrap(self) -> Self {
502 self.overflow(Overflow::wrap())
503 }
504
505 pub fn clip(self) -> Self {
507 self.overflow(Overflow::Clip)
508 }
509
510 pub fn truncate(mut self, at: TruncateAt) -> Self {
512 self.overflow = Some(match self.overflow {
513 Some(Overflow::Truncate { marker, .. }) => Overflow::Truncate { at, marker },
514 _ => Overflow::truncate(at),
515 });
516 self
517 }
518
519 pub fn ellipsis(mut self, ellipsis: impl Into<String>) -> Self {
521 self.overflow = Some(match self.overflow {
522 Some(Overflow::Truncate { at, .. }) => Overflow::Truncate {
523 at,
524 marker: ellipsis.into(),
525 },
526 _ => Overflow::truncate_with_marker(TruncateAt::End, ellipsis),
527 });
528 self
529 }
530
531 pub fn null_repr(mut self, null_repr: impl Into<String>) -> Self {
533 self.null_repr = Some(null_repr.into());
534 self
535 }
536
537 pub fn style(mut self, style: impl Into<String>) -> Self {
539 self.style = Some(style.into());
540 self
541 }
542
543 pub fn style_from_value(mut self) -> Self {
545 self.style_from_value = true;
546 self
547 }
548
549 pub fn key(mut self, key: impl Into<String>) -> Self {
551 self.key = Some(key.into());
552 self
553 }
554
555 pub fn header(mut self, header: impl Into<String>) -> Self {
557 self.header = Some(header.into());
558 self
559 }
560
561 pub fn sub_columns(mut self, sub_cols: SubColumns) -> Self {
563 self.sub_columns = Some(sub_cols);
564 self
565 }
566
567 pub fn build(self) -> Column {
569 let default = Column::default();
570 Column {
571 name: self.name,
572 width: self.width.unwrap_or(default.width),
573 align: self.align.unwrap_or(default.align),
574 anchor: self.anchor.unwrap_or(default.anchor),
575 overflow: self.overflow.unwrap_or(default.overflow),
576 null_repr: self.null_repr.unwrap_or(default.null_repr),
577 style: self.style,
578 style_from_value: self.style_from_value,
579 key: self.key,
580 header: self.header,
581 sub_columns: self.sub_columns,
582 }
583 }
584}
585
586pub struct Col;
603
604impl Col {
605 pub fn fixed(width: usize) -> Column {
607 Column::new(Width::Fixed(width))
608 }
609
610 pub fn min(min: usize) -> Column {
612 Column::new(Width::min(min))
613 }
614
615 pub fn max(max: usize) -> Column {
617 Column::new(Width::max(max))
618 }
619
620 pub fn bounded(min: usize, max: usize) -> Column {
622 Column::new(Width::bounded(min, max))
623 }
624
625 pub fn fill() -> Column {
627 Column::new(Width::Fill)
628 }
629
630 pub fn fraction(n: usize) -> Column {
633 Column::new(Width::Fraction(n))
634 }
635}
636
637#[derive(Clone, Debug, Serialize, Deserialize)]
656pub struct SubColumn {
657 pub name: Option<String>,
659 pub width: Width,
661 pub align: Align,
663 pub overflow: Overflow,
665 pub null_repr: String,
667 pub style: Option<String>,
669}
670
671impl Default for SubColumn {
672 fn default() -> Self {
673 SubColumn {
674 name: None,
675 width: Width::Fill,
676 align: Align::Left,
677 overflow: Overflow::default(),
678 null_repr: String::new(),
679 style: None,
680 }
681 }
682}
683
684impl SubColumn {
685 pub fn new(width: Width) -> Self {
687 SubColumn {
688 width,
689 ..Default::default()
690 }
691 }
692
693 pub fn named(mut self, name: impl Into<String>) -> Self {
695 self.name = Some(name.into());
696 self
697 }
698
699 pub fn align(mut self, align: Align) -> Self {
701 self.align = align;
702 self
703 }
704
705 pub fn right(self) -> Self {
707 self.align(Align::Right)
708 }
709
710 pub fn center(self) -> Self {
712 self.align(Align::Center)
713 }
714
715 pub fn overflow(mut self, overflow: Overflow) -> Self {
717 self.overflow = overflow;
718 self
719 }
720
721 pub fn null_repr(mut self, null_repr: impl Into<String>) -> Self {
723 self.null_repr = null_repr.into();
724 self
725 }
726
727 pub fn style(mut self, style: impl Into<String>) -> Self {
729 self.style = Some(style.into());
730 self
731 }
732}
733
734#[derive(Clone, Debug, Serialize, Deserialize)]
751pub struct SubColumns {
752 pub columns: Vec<SubColumn>,
754 pub separator: String,
756}
757
758impl SubColumns {
759 pub fn new(columns: Vec<SubColumn>, separator: impl Into<String>) -> Result<Self, String> {
768 if columns.is_empty() {
769 return Err("sub_columns must contain at least one sub-column".into());
770 }
771
772 let fill_count = columns
773 .iter()
774 .filter(|c| matches!(c.width, Width::Fill))
775 .count();
776 if fill_count != 1 {
777 return Err(format!(
778 "sub_columns must have exactly one Fill sub-column, found {}",
779 fill_count
780 ));
781 }
782
783 for (i, col) in columns.iter().enumerate() {
784 if matches!(col.width, Width::Fraction(_)) {
785 return Err(format!(
786 "sub_column[{}]: Fraction width is not supported for sub-columns",
787 i
788 ));
789 }
790 }
791
792 Ok(SubColumns {
793 columns,
794 separator: separator.into(),
795 })
796 }
797}
798
799pub struct SubCol;
813
814impl SubCol {
815 pub fn fill() -> SubColumn {
817 SubColumn::new(Width::Fill)
818 }
819
820 pub fn fixed(width: usize) -> SubColumn {
822 SubColumn::new(Width::Fixed(width))
823 }
824
825 pub fn bounded(min: usize, max: usize) -> SubColumn {
827 SubColumn::new(Width::bounded(min, max))
828 }
829
830 pub fn max(max: usize) -> SubColumn {
832 SubColumn::new(Width::max(max))
833 }
834
835 pub fn min(min: usize) -> SubColumn {
837 SubColumn::new(Width::min(min))
838 }
839}
840
841#[derive(Clone, Debug, Default, Serialize, Deserialize)]
843pub struct Decorations {
844 pub column_sep: String,
846 pub row_prefix: String,
848 pub row_suffix: String,
850}
851
852impl Decorations {
853 pub fn with_separator(sep: impl Into<String>) -> Self {
855 Decorations {
856 column_sep: sep.into(),
857 row_prefix: String::new(),
858 row_suffix: String::new(),
859 }
860 }
861
862 pub fn separator(mut self, sep: impl Into<String>) -> Self {
864 self.column_sep = sep.into();
865 self
866 }
867
868 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
870 self.row_prefix = prefix.into();
871 self
872 }
873
874 pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
876 self.row_suffix = suffix.into();
877 self
878 }
879
880 pub fn overhead(&self, num_columns: usize) -> usize {
882 use crate::tabular::display_width;
883 let prefix_width = display_width(&self.row_prefix);
884 let suffix_width = display_width(&self.row_suffix);
885 let sep_width = display_width(&self.column_sep);
886 let sep_count = num_columns.saturating_sub(1);
887 prefix_width + suffix_width + (sep_width * sep_count)
888 }
889}
890
891#[derive(Clone, Debug, Serialize, Deserialize)]
893pub struct FlatDataSpec {
894 pub columns: Vec<Column>,
896 pub decorations: Decorations,
898}
899
900impl FlatDataSpec {
901 pub fn new(columns: Vec<Column>) -> Self {
903 FlatDataSpec {
904 columns,
905 decorations: Decorations::default(),
906 }
907 }
908
909 pub fn builder() -> FlatDataSpecBuilder {
911 FlatDataSpecBuilder::default()
912 }
913
914 pub fn num_columns(&self) -> usize {
916 self.columns.len()
917 }
918
919 pub fn has_fill_column(&self) -> bool {
921 self.columns.iter().any(|c| matches!(c.width, Width::Fill))
922 }
923
924 pub fn extract_header(&self) -> Vec<String> {
928 self.columns
929 .iter()
930 .map(|col| {
931 col.header
932 .as_deref()
933 .or(col.key.as_deref())
934 .unwrap_or("")
935 .to_string()
936 })
937 .collect()
938 }
939
940 pub fn extract_row(&self, data: &Value) -> Vec<String> {
947 self.columns
948 .iter()
949 .map(|col| {
950 if let Some(key) = &col.key {
951 extract_value(data, key).unwrap_or(col.null_repr.clone())
952 } else {
953 col.null_repr.clone()
954 }
955 })
956 .collect()
957 }
958}
959
960fn extract_value(data: &Value, path: &str) -> Option<String> {
962 let mut current = data;
963 for part in path.split('.') {
964 match current {
965 Value::Object(map) => {
966 current = map.get(part)?;
967 }
968 _ => return None,
969 }
970 }
971
972 match current {
973 Value::String(s) => Some(s.clone()),
974 Value::Null => None,
975 v => Some(v.to_string()),
977 }
978}
979
980#[derive(Clone, Debug, Default)]
982pub struct FlatDataSpecBuilder {
983 columns: Vec<Column>,
984 decorations: Decorations,
985}
986
987impl FlatDataSpecBuilder {
988 pub fn column(mut self, column: Column) -> Self {
990 self.columns.push(column);
991 self
992 }
993
994 pub fn columns(mut self, columns: impl IntoIterator<Item = Column>) -> Self {
996 self.columns.extend(columns);
997 self
998 }
999
1000 pub fn separator(mut self, sep: impl Into<String>) -> Self {
1002 self.decorations.column_sep = sep.into();
1003 self
1004 }
1005
1006 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
1008 self.decorations.row_prefix = prefix.into();
1009 self
1010 }
1011
1012 pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
1014 self.decorations.row_suffix = suffix.into();
1015 self
1016 }
1017
1018 pub fn decorations(mut self, decorations: Decorations) -> Self {
1020 self.decorations = decorations;
1021 self
1022 }
1023
1024 pub fn build(self) -> FlatDataSpec {
1026 FlatDataSpec {
1027 columns: self.columns,
1028 decorations: self.decorations,
1029 }
1030 }
1031}
1032
1033pub type TabularSpec = FlatDataSpec;
1035pub type TabularSpecBuilder = FlatDataSpecBuilder;
1037
1038#[cfg(test)]
1039mod tests {
1040 use super::*;
1041
1042 #[test]
1045 fn align_default_is_left() {
1046 assert_eq!(Align::default(), Align::Left);
1047 }
1048
1049 #[test]
1050 fn align_serde_roundtrip() {
1051 let values = [Align::Left, Align::Right, Align::Center];
1052 for align in values {
1053 let json = serde_json::to_string(&align).unwrap();
1054 let parsed: Align = serde_json::from_str(&json).unwrap();
1055 assert_eq!(parsed, align);
1056 }
1057 }
1058
1059 #[test]
1062 fn truncate_at_default_is_end() {
1063 assert_eq!(TruncateAt::default(), TruncateAt::End);
1064 }
1065
1066 #[test]
1067 fn truncate_at_serde_roundtrip() {
1068 let values = [TruncateAt::End, TruncateAt::Start, TruncateAt::Middle];
1069 for truncate in values {
1070 let json = serde_json::to_string(&truncate).unwrap();
1071 let parsed: TruncateAt = serde_json::from_str(&json).unwrap();
1072 assert_eq!(parsed, truncate);
1073 }
1074 }
1075
1076 #[test]
1079 fn width_constructors() {
1080 assert_eq!(Width::fixed(10), Width::Fixed(10));
1081 assert_eq!(
1082 Width::bounded(5, 20),
1083 Width::Bounded {
1084 min: Some(5),
1085 max: Some(20)
1086 }
1087 );
1088 assert_eq!(
1089 Width::min(5),
1090 Width::Bounded {
1091 min: Some(5),
1092 max: None
1093 }
1094 );
1095 assert_eq!(
1096 Width::max(20),
1097 Width::Bounded {
1098 min: None,
1099 max: Some(20)
1100 }
1101 );
1102 assert_eq!(Width::fill(), Width::Fill);
1103 }
1104
1105 #[test]
1106 fn width_serde_fixed() {
1107 let width = Width::Fixed(10);
1108 let json = serde_json::to_string(&width).unwrap();
1109 assert_eq!(json, "10");
1110 let parsed: Width = serde_json::from_str(&json).unwrap();
1111 assert_eq!(parsed, width);
1112 }
1113
1114 #[test]
1115 fn width_serde_bounded() {
1116 let width = Width::Bounded {
1117 min: Some(5),
1118 max: Some(20),
1119 };
1120 let json = serde_json::to_string(&width).unwrap();
1121 let parsed: Width = serde_json::from_str(&json).unwrap();
1122 assert_eq!(parsed, width);
1123 }
1124
1125 #[test]
1126 fn width_serde_fill() {
1127 let width = Width::Fill;
1128 let json = serde_json::to_string(&width).unwrap();
1129 assert_eq!(json, "\"fill\"");
1131
1132 let parsed: Width = serde_json::from_str("\"fill\"").unwrap();
1133 assert_eq!(parsed, width);
1134 }
1135
1136 #[test]
1137 fn width_serde_fraction() {
1138 let width = Width::Fraction(2);
1139 let json = serde_json::to_string(&width).unwrap();
1140 assert_eq!(json, "\"2fr\"");
1141
1142 let parsed: Width = serde_json::from_str("\"2fr\"").unwrap();
1143 assert_eq!(parsed, width);
1144
1145 let parsed_1: Width = serde_json::from_str("\"1fr\"").unwrap();
1147 assert_eq!(parsed_1, Width::Fraction(1));
1148 }
1149
1150 #[test]
1151 fn width_fraction_constructor() {
1152 assert_eq!(Width::fraction(3), Width::Fraction(3));
1153 }
1154
1155 #[test]
1158 fn overflow_default() {
1159 let overflow = Overflow::default();
1160 assert!(matches!(
1161 overflow,
1162 Overflow::Truncate {
1163 at: TruncateAt::End,
1164 ..
1165 }
1166 ));
1167 }
1168
1169 #[test]
1170 fn overflow_constructors() {
1171 let truncate = Overflow::truncate(TruncateAt::Middle);
1172 assert!(matches!(
1173 truncate,
1174 Overflow::Truncate {
1175 at: TruncateAt::Middle,
1176 ref marker
1177 } if marker == "…"
1178 ));
1179
1180 let truncate_custom = Overflow::truncate_with_marker(TruncateAt::Start, "...");
1181 assert!(matches!(
1182 truncate_custom,
1183 Overflow::Truncate {
1184 at: TruncateAt::Start,
1185 ref marker
1186 } if marker == "..."
1187 ));
1188
1189 let wrap = Overflow::wrap();
1190 assert!(matches!(wrap, Overflow::Wrap { indent: 0 }));
1191
1192 let wrap_indent = Overflow::wrap_with_indent(4);
1193 assert!(matches!(wrap_indent, Overflow::Wrap { indent: 4 }));
1194 }
1195
1196 #[test]
1199 fn anchor_default() {
1200 assert_eq!(Anchor::default(), Anchor::Left);
1201 }
1202
1203 #[test]
1204 fn anchor_serde_roundtrip() {
1205 let values = [Anchor::Left, Anchor::Right];
1206 for anchor in values {
1207 let json = serde_json::to_string(&anchor).unwrap();
1208 let parsed: Anchor = serde_json::from_str(&json).unwrap();
1209 assert_eq!(parsed, anchor);
1210 }
1211 }
1212
1213 #[test]
1216 fn col_shorthand_constructors() {
1217 let fixed = Col::fixed(10);
1218 assert_eq!(fixed.width, Width::Fixed(10));
1219
1220 let min = Col::min(5);
1221 assert_eq!(
1222 min.width,
1223 Width::Bounded {
1224 min: Some(5),
1225 max: None
1226 }
1227 );
1228
1229 let bounded = Col::bounded(5, 20);
1230 assert_eq!(
1231 bounded.width,
1232 Width::Bounded {
1233 min: Some(5),
1234 max: Some(20)
1235 }
1236 );
1237
1238 let fill = Col::fill();
1239 assert_eq!(fill.width, Width::Fill);
1240
1241 let fraction = Col::fraction(3);
1242 assert_eq!(fraction.width, Width::Fraction(3));
1243 }
1244
1245 #[test]
1246 fn col_shorthand_chaining() {
1247 let col = Col::fixed(10).right().anchor_right().style("header");
1248 assert_eq!(col.width, Width::Fixed(10));
1249 assert_eq!(col.align, Align::Right);
1250 assert_eq!(col.anchor, Anchor::Right);
1251 assert_eq!(col.style, Some("header".to_string()));
1252 }
1253
1254 #[test]
1255 fn column_wrap_shorthand() {
1256 let col = Col::fill().wrap();
1257 assert!(matches!(col.overflow, Overflow::Wrap { indent: 0 }));
1258
1259 let col_indent = Col::fill().wrap_indent(2);
1260 assert!(matches!(col_indent.overflow, Overflow::Wrap { indent: 2 }));
1261 }
1262
1263 #[test]
1264 fn column_clip_shorthand() {
1265 let col = Col::fixed(10).clip();
1266 assert!(matches!(col.overflow, Overflow::Clip));
1267 }
1268
1269 #[test]
1270 fn column_named() {
1271 let col = Col::fixed(10).named("author");
1272 assert_eq!(col.name, Some("author".to_string()));
1273 }
1274
1275 #[test]
1278 fn column_defaults() {
1279 let col = Column::default();
1280 assert!(matches!(
1281 col.width,
1282 Width::Bounded {
1283 min: None,
1284 max: None
1285 }
1286 ));
1287 assert_eq!(col.align, Align::Left);
1288 assert_eq!(col.anchor, Anchor::Left);
1289 assert!(matches!(
1290 col.overflow,
1291 Overflow::Truncate {
1292 at: TruncateAt::End,
1293 ..
1294 }
1295 ));
1296 assert_eq!(col.null_repr, "-");
1297 assert!(col.style.is_none());
1298 }
1299
1300 #[test]
1301 fn column_fluent_api() {
1302 let col = Column::new(Width::Fixed(10))
1303 .align(Align::Right)
1304 .truncate(TruncateAt::Middle)
1305 .ellipsis("...")
1306 .null_repr("N/A")
1307 .style("header");
1308
1309 assert_eq!(col.width, Width::Fixed(10));
1310 assert_eq!(col.align, Align::Right);
1311 assert!(matches!(
1312 col.overflow,
1313 Overflow::Truncate {
1314 at: TruncateAt::Middle,
1315 ref marker
1316 } if marker == "..."
1317 ));
1318 assert_eq!(col.null_repr, "N/A");
1319 assert_eq!(col.style, Some("header".to_string()));
1320 }
1321
1322 #[test]
1323 fn column_builder() {
1324 let col = Column::builder()
1325 .fixed(15)
1326 .align(Align::Center)
1327 .truncate(TruncateAt::Start)
1328 .build();
1329
1330 assert_eq!(col.width, Width::Fixed(15));
1331 assert_eq!(col.align, Align::Center);
1332 assert!(matches!(
1333 col.overflow,
1334 Overflow::Truncate {
1335 at: TruncateAt::Start,
1336 ..
1337 }
1338 ));
1339 }
1340
1341 #[test]
1342 fn column_builder_fill() {
1343 let col = Column::builder().fill().build();
1344 assert_eq!(col.width, Width::Fill);
1345 }
1346
1347 #[test]
1350 fn decorations_default() {
1351 let dec = Decorations::default();
1352 assert_eq!(dec.column_sep, "");
1353 assert_eq!(dec.row_prefix, "");
1354 assert_eq!(dec.row_suffix, "");
1355 }
1356
1357 #[test]
1358 fn decorations_with_separator() {
1359 let dec = Decorations::with_separator(" ");
1360 assert_eq!(dec.column_sep, " ");
1361 }
1362
1363 #[test]
1364 fn decorations_overhead() {
1365 let dec = Decorations::default()
1366 .separator(" ")
1367 .prefix("│ ")
1368 .suffix(" │");
1369
1370 assert_eq!(dec.overhead(3), 8);
1372 assert_eq!(dec.overhead(1), 4);
1374 assert_eq!(dec.overhead(0), 4);
1376 }
1377
1378 #[test]
1381 fn flat_data_spec_builder() {
1382 let spec = FlatDataSpec::builder()
1383 .column(Column::new(Width::Fixed(8)))
1384 .column(Column::new(Width::Fill))
1385 .column(Column::new(Width::Fixed(10)))
1386 .separator(" ")
1387 .build();
1388
1389 assert_eq!(spec.num_columns(), 3);
1390 assert!(spec.has_fill_column());
1391 assert_eq!(spec.decorations.column_sep, " ");
1392 }
1393
1394 #[test]
1395 fn table_spec_no_fill() {
1396 let spec = TabularSpec::builder()
1397 .column(Column::new(Width::Fixed(8)))
1398 .column(Column::new(Width::Fixed(10)))
1399 .build();
1400
1401 assert!(!spec.has_fill_column());
1402 }
1403
1404 #[test]
1405 fn extract_fields_from_json() {
1406 let json = serde_json::json!({
1407 "name": "Alice",
1408 "meta": {
1409 "age": 30,
1410 "role": "admin"
1411 }
1412 });
1413
1414 let spec = FlatDataSpec::builder()
1415 .column(Column::new(Width::Fixed(10)).key("name"))
1416 .column(Column::new(Width::Fixed(5)).key("meta.age"))
1417 .column(Column::new(Width::Fixed(10)).key("meta.role"))
1418 .column(Column::new(Width::Fixed(10)).key("missing.field")) .build();
1420
1421 let row = spec.extract_row(&json);
1422 assert_eq!(row[0], "Alice");
1423 assert_eq!(row[1], "30"); assert_eq!(row[2], "admin");
1425 assert_eq!(row[3], "-"); }
1427
1428 #[test]
1429 fn extract_header_row() {
1430 let spec = FlatDataSpec::builder()
1431 .column(Column::new(Width::Fixed(10)).header("Name").key("name"))
1432 .column(Column::new(Width::Fixed(5)).key("age")) .column(Column::new(Width::Fixed(10))) .build();
1435
1436 let header = spec.extract_header();
1437 assert_eq!(header[0], "Name");
1438 assert_eq!(header[1], "age");
1439 assert_eq!(header[2], "");
1440 }
1441
1442 #[test]
1445 fn sub_column_defaults() {
1446 let sc = SubColumn::default();
1447 assert_eq!(sc.width, Width::Fill);
1448 assert_eq!(sc.align, Align::Left);
1449 assert!(sc.name.is_none());
1450 assert!(sc.style.is_none());
1451 assert_eq!(sc.null_repr, "");
1452 }
1453
1454 #[test]
1455 fn sub_column_fluent_api() {
1456 let sc = SubColumn::new(Width::Fixed(10))
1457 .named("tag")
1458 .right()
1459 .style("tag_style")
1460 .null_repr("N/A");
1461
1462 assert_eq!(sc.width, Width::Fixed(10));
1463 assert_eq!(sc.name, Some("tag".to_string()));
1464 assert_eq!(sc.align, Align::Right);
1465 assert_eq!(sc.style, Some("tag_style".to_string()));
1466 assert_eq!(sc.null_repr, "N/A");
1467 }
1468
1469 #[test]
1470 fn sub_col_shorthand_constructors() {
1471 let fill = SubCol::fill();
1472 assert_eq!(fill.width, Width::Fill);
1473
1474 let fixed = SubCol::fixed(10);
1475 assert_eq!(fixed.width, Width::Fixed(10));
1476
1477 let bounded = SubCol::bounded(0, 30);
1478 assert_eq!(
1479 bounded.width,
1480 Width::Bounded {
1481 min: Some(0),
1482 max: Some(30)
1483 }
1484 );
1485
1486 let max = SubCol::max(20);
1487 assert_eq!(
1488 max.width,
1489 Width::Bounded {
1490 min: None,
1491 max: Some(20)
1492 }
1493 );
1494
1495 let min = SubCol::min(5);
1496 assert_eq!(
1497 min.width,
1498 Width::Bounded {
1499 min: Some(5),
1500 max: None
1501 }
1502 );
1503 }
1504
1505 #[test]
1506 fn sub_col_shorthand_chaining() {
1507 let sc = SubCol::bounded(0, 30).right().style("tag");
1508 assert_eq!(sc.align, Align::Right);
1509 assert_eq!(sc.style, Some("tag".to_string()));
1510 }
1511
1512 #[test]
1515 fn sub_columns_valid_construction() {
1516 let result = SubColumns::new(vec![SubCol::fill(), SubCol::bounded(0, 30)], " ");
1517 assert!(result.is_ok());
1518 let sc = result.unwrap();
1519 assert_eq!(sc.columns.len(), 2);
1520 assert_eq!(sc.separator, " ");
1521 }
1522
1523 #[test]
1524 fn sub_columns_rejects_empty() {
1525 let result = SubColumns::new(vec![], " ");
1526 assert!(result.is_err());
1527 assert!(result.unwrap_err().contains("at least one"));
1528 }
1529
1530 #[test]
1531 fn sub_columns_rejects_no_fill() {
1532 let result = SubColumns::new(vec![SubCol::fixed(10), SubCol::bounded(0, 30)], " ");
1533 assert!(result.is_err());
1534 assert!(result.unwrap_err().contains("exactly one Fill"));
1535 }
1536
1537 #[test]
1538 fn sub_columns_rejects_two_fills() {
1539 let result = SubColumns::new(vec![SubCol::fill(), SubCol::fill()], " ");
1540 assert!(result.is_err());
1541 assert!(result.unwrap_err().contains("exactly one Fill"));
1542 }
1543
1544 #[test]
1545 fn sub_columns_rejects_fraction() {
1546 let result = SubColumns::new(
1547 vec![SubCol::fill(), SubColumn::new(Width::Fraction(2))],
1548 " ",
1549 );
1550 assert!(result.is_err());
1551 assert!(result.unwrap_err().contains("Fraction"));
1552 }
1553
1554 #[test]
1555 fn sub_columns_serde_roundtrip() {
1556 let sc = SubColumns::new(
1557 vec![
1558 SubCol::fill().named("title"),
1559 SubCol::bounded(0, 30).right().named("tag"),
1560 ],
1561 " ",
1562 )
1563 .unwrap();
1564
1565 let json = serde_json::to_string(&sc).unwrap();
1566 let parsed: SubColumns = serde_json::from_str(&json).unwrap();
1567 assert_eq!(parsed.columns.len(), 2);
1568 assert_eq!(parsed.separator, " ");
1569 assert_eq!(parsed.columns[0].width, Width::Fill);
1570 assert_eq!(
1571 parsed.columns[1].width,
1572 Width::Bounded {
1573 min: Some(0),
1574 max: Some(30)
1575 }
1576 );
1577 }
1578
1579 #[test]
1580 fn column_with_sub_columns() {
1581 let sub_cols =
1582 SubColumns::new(vec![SubCol::fill(), SubCol::bounded(0, 30).right()], " ").unwrap();
1583
1584 let col = Col::fill().sub_columns(sub_cols);
1585 assert!(col.sub_columns.is_some());
1586 assert_eq!(col.sub_columns.unwrap().columns.len(), 2);
1587 }
1588
1589 #[test]
1590 fn column_builder_with_sub_columns() {
1591 let sub_cols = SubColumns::new(vec![SubCol::fill(), SubCol::fixed(8)], " ").unwrap();
1592
1593 let col = Column::builder().fill().sub_columns(sub_cols).build();
1594
1595 assert_eq!(col.width, Width::Fill);
1596 assert!(col.sub_columns.is_some());
1597 }
1598}