use tabled::Table;
pub mod config;
pub mod helpers;
mod themes;
pub use config::TableStyleConfig;
pub use tabled::Tabled;
pub use tabled::builder::Builder;
pub use tabled::settings::object::Cell;
pub use tabled::settings::Color as TabledColor;
pub struct OxurTable<T: Tabled> {
data: Vec<T>,
theme: TableStyleConfig,
title: Option<String>,
has_footer: bool,
}
impl<T: Tabled> OxurTable<T> {
pub fn new(data: Vec<T>) -> Self {
Self { data, theme: TableStyleConfig::default(), title: None, has_footer: false }
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_footer(mut self) -> Self {
self.has_footer = true;
self
}
pub fn render(self) -> String {
if self.title.is_none() && !self.has_footer {
let mut table = Table::new(&self.data);
self.theme.apply_to_table::<T>(&mut table);
return table.to_string();
}
let col_count = T::headers().len();
let mut builder = Builder::default();
if let Some(ref title) = self.title {
let mut title_row = vec![title.clone()];
title_row.extend(std::iter::repeat_n(String::new(), col_count.saturating_sub(1)));
builder.push_record(title_row);
}
let headers: Vec<String> = T::headers().iter().map(|h| h.to_string()).collect();
builder.push_record(headers);
for item in &self.data {
let fields: Vec<String> = T::fields(item).iter().map(|f| f.to_string()).collect();
builder.push_record(fields);
}
if self.has_footer {
let footer_row: Vec<String> = std::iter::repeat_n(String::new(), col_count).collect();
builder.push_record(footer_row);
}
let mut table = builder.build();
self.theme.apply_to_table::<T>(&mut table);
table.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Tabled)]
struct TestRow {
#[tabled(rename = "ID")]
id: u32,
#[tabled(rename = "Name")]
name: String,
#[tabled(rename = "Status")]
status: String,
}
#[test]
fn test_new_creates_table_with_default_theme() {
let data = vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }];
let table = OxurTable::new(data);
assert_eq!(table.data.len(), 1);
}
#[test]
fn test_new_with_empty_data() {
let data: Vec<TestRow> = vec![];
let table = OxurTable::new(data);
assert_eq!(table.data.len(), 0);
}
#[test]
fn test_new_with_multiple_rows() {
let data = vec![
TestRow { id: 1, name: "Alice".into(), status: "Active".into() },
TestRow { id: 2, name: "Bob".into(), status: "Inactive".into() },
TestRow { id: 3, name: "Charlie".into(), status: "Active".into() },
];
let table = OxurTable::new(data);
assert_eq!(table.data.len(), 3);
}
#[test]
fn test_render_produces_output() {
let data = vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }];
let table = OxurTable::new(data);
let output = table.render();
assert!(!output.is_empty());
assert!(output.contains("Alice"));
assert!(output.contains("Active"));
}
#[test]
fn test_render_empty_data() {
let data: Vec<TestRow> = vec![];
let table = OxurTable::new(data);
let output = table.render();
assert!(!output.is_empty());
}
#[test]
fn test_render_multiple_rows() {
let data = vec![
TestRow { id: 1, name: "Alice".into(), status: "Active".into() },
TestRow { id: 2, name: "Bob".into(), status: "Inactive".into() },
];
let table = OxurTable::new(data);
let output = table.render();
assert!(output.contains("Alice"));
assert!(output.contains("Bob"));
assert!(output.contains("Active"));
assert!(output.contains("Inactive"));
}
#[test]
fn test_render_includes_headers() {
let data = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
let table = OxurTable::new(data);
let output = table.render();
assert!(output.contains("ID"));
assert!(output.contains("Name"));
assert!(output.contains("Status"));
}
#[test]
fn test_render_contains_ansi_codes() {
let data = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
let table = OxurTable::new(data);
let output = table.render();
assert!(output.contains("\x1b[") || output.contains("\u{001b}["));
}
#[test]
fn test_table_with_special_characters() {
let data = vec![TestRow { id: 1, name: "Test & \"Special\"".into(), status: "OK".into() }];
let table = OxurTable::new(data);
let output = table.render();
assert!(output.contains("Test & \"Special\""));
}
#[test]
fn test_table_with_unicode() {
let data = vec![TestRow { id: 1, name: "Ñoño 日本語".into(), status: "✓".into() }];
let table = OxurTable::new(data);
let output = table.render();
assert!(output.contains("Ñoño"));
assert!(output.contains("日本語"));
assert!(output.contains("✓"));
}
#[test]
fn test_table_with_long_text() {
let long_name = "A".repeat(100);
let data = vec![TestRow { id: 1, name: long_name.clone(), status: "OK".into() }];
let table = OxurTable::new(data);
let output = table.render();
assert!(output.contains(&long_name[..50])); }
#[test]
fn test_table_with_empty_strings() {
let data = vec![TestRow { id: 1, name: "".into(), status: "".into() }];
let table = OxurTable::new(data);
let output = table.render();
assert!(!output.is_empty());
assert!(output.contains("ID")); }
#[test]
fn test_different_struct_type() {
#[derive(Tabled)]
struct DifferentRow {
#[tabled(rename = "Col1")]
col1: String,
#[tabled(rename = "Col2")]
col2: i32,
}
let data = vec![DifferentRow { col1: "Test".into(), col2: 42 }];
let table = OxurTable::new(data);
let output = table.render();
assert!(output.contains("Test"));
assert!(output.contains("42"));
assert!(output.contains("Col1"));
assert!(output.contains("Col2"));
}
#[test]
fn test_with_title_adds_title_row() {
let data = vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }];
let table = OxurTable::new(data).with_title("MY TABLE");
let output = table.render();
assert!(output.contains("MY TABLE"));
assert!(output.contains("Alice"));
}
#[test]
fn test_with_title_string_slice() {
let data = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
let table = OxurTable::new(data).with_title("TITLE FROM &str");
let output = table.render();
assert!(output.contains("TITLE FROM &str"));
}
#[test]
fn test_with_title_owned_string() {
let data = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
let title = String::from("OWNED TITLE");
let table = OxurTable::new(data).with_title(title);
let output = table.render();
assert!(output.contains("OWNED TITLE"));
}
#[test]
fn test_with_footer_adds_footer_row() {
let data = vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }];
let table = OxurTable::new(data).with_footer();
let output = table.render();
assert!(output.contains("Alice"));
let without_footer =
OxurTable::new(vec![TestRow { id: 1, name: "Alice".into(), status: "Active".into() }])
.render();
assert!(output.len() > without_footer.len());
}
#[test]
fn test_with_title_and_footer() {
let data = vec![
TestRow { id: 1, name: "Alice".into(), status: "Active".into() },
TestRow { id: 2, name: "Bob".into(), status: "Inactive".into() },
];
let table = OxurTable::new(data).with_title("USER LIST").with_footer();
let output = table.render();
assert!(output.contains("USER LIST"));
assert!(output.contains("Alice"));
assert!(output.contains("Bob"));
assert!(output.contains("ID"));
assert!(output.contains("Name"));
assert!(output.contains("Status"));
}
#[test]
fn test_chaining_order_does_not_matter() {
let data1 = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
let data2 = vec![TestRow { id: 1, name: "Test".into(), status: "OK".into() }];
let output1 = OxurTable::new(data1).with_title("TITLE").with_footer().render();
let output2 = OxurTable::new(data2).with_footer().with_title("TITLE").render();
assert_eq!(output1, output2);
}
#[test]
fn test_empty_data_with_title_and_footer() {
let data: Vec<TestRow> = vec![];
let table = OxurTable::new(data).with_title("EMPTY TABLE").with_footer();
let output = table.render();
assert!(output.contains("EMPTY TABLE"));
assert!(output.contains("ID"));
}
}