1use tabled::Table;
57
58pub mod config;
59pub mod helpers;
60mod themes;
61
62pub use config::TableStyleConfig;
63pub use tabled::Tabled; pub use tabled::builder::Builder;
67pub use tabled::settings::object::Cell;
68pub use tabled::settings::Color as TabledColor;
69
70pub struct OxurTable<T: Tabled> {
79 data: Vec<T>,
80 theme: TableStyleConfig,
81 title: Option<String>,
82 has_footer: bool,
83}
84
85impl<T: Tabled> OxurTable<T> {
86 pub fn new(data: Vec<T>) -> Self {
109 Self { data, theme: TableStyleConfig::default(), title: None, has_footer: false }
110 }
111
112 pub fn with_title(mut self, title: impl Into<String>) -> Self {
130 self.title = Some(title.into());
131 self
132 }
133
134 pub fn with_footer(mut self) -> Self {
152 self.has_footer = true;
153 self
154 }
155
156 pub fn render(self) -> String {
173 if self.title.is_none() && !self.has_footer {
175 let mut table = Table::new(&self.data);
176 self.theme.apply_to_table::<T>(&mut table);
177 return table.to_string();
178 }
179
180 let col_count = T::headers().len();
182 let mut builder = Builder::default();
183
184 if let Some(ref title) = self.title {
186 let mut title_row = vec![title.clone()];
187 title_row.extend(std::iter::repeat_n(String::new(), col_count.saturating_sub(1)));
188 builder.push_record(title_row);
189 }
190
191 let headers: Vec<String> = T::headers().iter().map(|h| h.to_string()).collect();
193 builder.push_record(headers);
194
195 for item in &self.data {
197 let fields: Vec<String> = T::fields(item).iter().map(|f| f.to_string()).collect();
198 builder.push_record(fields);
199 }
200
201 if self.has_footer {
203 let footer_row: Vec<String> = std::iter::repeat_n(String::new(), col_count).collect();
204 builder.push_record(footer_row);
205 }
206
207 let mut table = builder.build();
208 self.theme.apply_to_table::<T>(&mut table);
209 table.to_string()
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[derive(Tabled)]
218 struct TestRow {
219 #[tabled(rename = "ID")]
220 id: u32,
221 #[tabled(rename = "Name")]
222 name: String,
223 #[tabled(rename = "Status")]
224 status: String,
225 }
226
227 #[test]
230 fn test_new_creates_table_with_default_theme() {
231 let data = vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }];
232
233 let table = OxurTable::new(data);
234
235 assert_eq!(table.data.len(), 1);
238 }
239
240 #[test]
241 fn test_new_with_empty_data() {
242 let data: Vec<TestRow> = vec![];
243 let table = OxurTable::new(data);
244 assert_eq!(table.data.len(), 0);
245 }
246
247 #[test]
248 fn test_new_with_multiple_rows() {
249 let data = vec![
250 TestRow { id: 1, name: "Alice".into(), status: "Active".into() },
251 TestRow { id: 2, name: "Bob".into(), status: "Inactive".into() },
252 TestRow { id: 3, name: "Charlie".into(), status: "Active".into() },
253 ];
254
255 let table = OxurTable::new(data);
256 assert_eq!(table.data.len(), 3);
257 }
258
259 #[test]
262 fn test_render_produces_output() {
263 let data = vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }];
264
265 let table = OxurTable::new(data);
266 let output = table.render();
267
268 assert!(!output.is_empty());
270 assert!(output.contains("Alice"));
271 assert!(output.contains("Active"));
272 }
273
274 #[test]
275 fn test_render_empty_data() {
276 let data: Vec<TestRow> = vec![];
277 let table = OxurTable::new(data);
278 let output = table.render();
279
280 assert!(!output.is_empty());
282 }
283
284 #[test]
285 fn test_render_multiple_rows() {
286 let data = vec![
287 TestRow { id: 1, name: "Alice".into(), status: "Active".into() },
288 TestRow { id: 2, name: "Bob".into(), status: "Inactive".into() },
289 ];
290
291 let table = OxurTable::new(data);
292 let output = table.render();
293
294 assert!(output.contains("Alice"));
295 assert!(output.contains("Bob"));
296 assert!(output.contains("Active"));
297 assert!(output.contains("Inactive"));
298 }
299
300 #[test]
301 fn test_render_includes_headers() {
302 let data = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
303
304 let table = OxurTable::new(data);
305 let output = table.render();
306
307 assert!(output.contains("ID"));
309 assert!(output.contains("Name"));
310 assert!(output.contains("Status"));
311 }
312
313 #[test]
314 fn test_render_contains_ansi_codes() {
315 let data = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
316
317 let table = OxurTable::new(data);
318 let output = table.render();
319
320 assert!(output.contains("\x1b[") || output.contains("\u{001b}["));
323 }
324
325 #[test]
328 fn test_table_with_special_characters() {
329 let data = vec![TestRow { id: 1, name: "Test & \"Special\"".into(), status: "OK".into() }];
330
331 let table = OxurTable::new(data);
332 let output = table.render();
333
334 assert!(output.contains("Test & \"Special\""));
335 }
336
337 #[test]
338 fn test_table_with_unicode() {
339 let data = vec![TestRow { id: 1, name: "Ñoño 日本語".into(), status: "✓".into() }];
340
341 let table = OxurTable::new(data);
342 let output = table.render();
343
344 assert!(output.contains("Ñoño"));
345 assert!(output.contains("日本語"));
346 assert!(output.contains("✓"));
347 }
348
349 #[test]
350 fn test_table_with_long_text() {
351 let long_name = "A".repeat(100);
352 let data = vec![TestRow { id: 1, name: long_name.clone(), status: "OK".into() }];
353
354 let table = OxurTable::new(data);
355 let output = table.render();
356
357 assert!(output.contains(&long_name[..50])); }
360
361 #[test]
362 fn test_table_with_empty_strings() {
363 let data = vec![TestRow { id: 1, name: "".into(), status: "".into() }];
364
365 let table = OxurTable::new(data);
366 let output = table.render();
367
368 assert!(!output.is_empty());
370 assert!(output.contains("ID")); }
372
373 #[test]
374 fn test_different_struct_type() {
375 #[derive(Tabled)]
376 struct DifferentRow {
377 #[tabled(rename = "Col1")]
378 col1: String,
379 #[tabled(rename = "Col2")]
380 col2: i32,
381 }
382
383 let data = vec![DifferentRow { col1: "Test".into(), col2: 42 }];
384
385 let table = OxurTable::new(data);
386 let output = table.render();
387
388 assert!(output.contains("Test"));
389 assert!(output.contains("42"));
390 assert!(output.contains("Col1"));
391 assert!(output.contains("Col2"));
392 }
393
394 #[test]
397 fn test_with_title_adds_title_row() {
398 let data = vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }];
399
400 let table = OxurTable::new(data).with_title("MY TABLE");
401 let output = table.render();
402
403 assert!(output.contains("MY TABLE"));
405 assert!(output.contains("Alice"));
407 }
408
409 #[test]
410 fn test_with_title_string_slice() {
411 let data = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
412
413 let table = OxurTable::new(data).with_title("TITLE FROM &str");
414 let output = table.render();
415
416 assert!(output.contains("TITLE FROM &str"));
417 }
418
419 #[test]
420 fn test_with_title_owned_string() {
421 let data = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
422 let title = String::from("OWNED TITLE");
423
424 let table = OxurTable::new(data).with_title(title);
425 let output = table.render();
426
427 assert!(output.contains("OWNED TITLE"));
428 }
429
430 #[test]
433 fn test_with_footer_adds_footer_row() {
434 let data = vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }];
435
436 let table = OxurTable::new(data).with_footer();
437 let output = table.render();
438
439 assert!(output.contains("Alice"));
441 let without_footer =
443 OxurTable::new(vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }])
444 .render();
445 assert!(output.len() > without_footer.len());
446 }
447
448 #[test]
451 fn test_with_title_and_footer() {
452 let data = vec![
453 TestRow { id: 1, name: "Alice".into(), status: "Active".into() },
454 TestRow { id: 2, name: "Bob".into(), status: "Inactive".into() },
455 ];
456
457 let table = OxurTable::new(data).with_title("USER LIST").with_footer();
458 let output = table.render();
459
460 assert!(output.contains("USER LIST"));
462 assert!(output.contains("Alice"));
464 assert!(output.contains("Bob"));
465 assert!(output.contains("ID"));
467 assert!(output.contains("Name"));
468 assert!(output.contains("Status"));
469 }
470
471 #[test]
472 fn test_chaining_order_does_not_matter() {
473 let data1 = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
474 let data2 = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
475
476 let output1 = OxurTable::new(data1).with_title("TITLE").with_footer().render();
477 let output2 = OxurTable::new(data2).with_footer().with_title("TITLE").render();
478
479 assert_eq!(output1, output2);
481 }
482
483 #[test]
484 fn test_empty_data_with_title_and_footer() {
485 let data: Vec<TestRow> = vec![];
486
487 let table = OxurTable::new(data).with_title("EMPTY TABLE").with_footer();
488 let output = table.render();
489
490 assert!(output.contains("EMPTY TABLE"));
492 assert!(output.contains("ID"));
494 }
495}