alopex_cli/output/
table.rs1use 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
14pub struct TableFormatter {
19 table: Table,
21 header_set: bool,
23}
24
25impl TableFormatter {
26 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 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 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 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
75fn 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 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 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}