Skip to main content

standout_render/tabular/
types.rs

1//! Core types for tabular output configuration.
2//!
3//! This module defines the data structures used to specify table layout:
4//! column widths, alignment, truncation strategies, and decorations.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// Text alignment within a column.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum Align {
13    /// Left-align text (pad on the right).
14    #[default]
15    Left,
16    /// Right-align text (pad on the left).
17    Right,
18    /// Center text (pad on both sides).
19    Center,
20}
21
22/// Position where truncation occurs when content exceeds max width.
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum TruncateAt {
26    /// Truncate at the end, keeping the start visible.
27    /// Example: "Hello World" → "Hello W…"
28    #[default]
29    End,
30    /// Truncate at the start, keeping the end visible.
31    /// Example: "Hello World" → "…o World"
32    Start,
33    /// Truncate in the middle, keeping both start and end visible.
34    /// Example: "Hello World" → "Hel…orld"
35    Middle,
36}
37
38/// How a column handles content that exceeds its width.
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub enum Overflow {
41    /// Truncate content with an ellipsis marker.
42    Truncate {
43        /// Where to truncate (start, middle, or end).
44        at: TruncateAt,
45        /// The marker to show when truncation occurs (default: "…").
46        marker: String,
47    },
48    /// Wrap content to multiple lines at word boundaries.
49    Wrap {
50        /// Number of spaces to indent continuation lines (default: 0).
51        indent: usize,
52    },
53    /// Hard cut without any marker.
54    Clip,
55    /// Allow content to overflow (ignore width limit).
56    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    /// Create a truncate overflow with default marker.
70    pub fn truncate(at: TruncateAt) -> Self {
71        Overflow::Truncate {
72            at,
73            marker: "…".to_string(),
74        }
75    }
76
77    /// Create a truncate overflow with custom marker.
78    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    /// Create a wrap overflow with no indent.
86    pub fn wrap() -> Self {
87        Overflow::Wrap { indent: 0 }
88    }
89
90    /// Create a wrap overflow with continuation indent.
91    pub fn wrap_with_indent(indent: usize) -> Self {
92        Overflow::Wrap { indent }
93    }
94}
95
96/// Column position anchor on the row.
97#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "lowercase")]
99pub enum Anchor {
100    /// Column flows left-to-right from the start (default).
101    #[default]
102    Left,
103    /// Column is positioned at the right edge.
104    Right,
105}
106
107/// Specifies how a column determines its width.
108#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(try_from = "WidthRaw", into = "WidthRaw")]
110pub enum Width {
111    /// Fixed width in display columns.
112    Fixed(usize),
113    /// Width calculated from content, constrained by optional min/max bounds.
114    Bounded {
115        /// Minimum width (defaults to 0 if not specified).
116        min: Option<usize>,
117        /// Maximum width (unlimited if not specified).
118        max: Option<usize>,
119    },
120    /// Expand to fill all remaining space.
121    /// Multiple Fill columns share remaining space equally.
122    Fill,
123    /// Proportional: takes n parts of the remaining space.
124    /// `Fraction(2)` gets twice the space of `Fraction(1)` or `Fill`.
125    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    /// Create a fixed-width column.
186    pub fn fixed(width: usize) -> Self {
187        Width::Fixed(width)
188    }
189
190    /// Create a bounded-width column with both min and max.
191    pub fn bounded(min: usize, max: usize) -> Self {
192        Width::Bounded {
193            min: Some(min),
194            max: Some(max),
195        }
196    }
197
198    /// Create a column with only a minimum width.
199    pub fn min(min: usize) -> Self {
200        Width::Bounded {
201            min: Some(min),
202            max: None,
203        }
204    }
205
206    /// Create a column with only a maximum width.
207    pub fn max(max: usize) -> Self {
208        Width::Bounded {
209            min: None,
210            max: Some(max),
211        }
212    }
213
214    /// Create a fill column that expands to remaining space.
215    pub fn fill() -> Self {
216        Width::Fill
217    }
218
219    /// Create a fractional width column.
220    /// `Fraction(2)` gets twice the space of `Fraction(1)` or `Fill`.
221    pub fn fraction(n: usize) -> Self {
222        Width::Fraction(n)
223    }
224}
225
226/// Configuration for a single column in a table.
227#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct Column {
229    /// Optional column name/identifier.
230    pub name: Option<String>,
231    /// How the column determines its width.
232    pub width: Width,
233    /// Text alignment within the column.
234    pub align: Align,
235    /// Column position anchor (left or right edge).
236    pub anchor: Anchor,
237    /// How to handle content that exceeds width.
238    pub overflow: Overflow,
239    /// Representation for null/empty values.
240    pub null_repr: String,
241    /// Optional style name (resolved via theme).
242    pub style: Option<String>,
243    /// When true, use the cell value as the style name.
244    pub style_from_value: bool,
245    /// Optional key for data extraction (supports dot notation for nested fields).
246    pub key: Option<String>,
247    /// Optional header title (for table headers and CSV export).
248    pub header: Option<String>,
249    /// Optional sub-column layout within this column.
250    ///
251    /// When set, cell values for this column should be arrays of sub-values.
252    /// Sub-column widths are resolved per-row within the parent column's
253    /// resolved width.
254    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    /// Create a new column with the specified width.
277    pub fn new(width: Width) -> Self {
278        Column {
279            width,
280            ..Default::default()
281        }
282    }
283
284    /// Create a column builder for fluent construction.
285    pub fn builder() -> ColumnBuilder {
286        ColumnBuilder::default()
287    }
288
289    /// Set the column name/identifier.
290    pub fn named(mut self, name: impl Into<String>) -> Self {
291        self.name = Some(name.into());
292        self
293    }
294
295    /// Set the text alignment.
296    pub fn align(mut self, align: Align) -> Self {
297        self.align = align;
298        self
299    }
300
301    /// Set alignment to right (shorthand for `.align(Align::Right)`).
302    pub fn right(self) -> Self {
303        self.align(Align::Right)
304    }
305
306    /// Set alignment to center (shorthand for `.align(Align::Center)`).
307    pub fn center(self) -> Self {
308        self.align(Align::Center)
309    }
310
311    /// Set the column anchor position.
312    pub fn anchor(mut self, anchor: Anchor) -> Self {
313        self.anchor = anchor;
314        self
315    }
316
317    /// Anchor column to the right edge (shorthand for `.anchor(Anchor::Right)`).
318    pub fn anchor_right(self) -> Self {
319        self.anchor(Anchor::Right)
320    }
321
322    /// Set the overflow behavior.
323    pub fn overflow(mut self, overflow: Overflow) -> Self {
324        self.overflow = overflow;
325        self
326    }
327
328    /// Set overflow to wrap (shorthand for `.overflow(Overflow::wrap())`).
329    pub fn wrap(self) -> Self {
330        self.overflow(Overflow::wrap())
331    }
332
333    /// Set overflow to wrap with indent.
334    pub fn wrap_indent(self, indent: usize) -> Self {
335        self.overflow(Overflow::wrap_with_indent(indent))
336    }
337
338    /// Set overflow to clip (shorthand for `.overflow(Overflow::Clip)`).
339    pub fn clip(self) -> Self {
340        self.overflow(Overflow::Clip)
341    }
342
343    /// Set truncation position (configures Overflow::Truncate).
344    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    /// Set truncation to middle (shorthand for `.truncate(TruncateAt::Middle)`).
353    pub fn truncate_middle(self) -> Self {
354        self.truncate(TruncateAt::Middle)
355    }
356
357    /// Set truncation to start (shorthand for `.truncate(TruncateAt::Start)`).
358    pub fn truncate_start(self) -> Self {
359        self.truncate(TruncateAt::Start)
360    }
361
362    /// Set the ellipsis/marker for truncation.
363    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    /// Set the null/empty value representation.
375    pub fn null_repr(mut self, null_repr: impl Into<String>) -> Self {
376        self.null_repr = null_repr.into();
377        self
378    }
379
380    /// Set the style name for this column.
381    pub fn style(mut self, style: impl Into<String>) -> Self {
382        self.style = Some(style.into());
383        self
384    }
385
386    /// Use the cell value as the style name.
387    ///
388    /// When enabled, the cell content becomes the style tag.
389    /// For example, cell value "error" renders as `[error]error[/error]`.
390    pub fn style_from_value(mut self) -> Self {
391        self.style_from_value = true;
392        self
393    }
394
395    /// Set the data key for this column (e.g. "author.name").
396    pub fn key(mut self, key: impl Into<String>) -> Self {
397        self.key = Some(key.into());
398        self
399    }
400
401    /// Set the header title for this column.
402    pub fn header(mut self, header: impl Into<String>) -> Self {
403        self.header = Some(header.into());
404        self
405    }
406
407    /// Set sub-columns for per-row width distribution within this column.
408    pub fn sub_columns(mut self, sub_cols: SubColumns) -> Self {
409        self.sub_columns = Some(sub_cols);
410        self
411    }
412}
413
414/// Builder for constructing `Column` instances.
415#[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    /// Set the column name/identifier.
432    pub fn named(mut self, name: impl Into<String>) -> Self {
433        self.name = Some(name.into());
434        self
435    }
436
437    /// Set the width strategy.
438    pub fn width(mut self, width: Width) -> Self {
439        self.width = Some(width);
440        self
441    }
442
443    /// Set a fixed width.
444    pub fn fixed(mut self, width: usize) -> Self {
445        self.width = Some(Width::Fixed(width));
446        self
447    }
448
449    /// Set the column to fill remaining space.
450    pub fn fill(mut self) -> Self {
451        self.width = Some(Width::Fill);
452        self
453    }
454
455    /// Set bounded width with min and max.
456    pub fn bounded(mut self, min: usize, max: usize) -> Self {
457        self.width = Some(Width::bounded(min, max));
458        self
459    }
460
461    /// Set fractional width.
462    pub fn fraction(mut self, n: usize) -> Self {
463        self.width = Some(Width::Fraction(n));
464        self
465    }
466
467    /// Set the text alignment.
468    pub fn align(mut self, align: Align) -> Self {
469        self.align = Some(align);
470        self
471    }
472
473    /// Set alignment to right.
474    pub fn right(self) -> Self {
475        self.align(Align::Right)
476    }
477
478    /// Set alignment to center.
479    pub fn center(self) -> Self {
480        self.align(Align::Center)
481    }
482
483    /// Set the column anchor position.
484    pub fn anchor(mut self, anchor: Anchor) -> Self {
485        self.anchor = Some(anchor);
486        self
487    }
488
489    /// Anchor column to the right edge.
490    pub fn anchor_right(self) -> Self {
491        self.anchor(Anchor::Right)
492    }
493
494    /// Set the overflow behavior.
495    pub fn overflow(mut self, overflow: Overflow) -> Self {
496        self.overflow = Some(overflow);
497        self
498    }
499
500    /// Set overflow to wrap.
501    pub fn wrap(self) -> Self {
502        self.overflow(Overflow::wrap())
503    }
504
505    /// Set overflow to clip.
506    pub fn clip(self) -> Self {
507        self.overflow(Overflow::Clip)
508    }
509
510    /// Set the truncation position (configures Overflow::Truncate).
511    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    /// Set the ellipsis string for truncation.
520    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    /// Set the null/empty value representation.
532    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    /// Set the style name.
538    pub fn style(mut self, style: impl Into<String>) -> Self {
539        self.style = Some(style.into());
540        self
541    }
542
543    /// Use cell value as the style name.
544    pub fn style_from_value(mut self) -> Self {
545        self.style_from_value = true;
546        self
547    }
548
549    /// Set the data key.
550    pub fn key(mut self, key: impl Into<String>) -> Self {
551        self.key = Some(key.into());
552        self
553    }
554
555    /// Set the header title.
556    pub fn header(mut self, header: impl Into<String>) -> Self {
557        self.header = Some(header.into());
558        self
559    }
560
561    /// Set sub-columns for per-row width distribution within this column.
562    pub fn sub_columns(mut self, sub_cols: SubColumns) -> Self {
563        self.sub_columns = Some(sub_cols);
564        self
565    }
566
567    /// Build the `Column` instance.
568    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
586/// Shorthand constructors for creating columns.
587///
588/// Provides a concise API for common column configurations:
589///
590/// ```rust
591/// use standout_render::tabular::Col;
592///
593/// let col = Col::fixed(10);           // Fixed width 10
594/// let col = Col::min(5);              // At least 5, grows to fit
595/// let col = Col::bounded(5, 20);      // Between 5 and 20
596/// let col = Col::fill();              // Fill remaining space
597/// let col = Col::fraction(2);         // 2 parts of remaining space
598///
599/// // Chain with fluent methods
600/// let col = Col::fixed(10).right().style("header");
601/// ```
602pub struct Col;
603
604impl Col {
605    /// Create a fixed-width column.
606    pub fn fixed(width: usize) -> Column {
607        Column::new(Width::Fixed(width))
608    }
609
610    /// Create a column with minimum width that grows to fit content.
611    pub fn min(min: usize) -> Column {
612        Column::new(Width::min(min))
613    }
614
615    /// Create a column with maximum width that shrinks to fit content.
616    pub fn max(max: usize) -> Column {
617        Column::new(Width::max(max))
618    }
619
620    /// Create a bounded-width column (between min and max).
621    pub fn bounded(min: usize, max: usize) -> Column {
622        Column::new(Width::bounded(min, max))
623    }
624
625    /// Create a fill column that expands to remaining space.
626    pub fn fill() -> Column {
627        Column::new(Width::Fill)
628    }
629
630    /// Create a fractional width column.
631    /// `Col::fraction(2)` gets twice the space of `Col::fraction(1)` or `Col::fill()`.
632    pub fn fraction(n: usize) -> Column {
633        Column::new(Width::Fraction(n))
634    }
635}
636
637/// A sub-column within a parent column for per-row width distribution.
638///
639/// Sub-columns partition a parent column's resolved width on a per-row basis.
640/// Within a set of sub-columns, exactly one must use [`Width::Fill`] (the "grower")
641/// which absorbs remaining space after fixed/bounded sub-columns are satisfied.
642///
643/// This enables layouts where a single column contains multiple logical fields
644/// that share space dynamically — for example, a title that grows to fill
645/// available space alongside an optional tag of variable width:
646///
647/// ```text
648/// Gallery Navigation                            [feature]
649/// Bug : Static                                      [bug]
650/// Fixing Layout of Image Nav
651/// ```
652///
653/// Sub-column widths are resolved per-row from actual content, not across all rows.
654/// [`Width::Fraction`] is not supported for sub-columns.
655#[derive(Clone, Debug, Serialize, Deserialize)]
656pub struct SubColumn {
657    /// Optional name/identifier.
658    pub name: Option<String>,
659    /// Width strategy (Fixed, Bounded, or Fill only — no Fraction).
660    pub width: Width,
661    /// Text alignment within this sub-column.
662    pub align: Align,
663    /// How to handle overflow.
664    pub overflow: Overflow,
665    /// Representation for null/empty values.
666    pub null_repr: String,
667    /// Optional style name.
668    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    /// Create a sub-column with the specified width.
686    pub fn new(width: Width) -> Self {
687        SubColumn {
688            width,
689            ..Default::default()
690        }
691    }
692
693    /// Set the sub-column name/identifier.
694    pub fn named(mut self, name: impl Into<String>) -> Self {
695        self.name = Some(name.into());
696        self
697    }
698
699    /// Set the text alignment.
700    pub fn align(mut self, align: Align) -> Self {
701        self.align = align;
702        self
703    }
704
705    /// Set alignment to right.
706    pub fn right(self) -> Self {
707        self.align(Align::Right)
708    }
709
710    /// Set alignment to center.
711    pub fn center(self) -> Self {
712        self.align(Align::Center)
713    }
714
715    /// Set the overflow behavior.
716    pub fn overflow(mut self, overflow: Overflow) -> Self {
717        self.overflow = overflow;
718        self
719    }
720
721    /// Set the null/empty value representation.
722    pub fn null_repr(mut self, null_repr: impl Into<String>) -> Self {
723        self.null_repr = null_repr.into();
724        self
725    }
726
727    /// Set the style name.
728    pub fn style(mut self, style: impl Into<String>) -> Self {
729        self.style = Some(style.into());
730        self
731    }
732}
733
734/// Configuration for sub-columns within a parent column.
735///
736/// Wraps a list of [`SubColumn`] definitions with a separator and validates
737/// the configuration: exactly one sub-column must use [`Width::Fill`], and
738/// [`Width::Fraction`] is not allowed.
739///
740/// # Example
741///
742/// ```rust
743/// use standout_render::tabular::{SubColumns, SubCol};
744///
745/// let sub_cols = SubColumns::new(
746///     vec![SubCol::fill(), SubCol::bounded(0, 30).right()],
747///     " ",
748/// ).unwrap();
749/// ```
750#[derive(Clone, Debug, Serialize, Deserialize)]
751pub struct SubColumns {
752    /// The sub-column definitions.
753    pub columns: Vec<SubColumn>,
754    /// Separator string between sub-columns.
755    pub separator: String,
756}
757
758impl SubColumns {
759    /// Create and validate a sub-columns configuration.
760    ///
761    /// # Errors
762    ///
763    /// Returns an error if:
764    /// - No sub-columns are provided
765    /// - There is not exactly one Fill sub-column
766    /// - Any sub-column uses Fraction width
767    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
799/// Shorthand constructors for creating sub-columns.
800///
801/// ```rust
802/// use standout_render::tabular::SubCol;
803///
804/// let fill = SubCol::fill();                   // Fill remaining space
805/// let fixed = SubCol::fixed(10);               // Fixed width 10
806/// let bounded = SubCol::bounded(0, 30);        // Between 0 and 30
807/// let max = SubCol::max(20);                   // Up to 20
808///
809/// // Chain with fluent methods
810/// let tag = SubCol::bounded(0, 30).right().style("tag");
811/// ```
812pub struct SubCol;
813
814impl SubCol {
815    /// Create a fill sub-column that absorbs remaining space.
816    pub fn fill() -> SubColumn {
817        SubColumn::new(Width::Fill)
818    }
819
820    /// Create a fixed-width sub-column.
821    pub fn fixed(width: usize) -> SubColumn {
822        SubColumn::new(Width::Fixed(width))
823    }
824
825    /// Create a bounded-width sub-column (between min and max).
826    pub fn bounded(min: usize, max: usize) -> SubColumn {
827        SubColumn::new(Width::bounded(min, max))
828    }
829
830    /// Create a sub-column with maximum width.
831    pub fn max(max: usize) -> SubColumn {
832        SubColumn::new(Width::max(max))
833    }
834
835    /// Create a sub-column with minimum width.
836    pub fn min(min: usize) -> SubColumn {
837        SubColumn::new(Width::min(min))
838    }
839}
840
841/// Decorations for table rows (separators, prefixes, suffixes).
842#[derive(Clone, Debug, Default, Serialize, Deserialize)]
843pub struct Decorations {
844    /// Separator between columns (e.g., "  " or " │ ").
845    pub column_sep: String,
846    /// Prefix at the start of each row.
847    pub row_prefix: String,
848    /// Suffix at the end of each row.
849    pub row_suffix: String,
850}
851
852impl Decorations {
853    /// Create decorations with just a column separator.
854    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    /// Set the column separator.
863    pub fn separator(mut self, sep: impl Into<String>) -> Self {
864        self.column_sep = sep.into();
865        self
866    }
867
868    /// Set the row prefix.
869    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
870        self.row_prefix = prefix.into();
871        self
872    }
873
874    /// Set the row suffix.
875    pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
876        self.row_suffix = suffix.into();
877        self
878    }
879
880    /// Calculate the total overhead (prefix + suffix + separators between n columns).
881    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/// Complete specification for a flat data layout (Table or CSV).
892#[derive(Clone, Debug, Serialize, Deserialize)]
893pub struct FlatDataSpec {
894    /// Column specifications.
895    pub columns: Vec<Column>,
896    /// Row decorations (separators, prefix, suffix).
897    pub decorations: Decorations,
898}
899
900impl FlatDataSpec {
901    /// Create a new spec with the given columns and default decorations.
902    pub fn new(columns: Vec<Column>) -> Self {
903        FlatDataSpec {
904            columns,
905            decorations: Decorations::default(),
906        }
907    }
908
909    /// Create a spec builder.
910    pub fn builder() -> FlatDataSpecBuilder {
911        FlatDataSpecBuilder::default()
912    }
913
914    /// Get the number of columns.
915    pub fn num_columns(&self) -> usize {
916        self.columns.len()
917    }
918
919    /// Check if any column uses Fill width.
920    pub fn has_fill_column(&self) -> bool {
921        self.columns.iter().any(|c| matches!(c.width, Width::Fill))
922    }
923
924    /// Extract a header row from the spec.
925    ///
926    /// Uses column `header` if present, otherwise `key`, otherwise empty string.
927    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    /// Extract a data row from a JSON value using the spec.
941    ///
942    /// For each column:
943    /// - If `key` is set, traverses the JSON to find the value.
944    /// - If `key` is unset/missing, uses `null_repr`.
945    /// - Handles nested objects via dot notation (e.g. "author.name").
946    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
960/// Helper to extract a value from nested JSON using dot notation.
961fn 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        // For structured types, just jsonify them effectively
976        v => Some(v.to_string()),
977    }
978}
979
980/// Builder for constructing `FlatDataSpec` instances.
981#[derive(Clone, Debug, Default)]
982pub struct FlatDataSpecBuilder {
983    columns: Vec<Column>,
984    decorations: Decorations,
985}
986
987impl FlatDataSpecBuilder {
988    /// Add a column to the table.
989    pub fn column(mut self, column: Column) -> Self {
990        self.columns.push(column);
991        self
992    }
993
994    /// Add multiple columns from an iterator.
995    pub fn columns(mut self, columns: impl IntoIterator<Item = Column>) -> Self {
996        self.columns.extend(columns);
997        self
998    }
999
1000    /// Set the column separator.
1001    pub fn separator(mut self, sep: impl Into<String>) -> Self {
1002        self.decorations.column_sep = sep.into();
1003        self
1004    }
1005
1006    /// Set the row prefix.
1007    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
1008        self.decorations.row_prefix = prefix.into();
1009        self
1010    }
1011
1012    /// Set the row suffix.
1013    pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
1014        self.decorations.row_suffix = suffix.into();
1015        self
1016    }
1017
1018    /// Set all decorations at once.
1019    pub fn decorations(mut self, decorations: Decorations) -> Self {
1020        self.decorations = decorations;
1021        self
1022    }
1023
1024    /// Build the `FlatDataSpec` instance.
1025    pub fn build(self) -> FlatDataSpec {
1026        FlatDataSpec {
1027            columns: self.columns,
1028            decorations: self.decorations,
1029        }
1030    }
1031}
1032
1033/// Type alias: TabularSpec is the preferred name for FlatDataSpec.
1034pub type TabularSpec = FlatDataSpec;
1035/// Type alias for the builder.
1036pub type TabularSpecBuilder = FlatDataSpecBuilder;
1037
1038#[cfg(test)]
1039mod tests {
1040    use super::*;
1041
1042    // --- Align tests ---
1043
1044    #[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    // --- TruncateAt tests ---
1060
1061    #[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    // --- Width tests ---
1077
1078    #[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        // Now serializes to "fill"
1130        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        // Also test 1fr
1146        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    // --- Overflow tests ---
1156
1157    #[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    // --- Anchor tests ---
1197
1198    #[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    // --- Col shorthand tests ---
1214
1215    #[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    // --- Column tests ---
1276
1277    #[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    // --- Decorations tests ---
1348
1349    #[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        // 3 columns: prefix(2) + suffix(2) + 2 separators(4) = 8
1371        assert_eq!(dec.overhead(3), 8);
1372        // 1 column: prefix(2) + suffix(2) + 0 separators = 4
1373        assert_eq!(dec.overhead(1), 4);
1374        // 0 columns: just prefix + suffix
1375        assert_eq!(dec.overhead(0), 4);
1376    }
1377
1378    // --- FlatDataSpec tests ---
1379
1380    #[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")) // Should use null_repr
1419            .build();
1420
1421        let row = spec.extract_row(&json);
1422        assert_eq!(row[0], "Alice");
1423        assert_eq!(row[1], "30"); // Numbers coerced to string
1424        assert_eq!(row[2], "admin");
1425        assert_eq!(row[3], "-"); // Default null_repr
1426    }
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")) // Fallback to key
1433            .column(Column::new(Width::Fixed(10))) // Empty
1434            .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    // --- SubColumn tests ---
1443
1444    #[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    // --- SubColumns validation tests ---
1513
1514    #[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}