alopex_cli/output/
table.rs

1//! Table formatter using comfy-table
2//!
3//! Human-readable table output with auto-adjusting column widths.
4
5use std::io::Write;
6
7use comfy_table::{presets::UTF8_FULL, ContentArrangement, Table};
8
9use crate::error::Result;
10use crate::models::{Column, Row, Value};
11
12use super::formatter::Formatter;
13
14/// Table formatter using comfy-table.
15///
16/// Outputs data as a human-readable table with auto-adjusting column widths.
17/// Does not support streaming (requires buffering to determine column widths).
18pub struct TableFormatter {
19    /// The table being built
20    table: Table,
21    /// Whether the header has been set
22    header_set: bool,
23}
24
25impl TableFormatter {
26    /// Create a new table formatter.
27    pub fn new() -> Self {
28        let mut table = Table::new();
29        table
30            .load_preset(UTF8_FULL)
31            .set_content_arrangement(ContentArrangement::Dynamic);
32
33        Self {
34            table,
35            header_set: false,
36        }
37    }
38}
39
40impl Default for TableFormatter {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl Formatter for TableFormatter {
47    fn write_header(&mut self, _writer: &mut dyn Write, columns: &[Column]) -> Result<()> {
48        // Set the header row
49        let headers: Vec<&str> = columns.iter().map(|c| c.name.as_str()).collect();
50        self.table.set_header(headers);
51        self.header_set = true;
52        Ok(())
53    }
54
55    fn write_row(&mut self, _writer: &mut dyn Write, row: &Row) -> Result<()> {
56        // Add a row to the table
57        let cells: Vec<String> = row.columns.iter().map(format_value_table).collect();
58        self.table.add_row(cells);
59        Ok(())
60    }
61
62    fn write_footer(&mut self, writer: &mut dyn Write) -> Result<()> {
63        // Write the complete table to the output
64        if self.header_set {
65            writeln!(writer, "{}", self.table)?;
66        }
67        Ok(())
68    }
69
70    fn supports_streaming(&self) -> bool {
71        false
72    }
73}
74
75/// Format a value for table output.
76fn format_value_table(value: &Value) -> String {
77    match value {
78        Value::Null => "NULL".to_string(),
79        Value::Bool(b) => b.to_string(),
80        Value::Int(i) => i.to_string(),
81        Value::Float(f) => format!("{:.6}", f),
82        Value::Text(s) => s.clone(),
83        Value::Bytes(b) => {
84            // Format bytes as hex string (truncated if too long)
85            let hex: String = b
86                .iter()
87                .take(32)
88                .map(|byte| format!("{:02x}", byte))
89                .collect();
90            if b.len() > 32 {
91                format!("{}...", hex)
92            } else {
93                hex
94            }
95        }
96        Value::Vector(v) => {
97            // Format vector (truncated if too long)
98            if v.len() <= 4 {
99                format!(
100                    "[{}]",
101                    v.iter()
102                        .map(|x| format!("{:.4}", x))
103                        .collect::<Vec<_>>()
104                        .join(", ")
105                )
106            } else {
107                format!(
108                    "[{}, ... ({} dims)]",
109                    v.iter()
110                        .take(3)
111                        .map(|x| format!("{:.4}", x))
112                        .collect::<Vec<_>>()
113                        .join(", "),
114                    v.len()
115                )
116            }
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::models::DataType;
125
126    fn test_columns() -> Vec<Column> {
127        vec![
128            Column::new("id", DataType::Int),
129            Column::new("name", DataType::Text),
130        ]
131    }
132
133    #[test]
134    fn test_table_basic() {
135        let mut formatter = TableFormatter::new();
136        let mut output = Vec::new();
137
138        let columns = test_columns();
139        formatter.write_header(&mut output, &columns).unwrap();
140
141        let row = Row::new(vec![Value::Int(1), Value::Text("Alice".to_string())]);
142        formatter.write_row(&mut output, &row).unwrap();
143
144        formatter.write_footer(&mut output).unwrap();
145
146        let result = String::from_utf8(output).unwrap();
147        assert!(result.contains("id"));
148        assert!(result.contains("name"));
149        assert!(result.contains("1"));
150        assert!(result.contains("Alice"));
151    }
152
153    #[test]
154    fn test_table_multiple_rows() {
155        let mut formatter = TableFormatter::new();
156        let mut output = Vec::new();
157
158        let columns = test_columns();
159        formatter.write_header(&mut output, &columns).unwrap();
160
161        let row1 = Row::new(vec![Value::Int(1), Value::Text("Alice".to_string())]);
162        let row2 = Row::new(vec![Value::Int(2), Value::Text("Bob".to_string())]);
163
164        formatter.write_row(&mut output, &row1).unwrap();
165        formatter.write_row(&mut output, &row2).unwrap();
166
167        formatter.write_footer(&mut output).unwrap();
168
169        let result = String::from_utf8(output).unwrap();
170        assert!(result.contains("Alice"));
171        assert!(result.contains("Bob"));
172    }
173
174    #[test]
175    fn test_table_null_value() {
176        let mut formatter = TableFormatter::new();
177        let mut output = Vec::new();
178
179        let columns = test_columns();
180        formatter.write_header(&mut output, &columns).unwrap();
181
182        let row = Row::new(vec![Value::Int(1), Value::Null]);
183        formatter.write_row(&mut output, &row).unwrap();
184
185        formatter.write_footer(&mut output).unwrap();
186
187        let result = String::from_utf8(output).unwrap();
188        assert!(result.contains("NULL"));
189    }
190
191    #[test]
192    fn test_table_does_not_support_streaming() {
193        let formatter = TableFormatter::new();
194        assert!(!formatter.supports_streaming());
195    }
196
197    #[test]
198    fn test_format_value_vector_short() {
199        let v = Value::Vector(vec![1.0, 2.0, 3.0]);
200        let result = format_value_table(&v);
201        assert!(result.contains("["));
202        assert!(result.contains("]"));
203    }
204
205    #[test]
206    fn test_format_value_vector_long() {
207        let v = Value::Vector(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
208        let result = format_value_table(&v);
209        assert!(result.contains("..."));
210        assert!(result.contains("6 dims"));
211    }
212}