Skip to main content

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