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}
250
251impl Default for Column {
252 fn default() -> Self {
253 Column {
254 name: None,
255 width: Width::default(),
256 align: Align::default(),
257 anchor: Anchor::default(),
258 overflow: Overflow::default(),
259 null_repr: "-".to_string(),
260 style: None,
261 style_from_value: false,
262 key: None,
263 header: None,
264 }
265 }
266}
267
268impl Column {
269 pub fn new(width: Width) -> Self {
271 Column {
272 width,
273 ..Default::default()
274 }
275 }
276
277 pub fn builder() -> ColumnBuilder {
279 ColumnBuilder::default()
280 }
281
282 pub fn named(mut self, name: impl Into<String>) -> Self {
284 self.name = Some(name.into());
285 self
286 }
287
288 pub fn align(mut self, align: Align) -> Self {
290 self.align = align;
291 self
292 }
293
294 pub fn right(self) -> Self {
296 self.align(Align::Right)
297 }
298
299 pub fn center(self) -> Self {
301 self.align(Align::Center)
302 }
303
304 pub fn anchor(mut self, anchor: Anchor) -> Self {
306 self.anchor = anchor;
307 self
308 }
309
310 pub fn anchor_right(self) -> Self {
312 self.anchor(Anchor::Right)
313 }
314
315 pub fn overflow(mut self, overflow: Overflow) -> Self {
317 self.overflow = overflow;
318 self
319 }
320
321 pub fn wrap(self) -> Self {
323 self.overflow(Overflow::wrap())
324 }
325
326 pub fn wrap_indent(self, indent: usize) -> Self {
328 self.overflow(Overflow::wrap_with_indent(indent))
329 }
330
331 pub fn clip(self) -> Self {
333 self.overflow(Overflow::Clip)
334 }
335
336 pub fn truncate(mut self, at: TruncateAt) -> Self {
338 self.overflow = match self.overflow {
339 Overflow::Truncate { marker, .. } => Overflow::Truncate { at, marker },
340 _ => Overflow::truncate(at),
341 };
342 self
343 }
344
345 pub fn truncate_middle(self) -> Self {
347 self.truncate(TruncateAt::Middle)
348 }
349
350 pub fn truncate_start(self) -> Self {
352 self.truncate(TruncateAt::Start)
353 }
354
355 pub fn ellipsis(mut self, ellipsis: impl Into<String>) -> Self {
357 self.overflow = match self.overflow {
358 Overflow::Truncate { at, .. } => Overflow::Truncate {
359 at,
360 marker: ellipsis.into(),
361 },
362 _ => Overflow::truncate_with_marker(TruncateAt::End, ellipsis),
363 };
364 self
365 }
366
367 pub fn null_repr(mut self, null_repr: impl Into<String>) -> Self {
369 self.null_repr = null_repr.into();
370 self
371 }
372
373 pub fn style(mut self, style: impl Into<String>) -> Self {
375 self.style = Some(style.into());
376 self
377 }
378
379 pub fn style_from_value(mut self) -> Self {
384 self.style_from_value = true;
385 self
386 }
387
388 pub fn key(mut self, key: impl Into<String>) -> Self {
390 self.key = Some(key.into());
391 self
392 }
393
394 pub fn header(mut self, header: impl Into<String>) -> Self {
396 self.header = Some(header.into());
397 self
398 }
399}
400
401#[derive(Clone, Debug, Default)]
403pub struct ColumnBuilder {
404 name: Option<String>,
405 width: Option<Width>,
406 align: Option<Align>,
407 anchor: Option<Anchor>,
408 overflow: Option<Overflow>,
409 null_repr: Option<String>,
410 style: Option<String>,
411 style_from_value: bool,
412 key: Option<String>,
413 header: Option<String>,
414}
415
416impl ColumnBuilder {
417 pub fn named(mut self, name: impl Into<String>) -> Self {
419 self.name = Some(name.into());
420 self
421 }
422
423 pub fn width(mut self, width: Width) -> Self {
425 self.width = Some(width);
426 self
427 }
428
429 pub fn fixed(mut self, width: usize) -> Self {
431 self.width = Some(Width::Fixed(width));
432 self
433 }
434
435 pub fn fill(mut self) -> Self {
437 self.width = Some(Width::Fill);
438 self
439 }
440
441 pub fn bounded(mut self, min: usize, max: usize) -> Self {
443 self.width = Some(Width::bounded(min, max));
444 self
445 }
446
447 pub fn fraction(mut self, n: usize) -> Self {
449 self.width = Some(Width::Fraction(n));
450 self
451 }
452
453 pub fn align(mut self, align: Align) -> Self {
455 self.align = Some(align);
456 self
457 }
458
459 pub fn right(self) -> Self {
461 self.align(Align::Right)
462 }
463
464 pub fn center(self) -> Self {
466 self.align(Align::Center)
467 }
468
469 pub fn anchor(mut self, anchor: Anchor) -> Self {
471 self.anchor = Some(anchor);
472 self
473 }
474
475 pub fn anchor_right(self) -> Self {
477 self.anchor(Anchor::Right)
478 }
479
480 pub fn overflow(mut self, overflow: Overflow) -> Self {
482 self.overflow = Some(overflow);
483 self
484 }
485
486 pub fn wrap(self) -> Self {
488 self.overflow(Overflow::wrap())
489 }
490
491 pub fn clip(self) -> Self {
493 self.overflow(Overflow::Clip)
494 }
495
496 pub fn truncate(mut self, at: TruncateAt) -> Self {
498 self.overflow = Some(match self.overflow {
499 Some(Overflow::Truncate { marker, .. }) => Overflow::Truncate { at, marker },
500 _ => Overflow::truncate(at),
501 });
502 self
503 }
504
505 pub fn ellipsis(mut self, ellipsis: impl Into<String>) -> Self {
507 self.overflow = Some(match self.overflow {
508 Some(Overflow::Truncate { at, .. }) => Overflow::Truncate {
509 at,
510 marker: ellipsis.into(),
511 },
512 _ => Overflow::truncate_with_marker(TruncateAt::End, ellipsis),
513 });
514 self
515 }
516
517 pub fn null_repr(mut self, null_repr: impl Into<String>) -> Self {
519 self.null_repr = Some(null_repr.into());
520 self
521 }
522
523 pub fn style(mut self, style: impl Into<String>) -> Self {
525 self.style = Some(style.into());
526 self
527 }
528
529 pub fn style_from_value(mut self) -> Self {
531 self.style_from_value = true;
532 self
533 }
534
535 pub fn key(mut self, key: impl Into<String>) -> Self {
537 self.key = Some(key.into());
538 self
539 }
540
541 pub fn header(mut self, header: impl Into<String>) -> Self {
543 self.header = Some(header.into());
544 self
545 }
546
547 pub fn build(self) -> Column {
549 let default = Column::default();
550 Column {
551 name: self.name,
552 width: self.width.unwrap_or(default.width),
553 align: self.align.unwrap_or(default.align),
554 anchor: self.anchor.unwrap_or(default.anchor),
555 overflow: self.overflow.unwrap_or(default.overflow),
556 null_repr: self.null_repr.unwrap_or(default.null_repr),
557 style: self.style,
558 style_from_value: self.style_from_value,
559 key: self.key,
560 header: self.header,
561 }
562 }
563}
564
565pub struct Col;
582
583impl Col {
584 pub fn fixed(width: usize) -> Column {
586 Column::new(Width::Fixed(width))
587 }
588
589 pub fn min(min: usize) -> Column {
591 Column::new(Width::min(min))
592 }
593
594 pub fn max(max: usize) -> Column {
596 Column::new(Width::max(max))
597 }
598
599 pub fn bounded(min: usize, max: usize) -> Column {
601 Column::new(Width::bounded(min, max))
602 }
603
604 pub fn fill() -> Column {
606 Column::new(Width::Fill)
607 }
608
609 pub fn fraction(n: usize) -> Column {
612 Column::new(Width::Fraction(n))
613 }
614}
615
616#[derive(Clone, Debug, Default, Serialize, Deserialize)]
618pub struct Decorations {
619 pub column_sep: String,
621 pub row_prefix: String,
623 pub row_suffix: String,
625}
626
627impl Decorations {
628 pub fn with_separator(sep: impl Into<String>) -> Self {
630 Decorations {
631 column_sep: sep.into(),
632 row_prefix: String::new(),
633 row_suffix: String::new(),
634 }
635 }
636
637 pub fn separator(mut self, sep: impl Into<String>) -> Self {
639 self.column_sep = sep.into();
640 self
641 }
642
643 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
645 self.row_prefix = prefix.into();
646 self
647 }
648
649 pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
651 self.row_suffix = suffix.into();
652 self
653 }
654
655 pub fn overhead(&self, num_columns: usize) -> usize {
657 use crate::tabular::display_width;
658 let prefix_width = display_width(&self.row_prefix);
659 let suffix_width = display_width(&self.row_suffix);
660 let sep_width = display_width(&self.column_sep);
661 let sep_count = num_columns.saturating_sub(1);
662 prefix_width + suffix_width + (sep_width * sep_count)
663 }
664}
665
666#[derive(Clone, Debug, Serialize, Deserialize)]
668pub struct FlatDataSpec {
669 pub columns: Vec<Column>,
671 pub decorations: Decorations,
673}
674
675impl FlatDataSpec {
676 pub fn new(columns: Vec<Column>) -> Self {
678 FlatDataSpec {
679 columns,
680 decorations: Decorations::default(),
681 }
682 }
683
684 pub fn builder() -> FlatDataSpecBuilder {
686 FlatDataSpecBuilder::default()
687 }
688
689 pub fn num_columns(&self) -> usize {
691 self.columns.len()
692 }
693
694 pub fn has_fill_column(&self) -> bool {
696 self.columns.iter().any(|c| matches!(c.width, Width::Fill))
697 }
698
699 pub fn extract_header(&self) -> Vec<String> {
703 self.columns
704 .iter()
705 .map(|col| {
706 col.header
707 .as_deref()
708 .or(col.key.as_deref())
709 .unwrap_or("")
710 .to_string()
711 })
712 .collect()
713 }
714
715 pub fn extract_row(&self, data: &Value) -> Vec<String> {
722 self.columns
723 .iter()
724 .map(|col| {
725 if let Some(key) = &col.key {
726 extract_value(data, key).unwrap_or(col.null_repr.clone())
727 } else {
728 col.null_repr.clone()
729 }
730 })
731 .collect()
732 }
733}
734
735fn extract_value(data: &Value, path: &str) -> Option<String> {
737 let mut current = data;
738 for part in path.split('.') {
739 match current {
740 Value::Object(map) => {
741 current = map.get(part)?;
742 }
743 _ => return None,
744 }
745 }
746
747 match current {
748 Value::String(s) => Some(s.clone()),
749 Value::Null => None,
750 v => Some(v.to_string()),
752 }
753}
754
755#[derive(Clone, Debug, Default)]
757pub struct FlatDataSpecBuilder {
758 columns: Vec<Column>,
759 decorations: Decorations,
760}
761
762impl FlatDataSpecBuilder {
763 pub fn column(mut self, column: Column) -> Self {
765 self.columns.push(column);
766 self
767 }
768
769 pub fn columns(mut self, columns: impl IntoIterator<Item = Column>) -> Self {
771 self.columns.extend(columns);
772 self
773 }
774
775 pub fn separator(mut self, sep: impl Into<String>) -> Self {
777 self.decorations.column_sep = sep.into();
778 self
779 }
780
781 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
783 self.decorations.row_prefix = prefix.into();
784 self
785 }
786
787 pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
789 self.decorations.row_suffix = suffix.into();
790 self
791 }
792
793 pub fn decorations(mut self, decorations: Decorations) -> Self {
795 self.decorations = decorations;
796 self
797 }
798
799 pub fn build(self) -> FlatDataSpec {
801 FlatDataSpec {
802 columns: self.columns,
803 decorations: self.decorations,
804 }
805 }
806}
807
808pub type TabularSpec = FlatDataSpec;
810pub type TabularSpecBuilder = FlatDataSpecBuilder;
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816
817 #[test]
820 fn align_default_is_left() {
821 assert_eq!(Align::default(), Align::Left);
822 }
823
824 #[test]
825 fn align_serde_roundtrip() {
826 let values = [Align::Left, Align::Right, Align::Center];
827 for align in values {
828 let json = serde_json::to_string(&align).unwrap();
829 let parsed: Align = serde_json::from_str(&json).unwrap();
830 assert_eq!(parsed, align);
831 }
832 }
833
834 #[test]
837 fn truncate_at_default_is_end() {
838 assert_eq!(TruncateAt::default(), TruncateAt::End);
839 }
840
841 #[test]
842 fn truncate_at_serde_roundtrip() {
843 let values = [TruncateAt::End, TruncateAt::Start, TruncateAt::Middle];
844 for truncate in values {
845 let json = serde_json::to_string(&truncate).unwrap();
846 let parsed: TruncateAt = serde_json::from_str(&json).unwrap();
847 assert_eq!(parsed, truncate);
848 }
849 }
850
851 #[test]
854 fn width_constructors() {
855 assert_eq!(Width::fixed(10), Width::Fixed(10));
856 assert_eq!(
857 Width::bounded(5, 20),
858 Width::Bounded {
859 min: Some(5),
860 max: Some(20)
861 }
862 );
863 assert_eq!(
864 Width::min(5),
865 Width::Bounded {
866 min: Some(5),
867 max: None
868 }
869 );
870 assert_eq!(
871 Width::max(20),
872 Width::Bounded {
873 min: None,
874 max: Some(20)
875 }
876 );
877 assert_eq!(Width::fill(), Width::Fill);
878 }
879
880 #[test]
881 fn width_serde_fixed() {
882 let width = Width::Fixed(10);
883 let json = serde_json::to_string(&width).unwrap();
884 assert_eq!(json, "10");
885 let parsed: Width = serde_json::from_str(&json).unwrap();
886 assert_eq!(parsed, width);
887 }
888
889 #[test]
890 fn width_serde_bounded() {
891 let width = Width::Bounded {
892 min: Some(5),
893 max: Some(20),
894 };
895 let json = serde_json::to_string(&width).unwrap();
896 let parsed: Width = serde_json::from_str(&json).unwrap();
897 assert_eq!(parsed, width);
898 }
899
900 #[test]
901 fn width_serde_fill() {
902 let width = Width::Fill;
903 let json = serde_json::to_string(&width).unwrap();
904 assert_eq!(json, "\"fill\"");
906
907 let parsed: Width = serde_json::from_str("\"fill\"").unwrap();
908 assert_eq!(parsed, width);
909 }
910
911 #[test]
912 fn width_serde_fraction() {
913 let width = Width::Fraction(2);
914 let json = serde_json::to_string(&width).unwrap();
915 assert_eq!(json, "\"2fr\"");
916
917 let parsed: Width = serde_json::from_str("\"2fr\"").unwrap();
918 assert_eq!(parsed, width);
919
920 let parsed_1: Width = serde_json::from_str("\"1fr\"").unwrap();
922 assert_eq!(parsed_1, Width::Fraction(1));
923 }
924
925 #[test]
926 fn width_fraction_constructor() {
927 assert_eq!(Width::fraction(3), Width::Fraction(3));
928 }
929
930 #[test]
933 fn overflow_default() {
934 let overflow = Overflow::default();
935 assert!(matches!(
936 overflow,
937 Overflow::Truncate {
938 at: TruncateAt::End,
939 ..
940 }
941 ));
942 }
943
944 #[test]
945 fn overflow_constructors() {
946 let truncate = Overflow::truncate(TruncateAt::Middle);
947 assert!(matches!(
948 truncate,
949 Overflow::Truncate {
950 at: TruncateAt::Middle,
951 ref marker
952 } if marker == "…"
953 ));
954
955 let truncate_custom = Overflow::truncate_with_marker(TruncateAt::Start, "...");
956 assert!(matches!(
957 truncate_custom,
958 Overflow::Truncate {
959 at: TruncateAt::Start,
960 ref marker
961 } if marker == "..."
962 ));
963
964 let wrap = Overflow::wrap();
965 assert!(matches!(wrap, Overflow::Wrap { indent: 0 }));
966
967 let wrap_indent = Overflow::wrap_with_indent(4);
968 assert!(matches!(wrap_indent, Overflow::Wrap { indent: 4 }));
969 }
970
971 #[test]
974 fn anchor_default() {
975 assert_eq!(Anchor::default(), Anchor::Left);
976 }
977
978 #[test]
979 fn anchor_serde_roundtrip() {
980 let values = [Anchor::Left, Anchor::Right];
981 for anchor in values {
982 let json = serde_json::to_string(&anchor).unwrap();
983 let parsed: Anchor = serde_json::from_str(&json).unwrap();
984 assert_eq!(parsed, anchor);
985 }
986 }
987
988 #[test]
991 fn col_shorthand_constructors() {
992 let fixed = Col::fixed(10);
993 assert_eq!(fixed.width, Width::Fixed(10));
994
995 let min = Col::min(5);
996 assert_eq!(
997 min.width,
998 Width::Bounded {
999 min: Some(5),
1000 max: None
1001 }
1002 );
1003
1004 let bounded = Col::bounded(5, 20);
1005 assert_eq!(
1006 bounded.width,
1007 Width::Bounded {
1008 min: Some(5),
1009 max: Some(20)
1010 }
1011 );
1012
1013 let fill = Col::fill();
1014 assert_eq!(fill.width, Width::Fill);
1015
1016 let fraction = Col::fraction(3);
1017 assert_eq!(fraction.width, Width::Fraction(3));
1018 }
1019
1020 #[test]
1021 fn col_shorthand_chaining() {
1022 let col = Col::fixed(10).right().anchor_right().style("header");
1023 assert_eq!(col.width, Width::Fixed(10));
1024 assert_eq!(col.align, Align::Right);
1025 assert_eq!(col.anchor, Anchor::Right);
1026 assert_eq!(col.style, Some("header".to_string()));
1027 }
1028
1029 #[test]
1030 fn column_wrap_shorthand() {
1031 let col = Col::fill().wrap();
1032 assert!(matches!(col.overflow, Overflow::Wrap { indent: 0 }));
1033
1034 let col_indent = Col::fill().wrap_indent(2);
1035 assert!(matches!(col_indent.overflow, Overflow::Wrap { indent: 2 }));
1036 }
1037
1038 #[test]
1039 fn column_clip_shorthand() {
1040 let col = Col::fixed(10).clip();
1041 assert!(matches!(col.overflow, Overflow::Clip));
1042 }
1043
1044 #[test]
1045 fn column_named() {
1046 let col = Col::fixed(10).named("author");
1047 assert_eq!(col.name, Some("author".to_string()));
1048 }
1049
1050 #[test]
1053 fn column_defaults() {
1054 let col = Column::default();
1055 assert!(matches!(
1056 col.width,
1057 Width::Bounded {
1058 min: None,
1059 max: None
1060 }
1061 ));
1062 assert_eq!(col.align, Align::Left);
1063 assert_eq!(col.anchor, Anchor::Left);
1064 assert!(matches!(
1065 col.overflow,
1066 Overflow::Truncate {
1067 at: TruncateAt::End,
1068 ..
1069 }
1070 ));
1071 assert_eq!(col.null_repr, "-");
1072 assert!(col.style.is_none());
1073 }
1074
1075 #[test]
1076 fn column_fluent_api() {
1077 let col = Column::new(Width::Fixed(10))
1078 .align(Align::Right)
1079 .truncate(TruncateAt::Middle)
1080 .ellipsis("...")
1081 .null_repr("N/A")
1082 .style("header");
1083
1084 assert_eq!(col.width, Width::Fixed(10));
1085 assert_eq!(col.align, Align::Right);
1086 assert!(matches!(
1087 col.overflow,
1088 Overflow::Truncate {
1089 at: TruncateAt::Middle,
1090 ref marker
1091 } if marker == "..."
1092 ));
1093 assert_eq!(col.null_repr, "N/A");
1094 assert_eq!(col.style, Some("header".to_string()));
1095 }
1096
1097 #[test]
1098 fn column_builder() {
1099 let col = Column::builder()
1100 .fixed(15)
1101 .align(Align::Center)
1102 .truncate(TruncateAt::Start)
1103 .build();
1104
1105 assert_eq!(col.width, Width::Fixed(15));
1106 assert_eq!(col.align, Align::Center);
1107 assert!(matches!(
1108 col.overflow,
1109 Overflow::Truncate {
1110 at: TruncateAt::Start,
1111 ..
1112 }
1113 ));
1114 }
1115
1116 #[test]
1117 fn column_builder_fill() {
1118 let col = Column::builder().fill().build();
1119 assert_eq!(col.width, Width::Fill);
1120 }
1121
1122 #[test]
1125 fn decorations_default() {
1126 let dec = Decorations::default();
1127 assert_eq!(dec.column_sep, "");
1128 assert_eq!(dec.row_prefix, "");
1129 assert_eq!(dec.row_suffix, "");
1130 }
1131
1132 #[test]
1133 fn decorations_with_separator() {
1134 let dec = Decorations::with_separator(" ");
1135 assert_eq!(dec.column_sep, " ");
1136 }
1137
1138 #[test]
1139 fn decorations_overhead() {
1140 let dec = Decorations::default()
1141 .separator(" ")
1142 .prefix("│ ")
1143 .suffix(" │");
1144
1145 assert_eq!(dec.overhead(3), 8);
1147 assert_eq!(dec.overhead(1), 4);
1149 assert_eq!(dec.overhead(0), 4);
1151 }
1152
1153 #[test]
1156 fn flat_data_spec_builder() {
1157 let spec = FlatDataSpec::builder()
1158 .column(Column::new(Width::Fixed(8)))
1159 .column(Column::new(Width::Fill))
1160 .column(Column::new(Width::Fixed(10)))
1161 .separator(" ")
1162 .build();
1163
1164 assert_eq!(spec.num_columns(), 3);
1165 assert!(spec.has_fill_column());
1166 assert_eq!(spec.decorations.column_sep, " ");
1167 }
1168
1169 #[test]
1170 fn table_spec_no_fill() {
1171 let spec = TabularSpec::builder()
1172 .column(Column::new(Width::Fixed(8)))
1173 .column(Column::new(Width::Fixed(10)))
1174 .build();
1175
1176 assert!(!spec.has_fill_column());
1177 }
1178
1179 #[test]
1180 fn extract_fields_from_json() {
1181 let json = serde_json::json!({
1182 "name": "Alice",
1183 "meta": {
1184 "age": 30,
1185 "role": "admin"
1186 }
1187 });
1188
1189 let spec = FlatDataSpec::builder()
1190 .column(Column::new(Width::Fixed(10)).key("name"))
1191 .column(Column::new(Width::Fixed(5)).key("meta.age"))
1192 .column(Column::new(Width::Fixed(10)).key("meta.role"))
1193 .column(Column::new(Width::Fixed(10)).key("missing.field")) .build();
1195
1196 let row = spec.extract_row(&json);
1197 assert_eq!(row[0], "Alice");
1198 assert_eq!(row[1], "30"); assert_eq!(row[2], "admin");
1200 assert_eq!(row[3], "-"); }
1202
1203 #[test]
1204 fn extract_header_row() {
1205 let spec = FlatDataSpec::builder()
1206 .column(Column::new(Width::Fixed(10)).header("Name").key("name"))
1207 .column(Column::new(Width::Fixed(5)).key("age")) .column(Column::new(Width::Fixed(10))) .build();
1210
1211 let header = spec.extract_header();
1212 assert_eq!(header[0], "Name");
1213 assert_eq!(header[1], "age");
1214 assert_eq!(header[2], "");
1215 }
1216}