pub trait MarkdownTableRow {
fn column_names() -> Vec<&'static str>;
fn column_values(&self) -> Vec<String>;
}
pub fn as_table<T: MarkdownTableRow>(table: &[T]) -> String {
if table.is_empty() {
return String::new();
}
let column_names = T::column_names();
let mut max_widths: Vec<usize> = column_names.iter().map(|name| name.len()).collect();
for row in table {
let values = row
.column_values()
.into_iter()
.map(|v| v.replace('|', "\\|"))
.collect::<Vec<String>>();
for (i, value) in values.iter().enumerate() {
let width = value.chars().count(); max_widths[i] = max_widths[i].max(width);
}
}
let mut result = String::new();
result.push('|');
for (i, name) in column_names.iter().enumerate() {
result.push_str(&format!(" {:<width$} |", name, width = max_widths[i]));
}
result.push('\n');
result.push('|');
for width in &max_widths {
result.push_str(&format!("{:-<width$}|", "", width = width + 2));
}
result.push('\n');
for row in table {
result.push('|');
let values = row.column_values();
for (i, value) in values.iter().enumerate() {
result.push_str(&format!(" {:<width$} |", value, width = max_widths[i]));
}
result.push('\n');
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct Person {
name: String,
age: u32,
city: String,
}
impl MarkdownTableRow for Person {
fn column_names() -> Vec<&'static str> {
vec!["Name", "Age", "City"]
}
fn column_values(&self) -> Vec<String> {
vec![self.name.clone(), self.age.to_string(), self.city.clone()]
}
}
#[derive(Debug)]
struct Product {
id: u32,
name: String,
price: f64,
in_stock: bool,
}
impl MarkdownTableRow for Product {
fn column_names() -> Vec<&'static str> {
vec!["ID", "Product Name", "Price", "In Stock"]
}
fn column_values(&self) -> Vec<String> {
vec![
self.id.to_string(),
self.name.clone(),
format!("${:.2}", self.price),
if self.in_stock {
"Yes".to_string()
} else {
"No".to_string()
},
]
}
}
#[test]
fn test_empty_table() {
let people: Vec<Person> = vec![];
let result = as_table(&people);
assert_eq!(result, "");
}
#[test]
fn test_single_row() {
let people = vec![Person {
name: "Alice".to_string(),
age: 30,
city: "New York".to_string(),
}];
let result = as_table(&people);
let expected =
"| Name | Age | City |\n|-------|-----|----------|\n| Alice | 30 | New York |\n";
assert_eq!(result, expected);
}
#[test]
fn test_multiple_rows() {
let people = vec![
Person {
name: "Alice".to_string(),
age: 30,
city: "New York".to_string(),
},
Person {
name: "Bob".to_string(),
age: 25,
city: "Los Angeles".to_string(),
},
Person {
name: "Charlie".to_string(),
age: 35,
city: "Chicago".to_string(),
},
];
let result = as_table(&people);
let expected = "| Name | Age | City |\n|---------|-----|-------------|\n| Alice | 30 | New York |\n| Bob | 25 | Los Angeles |\n| Charlie | 35 | Chicago |\n";
assert_eq!(result, expected);
}
#[test]
fn test_varying_column_widths() {
let products = vec![
Product {
id: 1,
name: "Laptop".to_string(),
price: 999.99,
in_stock: true,
},
Product {
id: 2,
name: "Wireless Mouse".to_string(),
price: 29.99,
in_stock: false,
},
Product {
id: 100,
name: "USB-C Hub".to_string(),
price: 49.99,
in_stock: true,
},
];
let result = as_table(&products);
let expected = "| ID | Product Name | Price | In Stock |\n|-----|----------------|---------|----------|\n| 1 | Laptop | $999.99 | Yes |\n| 2 | Wireless Mouse | $29.99 | No |\n| 100 | USB-C Hub | $49.99 | Yes |\n";
assert_eq!(result, expected);
}
#[test]
fn test_pipe_character_escaping() {
let people = vec![
Person {
name: "Alice | Bob".to_string(),
age: 30,
city: "New York | NY".to_string(),
},
Person {
name: "Charlie".to_string(),
age: 35,
city: "Chicago".to_string(),
},
];
let result = as_table(&people);
let expected = r#"| Name | Age | City |
|--------------|-----|----------------|
| Alice | Bob | 30 | New York | NY |
| Charlie | 35 | Chicago |
"#;
assert_eq!(result, expected);
}
#[test]
fn test_unicode_characters() {
let people = vec![
Person {
name: "José".to_string(),
age: 28,
city: "São Paulo".to_string(),
},
Person {
name: "李明".to_string(),
age: 32,
city: "北京".to_string(),
},
Person {
name: "Müller".to_string(),
age: 45,
city: "München".to_string(),
},
];
let result = as_table(&people);
assert!(result.contains("José"));
assert!(result.contains("São Paulo"));
assert!(result.contains("李明"));
assert!(result.contains("北京"));
assert!(result.contains("Müller"));
assert!(result.contains("München"));
}
#[test]
fn test_empty_values() {
let people = vec![
Person {
name: "".to_string(),
age: 30,
city: "New York".to_string(),
},
Person {
name: "Bob".to_string(),
age: 25,
city: "".to_string(),
},
];
let result = as_table(&people);
let expected = "| Name | Age | City |\n|------|-----|----------|\n| | 30 | New York |\n| Bob | 25 | |\n";
assert_eq!(result, expected);
}
#[test]
fn test_single_column_table() {
struct SingleColumn {
value: String,
}
impl MarkdownTableRow for SingleColumn {
fn column_names() -> Vec<&'static str> {
vec!["Value"]
}
fn column_values(&self) -> Vec<String> {
vec![self.value.clone()]
}
}
let items = vec![
SingleColumn {
value: "First".to_string(),
},
SingleColumn {
value: "Second".to_string(),
},
SingleColumn {
value: "Third".to_string(),
},
];
let result = as_table(&items);
let expected = "| Value |\n|--------|\n| First |\n| Second |\n| Third |\n";
assert_eq!(result, expected);
}
#[test]
fn test_many_columns() {
struct ManyColumns {
a: String,
b: String,
c: String,
d: String,
e: String,
}
impl MarkdownTableRow for ManyColumns {
fn column_names() -> Vec<&'static str> {
vec!["A", "B", "C", "D", "E"]
}
fn column_values(&self) -> Vec<String> {
vec![
self.a.clone(),
self.b.clone(),
self.c.clone(),
self.d.clone(),
self.e.clone(),
]
}
}
let items = vec![ManyColumns {
a: "1".to_string(),
b: "2".to_string(),
c: "3".to_string(),
d: "4".to_string(),
e: "5".to_string(),
}];
let result = as_table(&items);
let expected = "| A | B | C | D | E |\n|---|---|---|---|---|\n| 1 | 2 | 3 | 4 | 5 |\n";
assert_eq!(result, expected);
}
#[test]
fn test_special_characters() {
let people = vec![
Person {
name: "Alice\tBob".to_string(),
age: 30,
city: "New\nYork".to_string(),
},
Person {
name: "Charlie\\Dave".to_string(),
age: 35,
city: "Chicago\"IL\"".to_string(),
},
];
let result = as_table(&people);
assert!(result.contains("Alice\tBob"));
assert!(result.contains("New\nYork"));
assert!(result.contains("Charlie\\Dave"));
assert!(result.contains("Chicago\"IL\""));
}
}