1use serde::Serialize;
7
8#[derive(Debug, Clone)]
10pub struct Table {
11 headers: Vec<String>,
12 rows: Vec<Vec<String>>,
13 column_widths: Vec<usize>,
14}
15
16impl Table {
17 pub fn headers(&self) -> &[String] {
19 &self.headers
20 }
21
22 pub fn rows(&self) -> &[Vec<String>] {
24 &self.rows
25 }
26
27 pub fn render(&self) -> String {
29 if self.headers.is_empty() {
30 return String::new();
31 }
32
33 let mut output = String::new();
34
35 output.push_str(&self.render_border('┌', '┬', '┐'));
37
38 output.push_str(&self.render_row(&self.headers));
40
41 output.push_str(&self.render_border('├', '┼', '┤'));
43
44 for row in &self.rows {
46 output.push_str(&self.render_row(row));
47 }
48
49 output.push_str(&self.render_border('└', '┴', '┘'));
51
52 output
53 }
54
55 fn render_border(&self, left: char, mid: char, right: char) -> String {
56 let mut line = String::new();
57 line.push(left);
58 for (i, width) in self.column_widths.iter().enumerate() {
59 line.push_str(&"─".repeat(*width + 2));
60 if i < self.column_widths.len() - 1 {
61 line.push(mid);
62 }
63 }
64 line.push(right);
65 line.push('\n');
66 line
67 }
68
69 fn render_row(&self, values: &[String]) -> String {
70 let mut line = String::new();
71 line.push('│');
72 for (i, (value, width)) in values.iter().zip(&self.column_widths).enumerate() {
73 line.push(' ');
74 line.push_str(&format!("{:width$}", value, width = *width));
75 line.push(' ');
76 if i < self.column_widths.len() - 1 {
77 line.push('│');
78 }
79 }
80 line.push('│');
81 line.push('\n');
82 line
83 }
84
85 pub fn to_json(&self) -> String {
87 let records: Vec<_> = self
88 .rows
89 .iter()
90 .map(|row| {
91 self.headers
92 .iter()
93 .zip(row)
94 .map(|(h, v)| (h.clone(), v.clone()))
95 .collect::<std::collections::HashMap<_, _>>()
96 })
97 .collect();
98
99 serde_json::to_string_pretty(&records).unwrap_or_default()
100 }
101}
102
103#[derive(Debug, Default)]
105pub struct TableBuilder {
106 headers: Vec<String>,
107 rows: Vec<Vec<String>>,
108}
109
110impl TableBuilder {
111 pub fn new() -> Self {
113 Self::default()
114 }
115
116 pub fn headers(mut self, headers: Vec<impl Into<String>>) -> Self {
118 self.headers = headers.into_iter().map(Into::into).collect();
119 self
120 }
121
122 pub fn row(mut self, row: Vec<impl Into<String>>) -> Self {
124 self.rows.push(row.into_iter().map(Into::into).collect());
125 self
126 }
127
128 pub fn rows(mut self, rows: Vec<Vec<impl Into<String> + Clone>>) -> Self {
130 for row in rows {
131 self.rows.push(row.into_iter().map(Into::into).collect());
132 }
133 self
134 }
135
136 pub fn build(self) -> Table {
138 let mut column_widths: Vec<usize> = self.headers.iter().map(String::len).collect();
139
140 for row in &self.rows {
141 for (i, cell) in row.iter().enumerate() {
142 if i < column_widths.len() {
143 column_widths[i] = column_widths[i].max(cell.len());
144 }
145 }
146 }
147
148 Table {
149 headers: self.headers,
150 rows: self.rows,
151 column_widths,
152 }
153 }
154}
155
156pub fn format_bytes(bytes: u64) -> String {
158 const KB: u64 = 1024;
159 const MB: u64 = KB * 1024;
160 const GB: u64 = MB * 1024;
161 const TB: u64 = GB * 1024;
162
163 if bytes >= TB {
164 format!("{:.1} TB", bytes as f64 / TB as f64)
165 } else if bytes >= GB {
166 format!("{:.1} GB", bytes as f64 / GB as f64)
167 } else if bytes >= MB {
168 format!("{:.1} MB", bytes as f64 / MB as f64)
169 } else if bytes >= KB {
170 format!("{:.1} KB", bytes as f64 / KB as f64)
171 } else {
172 format!("{bytes} B")
173 }
174}
175
176pub fn format_number(n: u64) -> String {
178 let s = n.to_string();
179 let mut result = String::new();
180 for (i, c) in s.chars().rev().enumerate() {
181 if i > 0 && i % 3 == 0 {
182 result.push(',');
183 }
184 result.push(c);
185 }
186 result.chars().rev().collect()
187}
188
189pub fn format_duration(seconds: f64) -> String {
191 if seconds < 60.0 {
192 format!("{seconds:.1}s")
193 } else if seconds < 3600.0 {
194 let mins = (seconds / 60.0).floor();
195 let secs = seconds % 60.0;
196 format!("{mins}m {secs:.0}s")
197 } else {
198 let hours = (seconds / 3600.0).floor();
199 let mins = ((seconds % 3600.0) / 60.0).floor();
200 format!("{hours}h {mins}m")
201 }
202}
203
204#[derive(Debug, Clone, Serialize)]
206#[allow(clippy::type_complexity)]
207pub struct StructuredOutput<T: Serialize> {
208 pub data: T,
209 #[serde(skip)]
210 pub table_headers: Vec<String>,
211 #[serde(skip)]
212 pub row_fn: Option<fn(&T) -> Vec<Vec<String>>>,
213}
214
215impl<T: Serialize> StructuredOutput<T> {
216 pub fn new(data: T) -> Self {
218 Self {
219 data,
220 table_headers: vec![],
221 row_fn: None,
222 }
223 }
224
225 pub fn to_json(&self) -> String {
227 serde_json::to_string_pretty(&self.data).unwrap_or_default()
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_table_builder() {
237 let table = TableBuilder::new()
238 .headers(vec!["Name", "Value", "Type"])
239 .row(vec!["alpha", "0.7", "float"])
240 .row(vec!["temperature", "4.0", "float"])
241 .build();
242
243 assert_eq!(table.headers().len(), 3);
244 assert_eq!(table.rows().len(), 2);
245 }
246
247 #[test]
248 fn test_table_render() {
249 let table = TableBuilder::new()
250 .headers(vec!["A", "B"])
251 .row(vec!["1", "2"])
252 .build();
253
254 let rendered = table.render();
255 assert!(rendered.contains('┌'));
256 assert!(rendered.contains('│'));
257 assert!(rendered.contains('└'));
258 assert!(rendered.contains('A'));
259 assert!(rendered.contains('1'));
260 }
261
262 #[test]
263 fn test_table_to_json() {
264 let table = TableBuilder::new()
265 .headers(vec!["name", "value"])
266 .row(vec!["test", "123"])
267 .build();
268
269 let json = table.to_json();
270 assert!(json.contains("\"name\""));
271 assert!(json.contains("\"test\""));
272 assert!(json.contains("\"value\""));
273 assert!(json.contains("\"123\""));
274 }
275
276 #[test]
277 fn test_format_bytes() {
278 assert_eq!(format_bytes(500), "500 B");
279 assert_eq!(format_bytes(1500), "1.5 KB");
280 assert_eq!(format_bytes(1_500_000), "1.4 MB");
281 assert_eq!(format_bytes(1_500_000_000), "1.4 GB");
282 assert_eq!(format_bytes(1_500_000_000_000), "1.4 TB");
283 }
284
285 #[test]
286 fn test_format_number() {
287 assert_eq!(format_number(0), "0");
288 assert_eq!(format_number(999), "999");
289 assert_eq!(format_number(1000), "1,000");
290 assert_eq!(format_number(1_000_000), "1,000,000");
291 assert_eq!(format_number(7_000_000_000), "7,000,000,000");
292 }
293
294 #[test]
295 fn test_format_duration() {
296 assert_eq!(format_duration(30.0), "30.0s");
297 assert_eq!(format_duration(90.0), "1m 30s");
298 assert_eq!(format_duration(3700.0), "1h 1m");
299 }
300
301 #[test]
302 fn test_column_widths_adapt_to_content() {
303 let table = TableBuilder::new()
304 .headers(vec!["Short", "X"])
305 .row(vec!["A very long value here", "Y"])
306 .build();
307
308 let rendered = table.render();
309 assert!(rendered.contains("A very long value here"));
311 }
312
313 #[test]
314 fn test_empty_table_renders_empty() {
315 let table = TableBuilder::new().build();
316 let rendered = table.render();
317 assert!(rendered.is_empty());
318 }
319
320 #[test]
321 fn test_table_with_no_rows() {
322 let table = TableBuilder::new().headers(vec!["A", "B"]).build();
323
324 let rendered = table.render();
325 assert!(rendered.contains('A'));
326 assert!(rendered.contains('B'));
327 assert!(rendered.contains('┌'));
329 assert!(rendered.contains('└'));
330 }
331
332 #[test]
333 fn test_table_builder_rows_method() {
334 let table = TableBuilder::new()
335 .headers(vec!["X", "Y"])
336 .rows(vec![vec!["1", "2"], vec!["3", "4"]])
337 .build();
338
339 assert_eq!(table.rows().len(), 2);
340 assert_eq!(table.rows()[0], vec!["1", "2"]);
341 assert_eq!(table.rows()[1], vec!["3", "4"]);
342 }
343
344 #[test]
345 fn test_format_bytes_edge_cases() {
346 assert_eq!(format_bytes(0), "0 B");
347 assert_eq!(format_bytes(1), "1 B");
348 assert_eq!(format_bytes(1023), "1023 B");
349 assert_eq!(format_bytes(1024), "1.0 KB");
350 }
351
352 #[test]
353 fn test_format_number_edge_cases() {
354 assert_eq!(format_number(1), "1");
355 assert_eq!(format_number(12), "12");
356 assert_eq!(format_number(123), "123");
357 assert_eq!(format_number(1234), "1,234");
358 assert_eq!(format_number(12345), "12,345");
359 assert_eq!(format_number(123456), "123,456");
360 }
361
362 #[test]
363 fn test_format_duration_edge_cases() {
364 assert_eq!(format_duration(0.0), "0.0s");
365 assert_eq!(format_duration(59.9), "59.9s");
366 assert_eq!(format_duration(60.0), "1m 0s");
367 assert_eq!(format_duration(3599.0), "59m 59s");
368 assert_eq!(format_duration(3600.0), "1h 0m");
369 }
370
371 #[test]
372 fn test_structured_output_to_json() {
373 #[derive(serde::Serialize)]
374 struct TestData {
375 name: String,
376 value: i32,
377 }
378
379 let output = StructuredOutput::new(TestData {
380 name: "test".into(),
381 value: 42,
382 });
383
384 let json = output.to_json();
385 assert!(json.contains("\"name\""));
386 assert!(json.contains("\"test\""));
387 assert!(json.contains("42"));
388 }
389
390 #[test]
391 fn test_table_json_empty_rows() {
392 let table = TableBuilder::new().headers(vec!["a", "b"]).build();
393
394 let json = table.to_json();
395 assert_eq!(json, "[]");
396 }
397
398 #[test]
399 fn test_table_json_multiple_rows() {
400 let table = TableBuilder::new()
401 .headers(vec!["key", "val"])
402 .row(vec!["x", "1"])
403 .row(vec!["y", "2"])
404 .build();
405
406 let json = table.to_json();
407 assert!(json.contains("\"key\""));
408 assert!(json.contains("\"val\""));
409 assert!(json.contains("\"x\""));
410 assert!(json.contains("\"y\""));
411 }
412}