oxur_cli/table/
mod.rs

1//! Styled table rendering for Oxur tools
2//!
3//! Provides a flexible table builder with TOML-based theming for terminal output.
4//!
5//! # Examples
6//!
7//! ```no_run
8//! use oxur_cli::table::{OxurTable, Tabled};
9//!
10//! #[derive(Tabled)]
11//! struct Employee {
12//!     #[tabled(rename = "Name")]
13//!     name: String,
14//!     #[tabled(rename = "Age")]
15//!     age: u32,
16//!     #[tabled(rename = "Role")]
17//!     role: String,
18//! }
19//!
20//! let employees = vec![
21//!     Employee { name: "Alice".into(), age: 30, role: "Engineer".into() },
22//!     Employee { name: "Bob".into(), age: 25, role: "Designer".into() },
23//! ];
24//!
25//! let table = OxurTable::new(employees).render();
26//! println!("{}", table);
27//! ```
28
29use tabled::Table;
30
31pub mod config;
32pub mod helpers;
33mod themes;
34
35pub use config::TableStyleConfig;
36pub use tabled::Tabled; // Re-export for convenience
37
38// Re-exports for advanced usage (manual table building and cell coloring)
39pub use tabled::builder::Builder;
40pub use tabled::settings::object::Cell;
41pub use tabled::settings::Color as TabledColor;
42
43/// A themed table builder for terminal output
44///
45/// Creates tables with the default Oxur theme (warm orange sunset colors).
46/// Supports any data type that implements `Tabled`.
47pub struct OxurTable<T: Tabled> {
48    data: Vec<T>,
49    theme: TableStyleConfig,
50}
51
52impl<T: Tabled> OxurTable<T> {
53    /// Create a new table with data, using the default Oxur theme
54    ///
55    /// # Examples
56    ///
57    /// ```no_run
58    /// use oxur_cli::table::{OxurTable, Tabled};
59    ///
60    /// #[derive(Tabled)]
61    /// struct Row {
62    ///     #[tabled(rename = "ID")]
63    ///     id: u32,
64    ///     #[tabled(rename = "Name")]
65    ///     name: String,
66    /// }
67    ///
68    /// let data = vec![
69    ///     Row { id: 1, name: "Alice".into() },
70    ///     Row { id: 2, name: "Bob".into() },
71    /// ];
72    ///
73    /// let table = OxurTable::new(data);
74    /// ```
75    pub fn new(data: Vec<T>) -> Self {
76        Self { data, theme: TableStyleConfig::default() }
77    }
78
79    /// Render the table as a styled string for terminal output
80    ///
81    /// # Examples
82    ///
83    /// ```no_run
84    /// use oxur_cli::table::{OxurTable, Tabled};
85    ///
86    /// #[derive(Tabled)]
87    /// struct Row {
88    ///     name: String,
89    /// }
90    ///
91    /// let data = vec![Row { name: "Test".into() }];
92    /// let output = OxurTable::new(data).render();
93    /// println!("{}", output);
94    /// ```
95    pub fn render(self) -> String {
96        let mut table = Table::new(&self.data);
97        self.theme.apply_to_table::<T>(&mut table);
98        table.to_string()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[derive(Tabled)]
107    struct TestRow {
108        #[tabled(rename = "ID")]
109        id: u32,
110        #[tabled(rename = "Name")]
111        name: String,
112        #[tabled(rename = "Status")]
113        status: String,
114    }
115
116    // ===== OxurTable::new tests =====
117
118    #[test]
119    fn test_new_creates_table_with_default_theme() {
120        let data = vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }];
121
122        let table = OxurTable::new(data);
123
124        // Verify the table was created (we can't easily inspect internals)
125        // Just ensure it doesn't panic
126        assert_eq!(table.data.len(), 1);
127    }
128
129    #[test]
130    fn test_new_with_empty_data() {
131        let data: Vec<TestRow> = vec![];
132        let table = OxurTable::new(data);
133        assert_eq!(table.data.len(), 0);
134    }
135
136    #[test]
137    fn test_new_with_multiple_rows() {
138        let data = vec![
139            TestRow { id: 1, name: "Alice".into(), status: "Active".into() },
140            TestRow { id: 2, name: "Bob".into(), status: "Inactive".into() },
141            TestRow { id: 3, name: "Charlie".into(), status: "Active".into() },
142        ];
143
144        let table = OxurTable::new(data);
145        assert_eq!(table.data.len(), 3);
146    }
147
148    // ===== OxurTable::render tests =====
149
150    #[test]
151    fn test_render_produces_output() {
152        let data = vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }];
153
154        let table = OxurTable::new(data);
155        let output = table.render();
156
157        // Verify output is not empty and contains data
158        assert!(!output.is_empty());
159        assert!(output.contains("Alice"));
160        assert!(output.contains("Active"));
161    }
162
163    #[test]
164    fn test_render_empty_data() {
165        let data: Vec<TestRow> = vec![];
166        let table = OxurTable::new(data);
167        let output = table.render();
168
169        // Should still produce some output (at least headers)
170        assert!(!output.is_empty());
171    }
172
173    #[test]
174    fn test_render_multiple_rows() {
175        let data = vec![
176            TestRow { id: 1, name: "Alice".into(), status: "Active".into() },
177            TestRow { id: 2, name: "Bob".into(), status: "Inactive".into() },
178        ];
179
180        let table = OxurTable::new(data);
181        let output = table.render();
182
183        assert!(output.contains("Alice"));
184        assert!(output.contains("Bob"));
185        assert!(output.contains("Active"));
186        assert!(output.contains("Inactive"));
187    }
188
189    #[test]
190    fn test_render_includes_headers() {
191        let data = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
192
193        let table = OxurTable::new(data);
194        let output = table.render();
195
196        // Headers from #[tabled(rename = "...")] should be present
197        assert!(output.contains("ID"));
198        assert!(output.contains("Name"));
199        assert!(output.contains("Status"));
200    }
201
202    #[test]
203    fn test_render_contains_ansi_codes() {
204        let data = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
205
206        let table = OxurTable::new(data);
207        let output = table.render();
208
209        // Should contain ANSI escape codes for colors
210        // (The theme applies colors, so there should be escape sequences)
211        assert!(output.contains("\x1b[") || output.contains("\u{001b}["));
212    }
213
214    // ===== Integration tests =====
215
216    #[test]
217    fn test_table_with_special_characters() {
218        let data = vec![TestRow { id: 1, name: "Test & \"Special\"".into(), status: "OK".into() }];
219
220        let table = OxurTable::new(data);
221        let output = table.render();
222
223        assert!(output.contains("Test & \"Special\""));
224    }
225
226    #[test]
227    fn test_table_with_unicode() {
228        let data = vec![TestRow { id: 1, name: "Ñoño 日本語".into(), status: "✓".into() }];
229
230        let table = OxurTable::new(data);
231        let output = table.render();
232
233        assert!(output.contains("Ñoño"));
234        assert!(output.contains("日本語"));
235        assert!(output.contains("✓"));
236    }
237
238    #[test]
239    fn test_table_with_long_text() {
240        let long_name = "A".repeat(100);
241        let data = vec![TestRow { id: 1, name: long_name.clone(), status: "OK".into() }];
242
243        let table = OxurTable::new(data);
244        let output = table.render();
245
246        // Long text should be in the output (might be wrapped or truncated by tabled)
247        assert!(output.contains(&long_name[..50])); // At least first 50 chars
248    }
249
250    #[test]
251    fn test_table_with_empty_strings() {
252        let data = vec![TestRow { id: 1, name: "".into(), status: "".into() }];
253
254        let table = OxurTable::new(data);
255        let output = table.render();
256
257        // Should handle empty strings gracefully
258        assert!(!output.is_empty());
259        assert!(output.contains("ID")); // Headers should still be present
260    }
261
262    #[test]
263    fn test_different_struct_type() {
264        #[derive(Tabled)]
265        struct DifferentRow {
266            #[tabled(rename = "Col1")]
267            col1: String,
268            #[tabled(rename = "Col2")]
269            col2: i32,
270        }
271
272        let data = vec![DifferentRow { col1: "Test".into(), col2: 42 }];
273
274        let table = OxurTable::new(data);
275        let output = table.render();
276
277        assert!(output.contains("Test"));
278        assert!(output.contains("42"));
279        assert!(output.contains("Col1"));
280        assert!(output.contains("Col2"));
281    }
282}