1pub trait MarkdownTableRow {
82 fn column_names() -> Vec<&'static str>;
84 fn column_values(&self) -> Vec<String>;
86}
87
88pub fn as_table<T: MarkdownTableRow>(table: &[T]) -> String {
144 if table.is_empty() {
145 return String::new();
146 }
147
148 let column_names = T::column_names();
149 let mut max_widths: Vec<usize> = column_names.iter().map(|name| name.len()).collect();
150
151 for row in table {
152 let values = row
153 .column_values()
154 .into_iter()
155 .map(|v| v.replace('|', "\\|"))
156 .collect::<Vec<String>>();
157 for (i, value) in values.iter().enumerate() {
158 let width = value.chars().count(); max_widths[i] = max_widths[i].max(width);
160 }
161 }
162
163 let mut result = String::new();
164
165 result.push('|');
167 for (i, name) in column_names.iter().enumerate() {
168 result.push_str(&format!(" {:<width$} |", name, width = max_widths[i]));
169 }
170 result.push('\n');
171
172 result.push('|');
174 for width in &max_widths {
175 result.push_str(&format!("{:-<width$}|", "", width = width + 2));
176 }
177 result.push('\n');
178
179 for row in table {
181 result.push('|');
182 let values = row.column_values();
183 for (i, value) in values.iter().enumerate() {
184 result.push_str(&format!(" {:<width$} |", value, width = max_widths[i]));
185 }
186 result.push('\n');
187 }
188
189 result
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[derive(Debug)]
197 struct Person {
198 name: String,
199 age: u32,
200 city: String,
201 }
202
203 impl MarkdownTableRow for Person {
204 fn column_names() -> Vec<&'static str> {
205 vec!["Name", "Age", "City"]
206 }
207
208 fn column_values(&self) -> Vec<String> {
209 vec![self.name.clone(), self.age.to_string(), self.city.clone()]
210 }
211 }
212
213 #[derive(Debug)]
214 struct Product {
215 id: u32,
216 name: String,
217 price: f64,
218 in_stock: bool,
219 }
220
221 impl MarkdownTableRow for Product {
222 fn column_names() -> Vec<&'static str> {
223 vec!["ID", "Product Name", "Price", "In Stock"]
224 }
225
226 fn column_values(&self) -> Vec<String> {
227 vec![
228 self.id.to_string(),
229 self.name.clone(),
230 format!("${:.2}", self.price),
231 if self.in_stock {
232 "Yes".to_string()
233 } else {
234 "No".to_string()
235 },
236 ]
237 }
238 }
239
240 #[test]
241 fn test_empty_table() {
242 let people: Vec<Person> = vec![];
243 let result = as_table(&people);
244 assert_eq!(result, "");
245 }
246
247 #[test]
248 fn test_single_row() {
249 let people = vec![Person {
250 name: "Alice".to_string(),
251 age: 30,
252 city: "New York".to_string(),
253 }];
254 let result = as_table(&people);
255 let expected =
256 "| Name | Age | City |\n|-------|-----|----------|\n| Alice | 30 | New York |\n";
257 assert_eq!(result, expected);
258 }
259
260 #[test]
261 fn test_multiple_rows() {
262 let people = vec![
263 Person {
264 name: "Alice".to_string(),
265 age: 30,
266 city: "New York".to_string(),
267 },
268 Person {
269 name: "Bob".to_string(),
270 age: 25,
271 city: "Los Angeles".to_string(),
272 },
273 Person {
274 name: "Charlie".to_string(),
275 age: 35,
276 city: "Chicago".to_string(),
277 },
278 ];
279 let result = as_table(&people);
280 let expected = "| Name | Age | City |\n|---------|-----|-------------|\n| Alice | 30 | New York |\n| Bob | 25 | Los Angeles |\n| Charlie | 35 | Chicago |\n";
281 assert_eq!(result, expected);
282 }
283
284 #[test]
285 fn test_varying_column_widths() {
286 let products = vec![
287 Product {
288 id: 1,
289 name: "Laptop".to_string(),
290 price: 999.99,
291 in_stock: true,
292 },
293 Product {
294 id: 2,
295 name: "Wireless Mouse".to_string(),
296 price: 29.99,
297 in_stock: false,
298 },
299 Product {
300 id: 100,
301 name: "USB-C Hub".to_string(),
302 price: 49.99,
303 in_stock: true,
304 },
305 ];
306 let result = as_table(&products);
307 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";
308 assert_eq!(result, expected);
309 }
310
311 #[test]
312 fn test_pipe_character_escaping() {
313 let people = vec![
314 Person {
315 name: "Alice | Bob".to_string(),
316 age: 30,
317 city: "New York | NY".to_string(),
318 },
319 Person {
320 name: "Charlie".to_string(),
321 age: 35,
322 city: "Chicago".to_string(),
323 },
324 ];
325 let result = as_table(&people);
326 let expected = r#"| Name | Age | City |
327|--------------|-----|----------------|
328| Alice | Bob | 30 | New York | NY |
329| Charlie | 35 | Chicago |
330"#;
331 assert_eq!(result, expected);
332 }
333
334 #[test]
335 fn test_unicode_characters() {
336 let people = vec![
337 Person {
338 name: "José".to_string(),
339 age: 28,
340 city: "São Paulo".to_string(),
341 },
342 Person {
343 name: "李明".to_string(),
344 age: 32,
345 city: "北京".to_string(),
346 },
347 Person {
348 name: "Müller".to_string(),
349 age: 45,
350 city: "München".to_string(),
351 },
352 ];
353 let result = as_table(&people);
354 assert!(result.contains("José"));
357 assert!(result.contains("São Paulo"));
358 assert!(result.contains("李明"));
359 assert!(result.contains("北京"));
360 assert!(result.contains("Müller"));
361 assert!(result.contains("München"));
362 }
363
364 #[test]
365 fn test_empty_values() {
366 let people = vec![
367 Person {
368 name: "".to_string(),
369 age: 30,
370 city: "New York".to_string(),
371 },
372 Person {
373 name: "Bob".to_string(),
374 age: 25,
375 city: "".to_string(),
376 },
377 ];
378 let result = as_table(&people);
379 let expected = "| Name | Age | City |\n|------|-----|----------|\n| | 30 | New York |\n| Bob | 25 | |\n";
380 assert_eq!(result, expected);
381 }
382
383 #[test]
384 fn test_single_column_table() {
385 struct SingleColumn {
386 value: String,
387 }
388
389 impl MarkdownTableRow for SingleColumn {
390 fn column_names() -> Vec<&'static str> {
391 vec!["Value"]
392 }
393
394 fn column_values(&self) -> Vec<String> {
395 vec![self.value.clone()]
396 }
397 }
398
399 let items = vec![
400 SingleColumn {
401 value: "First".to_string(),
402 },
403 SingleColumn {
404 value: "Second".to_string(),
405 },
406 SingleColumn {
407 value: "Third".to_string(),
408 },
409 ];
410 let result = as_table(&items);
411 let expected = "| Value |\n|--------|\n| First |\n| Second |\n| Third |\n";
412 assert_eq!(result, expected);
413 }
414
415 #[test]
416 fn test_many_columns() {
417 struct ManyColumns {
418 a: String,
419 b: String,
420 c: String,
421 d: String,
422 e: String,
423 }
424
425 impl MarkdownTableRow for ManyColumns {
426 fn column_names() -> Vec<&'static str> {
427 vec!["A", "B", "C", "D", "E"]
428 }
429
430 fn column_values(&self) -> Vec<String> {
431 vec![
432 self.a.clone(),
433 self.b.clone(),
434 self.c.clone(),
435 self.d.clone(),
436 self.e.clone(),
437 ]
438 }
439 }
440
441 let items = vec![ManyColumns {
442 a: "1".to_string(),
443 b: "2".to_string(),
444 c: "3".to_string(),
445 d: "4".to_string(),
446 e: "5".to_string(),
447 }];
448 let result = as_table(&items);
449 let expected = "| A | B | C | D | E |\n|---|---|---|---|---|\n| 1 | 2 | 3 | 4 | 5 |\n";
450 assert_eq!(result, expected);
451 }
452
453 #[test]
454 fn test_special_characters() {
455 let people = vec![
456 Person {
457 name: "Alice\tBob".to_string(),
458 age: 30,
459 city: "New\nYork".to_string(),
460 },
461 Person {
462 name: "Charlie\\Dave".to_string(),
463 age: 35,
464 city: "Chicago\"IL\"".to_string(),
465 },
466 ];
467 let result = as_table(&people);
468 assert!(result.contains("Alice\tBob"));
470 assert!(result.contains("New\nYork"));
471 assert!(result.contains("Charlie\\Dave"));
472 assert!(result.contains("Chicago\"IL\""));
473 }
474}