1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct TableData {
7 pub headers: Vec<String>,
8 pub rows: Vec<Vec<String>>,
9 pub metadata: HashMap<String, String>,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TableConfig {
15 pub title: Option<String>,
16 pub width: usize,
17 pub height: usize,
18 pub show_headers: bool,
19 pub sort_column: Option<String>,
20 pub sort_ascending: bool,
21 pub filters: HashMap<String, String>,
22 pub column_widths: Option<Vec<usize>>,
23 pub border_style: TableBorderStyle,
24 pub zebra_striping: bool,
25 pub highlight_row: Option<usize>,
26 pub max_column_width: Option<usize>,
27 pub show_row_numbers: bool,
28 pub pagination: Option<TablePagination>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub enum TableBorderStyle {
34 None,
35 Single,
36 Double,
37 Rounded,
38 Thick,
39 Custom {
40 horizontal: char,
41 vertical: char,
42 top_left: char,
43 top_right: char,
44 bottom_left: char,
45 bottom_right: char,
46 cross: char,
47 },
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct TablePagination {
53 pub page_size: usize,
54 pub current_page: usize,
55 pub show_page_info: bool,
56}
57
58#[derive(Debug, Clone, PartialEq)]
60pub enum SortDirection {
61 Ascending,
62 Descending,
63}
64
65#[derive(Debug, Clone)]
67pub enum ColumnType {
68 Text,
69 Number,
70 Date,
71 Boolean,
72}
73
74impl Default for TableConfig {
75 fn default() -> Self {
76 Self {
77 title: None,
78 width: 80,
79 height: 20,
80 show_headers: true,
81 sort_column: None,
82 sort_ascending: true,
83 filters: HashMap::new(),
84 column_widths: None,
85 border_style: TableBorderStyle::Single,
86 zebra_striping: false,
87 highlight_row: None,
88 max_column_width: Some(30),
89 show_row_numbers: false,
90 pagination: None,
91 }
92 }
93}
94
95pub fn render_table(data: &TableData, config: &TableConfig) -> String {
97 if data.headers.is_empty() || data.rows.is_empty() {
98 return "No data".to_string();
99 }
100
101 let filtered_rows = apply_filters(data, &config.filters);
103
104 let sorted_rows = apply_sorting(
106 &filtered_rows,
107 &data.headers,
108 &config.sort_column,
109 config.sort_ascending,
110 );
111
112 let (paginated_rows, page_info) = apply_pagination(&sorted_rows, &config.pagination);
114
115 let column_widths = calculate_column_widths(data, config, &paginated_rows);
117
118 let mut result = String::new();
119
120 if let Some(title) = &config.title {
122 result.push_str(&format!("{}\n", title));
123 }
124
125 result.push_str(&render_top_border(&column_widths, &config.border_style));
127
128 if config.show_headers {
130 result.push_str(&render_header_row(
131 &data.headers,
132 &column_widths,
133 &config.border_style,
134 config.show_row_numbers,
135 ));
136 result.push_str(&render_separator(&column_widths, &config.border_style));
137 }
138
139 for (index, row) in paginated_rows.iter().enumerate() {
141 let is_highlighted = config.highlight_row.map_or(false, |h| h == index);
142 let is_zebra = config.zebra_striping && index % 2 == 1;
143 result.push_str(&render_data_row(
144 row,
145 &column_widths,
146 &config.border_style,
147 config.show_row_numbers,
148 index,
149 is_highlighted,
150 is_zebra,
151 ));
152 }
153
154 result.push_str(&render_bottom_border(&column_widths, &config.border_style));
156
157 if let Some(info) = page_info {
159 result.push_str(&format!("\n{}", info));
160 }
161
162 result
163}
164
165fn apply_filters(data: &TableData, filters: &HashMap<String, String>) -> Vec<Vec<String>> {
167 if filters.is_empty() {
168 return data.rows.clone();
169 }
170
171 let mut filtered = Vec::new();
172
173 for row in &data.rows {
174 let mut matches = true;
175
176 for (column_name, filter_value) in filters {
177 if let Some(column_index) = data.headers.iter().position(|h| h == column_name) {
178 if column_index < row.len() {
179 let cell_value = &row[column_index];
180 if cell_value.to_lowercase() != filter_value.to_lowercase() {
181 matches = false;
182 break;
183 }
184 }
185 }
186 }
187
188 if matches {
189 filtered.push(row.clone());
190 }
191 }
192
193 filtered
194}
195
196fn apply_sorting(
198 rows: &[Vec<String>],
199 headers: &[String],
200 sort_column: &Option<String>,
201 ascending: bool,
202) -> Vec<Vec<String>> {
203 if let Some(column_name) = sort_column {
204 if let Some(column_index) = headers.iter().position(|h| h == column_name) {
205 let mut sorted = rows.to_vec();
206
207 sorted.sort_by(|a, b| {
208 let default_string = String::new();
209 let a_val = a.get(column_index).unwrap_or(&default_string);
210 let b_val = b.get(column_index).unwrap_or(&default_string);
211
212 if let (Ok(a_num), Ok(b_num)) = (a_val.parse::<f64>(), b_val.parse::<f64>()) {
214 if ascending {
215 a_num
216 .partial_cmp(&b_num)
217 .unwrap_or(std::cmp::Ordering::Equal)
218 } else {
219 b_num
220 .partial_cmp(&a_num)
221 .unwrap_or(std::cmp::Ordering::Equal)
222 }
223 } else {
224 if ascending {
226 a_val.cmp(b_val)
227 } else {
228 b_val.cmp(a_val)
229 }
230 }
231 });
232
233 return sorted;
234 }
235 }
236
237 rows.to_vec()
238}
239
240fn apply_pagination(
242 rows: &[Vec<String>],
243 pagination: &Option<TablePagination>,
244) -> (Vec<Vec<String>>, Option<String>) {
245 if let Some(pag) = pagination {
246 let total_rows = rows.len();
247 let total_pages = (total_rows + pag.page_size - 1) / pag.page_size;
248 let start_index = pag.current_page * pag.page_size;
249 let end_index = std::cmp::min(start_index + pag.page_size, total_rows);
250
251 let paginated = if start_index < total_rows {
252 rows[start_index..end_index].to_vec()
253 } else {
254 Vec::new()
255 };
256
257 let page_info = if pag.show_page_info {
258 Some(format!(
259 "Page {} of {} ({} total rows)",
260 pag.current_page + 1,
261 total_pages,
262 total_rows
263 ))
264 } else {
265 None
266 };
267
268 (paginated, page_info)
269 } else {
270 (rows.to_vec(), None)
271 }
272}
273
274fn calculate_column_widths(
276 data: &TableData,
277 config: &TableConfig,
278 rows: &[Vec<String>],
279) -> Vec<usize> {
280 if let Some(widths) = &config.column_widths {
281 return widths.clone();
282 }
283
284 let mut widths = Vec::new();
285 let max_width = config.max_column_width.unwrap_or(30);
286
287 for (i, header) in data.headers.iter().enumerate() {
288 let mut max_len = header.len();
289
290 for row in rows {
292 if let Some(cell) = row.get(i) {
293 max_len = max_len.max(cell.len());
294 }
295 }
296
297 max_len = max_len.min(max_width);
299
300 max_len = max_len.max(3);
302
303 widths.push(max_len);
304 }
305
306 widths
307}
308
309fn render_top_border(widths: &[usize], style: &TableBorderStyle) -> String {
311 match style {
312 TableBorderStyle::None => String::new(),
313 TableBorderStyle::Single => {
314 let mut border = String::from("┌");
315 for (i, &width) in widths.iter().enumerate() {
316 border.push_str(&"─".repeat(width + 2));
317 if i < widths.len() - 1 {
318 border.push('┬');
319 }
320 }
321 border.push('┐');
322 border.push('\n');
323 border
324 }
325 TableBorderStyle::Double => {
326 let mut border = String::from("╔");
327 for (i, &width) in widths.iter().enumerate() {
328 border.push_str(&"═".repeat(width + 2));
329 if i < widths.len() - 1 {
330 border.push('╦');
331 }
332 }
333 border.push('╗');
334 border.push('\n');
335 border
336 }
337 TableBorderStyle::Rounded => {
338 let mut border = String::from("╭");
339 for (i, &width) in widths.iter().enumerate() {
340 border.push_str(&"─".repeat(width + 2));
341 if i < widths.len() - 1 {
342 border.push('┬');
343 }
344 }
345 border.push('╮');
346 border.push('\n');
347 border
348 }
349 TableBorderStyle::Thick => {
350 let mut border = String::from("┏");
351 for (i, &width) in widths.iter().enumerate() {
352 border.push_str(&"━".repeat(width + 2));
353 if i < widths.len() - 1 {
354 border.push('┳');
355 }
356 }
357 border.push('┓');
358 border.push('\n');
359 border
360 }
361 TableBorderStyle::Custom {
362 horizontal,
363 vertical: _,
364 top_left,
365 top_right,
366 bottom_left: _,
367 bottom_right: _,
368 cross,
369 } => {
370 let mut border = String::from(*top_left);
371 for (i, &width) in widths.iter().enumerate() {
372 border.push_str(&horizontal.to_string().repeat(width + 2));
373 if i < widths.len() - 1 {
374 border.push(*cross);
375 }
376 }
377 border.push(*top_right);
378 border.push('\n');
379 border
380 }
381 }
382}
383
384fn render_bottom_border(widths: &[usize], style: &TableBorderStyle) -> String {
386 match style {
387 TableBorderStyle::None => String::new(),
388 TableBorderStyle::Single => {
389 let mut border = String::from("└");
390 for (i, &width) in widths.iter().enumerate() {
391 border.push_str(&"─".repeat(width + 2));
392 if i < widths.len() - 1 {
393 border.push('┴');
394 }
395 }
396 border.push('┘');
397 border.push('\n');
398 border
399 }
400 TableBorderStyle::Double => {
401 let mut border = String::from("╚");
402 for (i, &width) in widths.iter().enumerate() {
403 border.push_str(&"═".repeat(width + 2));
404 if i < widths.len() - 1 {
405 border.push('╩');
406 }
407 }
408 border.push('╝');
409 border.push('\n');
410 border
411 }
412 TableBorderStyle::Rounded => {
413 let mut border = String::from("╰");
414 for (i, &width) in widths.iter().enumerate() {
415 border.push_str(&"─".repeat(width + 2));
416 if i < widths.len() - 1 {
417 border.push('┴');
418 }
419 }
420 border.push('╯');
421 border.push('\n');
422 border
423 }
424 TableBorderStyle::Thick => {
425 let mut border = String::from("┗");
426 for (i, &width) in widths.iter().enumerate() {
427 border.push_str(&"━".repeat(width + 2));
428 if i < widths.len() - 1 {
429 border.push('┻');
430 }
431 }
432 border.push('┛');
433 border.push('\n');
434 border
435 }
436 TableBorderStyle::Custom {
437 horizontal,
438 vertical: _,
439 top_left: _,
440 top_right: _,
441 bottom_left,
442 bottom_right,
443 cross,
444 } => {
445 let mut border = String::from(*bottom_left);
446 for (i, &width) in widths.iter().enumerate() {
447 border.push_str(&horizontal.to_string().repeat(width + 2));
448 if i < widths.len() - 1 {
449 border.push(*cross);
450 }
451 }
452 border.push(*bottom_right);
453 border.push('\n');
454 border
455 }
456 }
457}
458
459fn render_separator(widths: &[usize], style: &TableBorderStyle) -> String {
461 match style {
462 TableBorderStyle::None => String::new(),
463 TableBorderStyle::Single => {
464 let mut sep = String::from("├");
465 for (i, &width) in widths.iter().enumerate() {
466 sep.push_str(&"─".repeat(width + 2));
467 if i < widths.len() - 1 {
468 sep.push('┼');
469 }
470 }
471 sep.push('┤');
472 sep.push('\n');
473 sep
474 }
475 TableBorderStyle::Double => {
476 let mut sep = String::from("╠");
477 for (i, &width) in widths.iter().enumerate() {
478 sep.push_str(&"═".repeat(width + 2));
479 if i < widths.len() - 1 {
480 sep.push('╬');
481 }
482 }
483 sep.push('╣');
484 sep.push('\n');
485 sep
486 }
487 TableBorderStyle::Rounded | TableBorderStyle::Thick => {
488 let mut sep = String::from("├");
490 for (i, &width) in widths.iter().enumerate() {
491 sep.push_str(&"─".repeat(width + 2));
492 if i < widths.len() - 1 {
493 sep.push('┼');
494 }
495 }
496 sep.push('┤');
497 sep.push('\n');
498 sep
499 }
500 TableBorderStyle::Custom {
501 horizontal,
502 vertical,
503 top_left: _,
504 top_right: _,
505 bottom_left: _,
506 bottom_right: _,
507 cross,
508 } => {
509 let mut sep = String::from(*vertical);
510 for (i, &width) in widths.iter().enumerate() {
511 sep.push_str(&horizontal.to_string().repeat(width + 2));
512 if i < widths.len() - 1 {
513 sep.push(*cross);
514 }
515 }
516 sep.push(*vertical);
517 sep.push('\n');
518 sep
519 }
520 }
521}
522
523fn render_header_row(
525 headers: &[String],
526 widths: &[usize],
527 style: &TableBorderStyle,
528 show_row_numbers: bool,
529) -> String {
530 let vertical_char = match style {
531 TableBorderStyle::None => ' ',
532 TableBorderStyle::Single | TableBorderStyle::Rounded | TableBorderStyle::Thick => '│',
533 TableBorderStyle::Double => '║',
534 TableBorderStyle::Custom { vertical, .. } => *vertical,
535 };
536
537 let mut row = String::new();
538
539 if style != &TableBorderStyle::None {
540 row.push(vertical_char);
541 }
542
543 if show_row_numbers {
545 row.push_str(" # ");
546 if style != &TableBorderStyle::None {
547 row.push(vertical_char);
548 }
549 }
550
551 for (i, header) in headers.iter().enumerate() {
552 let width = widths[i];
553 let truncated = if header.len() > width {
554 format!("{}…", &header[..width.saturating_sub(1)])
555 } else {
556 header.clone()
557 };
558
559 row.push(' ');
560 row.push_str(&format!("{:width$}", truncated, width = width));
561 row.push(' ');
562
563 if i < headers.len() - 1 && style != &TableBorderStyle::None {
564 row.push(vertical_char);
565 }
566 }
567
568 if style != &TableBorderStyle::None {
569 row.push(vertical_char);
570 }
571
572 row.push('\n');
573 row
574}
575
576fn render_data_row(
578 row: &[String],
579 widths: &[usize],
580 style: &TableBorderStyle,
581 show_row_numbers: bool,
582 row_index: usize,
583 _is_highlighted: bool,
584 _is_zebra: bool,
585) -> String {
586 let vertical_char = match style {
587 TableBorderStyle::None => ' ',
588 TableBorderStyle::Single | TableBorderStyle::Rounded | TableBorderStyle::Thick => '│',
589 TableBorderStyle::Double => '║',
590 TableBorderStyle::Custom { vertical, .. } => *vertical,
591 };
592
593 let mut result = String::new();
594
595 if style != &TableBorderStyle::None {
596 result.push(vertical_char);
597 }
598
599 if show_row_numbers {
601 result.push_str(&format!(" {} ", row_index + 1));
602 if style != &TableBorderStyle::None {
603 result.push(vertical_char);
604 }
605 }
606
607 for (i, cell) in row.iter().enumerate() {
608 let width = widths.get(i).copied().unwrap_or(10);
609 let truncated = if cell.len() > width {
610 format!("{}…", &cell[..width.saturating_sub(1)])
611 } else {
612 cell.clone()
613 };
614
615 result.push(' ');
616 result.push_str(&format!("{:width$}", truncated, width = width));
617 result.push(' ');
618
619 if i < row.len() - 1 && style != &TableBorderStyle::None {
620 result.push(vertical_char);
621 }
622 }
623
624 if style != &TableBorderStyle::None {
625 result.push(vertical_char);
626 }
627
628 result.push('\n');
629 result
630}
631
632pub fn parse_table_data(content: &str, delimiter: Option<char>) -> TableData {
634 let delimiter = delimiter.unwrap_or(',');
635 let lines: Vec<&str> = content.lines().collect();
636
637 if lines.is_empty() {
638 return TableData {
639 headers: Vec::new(),
640 rows: Vec::new(),
641 metadata: HashMap::new(),
642 };
643 }
644
645 let headers: Vec<String> = lines[0]
647 .split(delimiter)
648 .map(|s| s.trim().to_string())
649 .collect();
650
651 let mut rows = Vec::new();
653 for line in lines.iter().skip(1) {
654 if line.trim().is_empty() {
655 continue;
656 }
657
658 let row: Vec<String> = line
659 .split(delimiter)
660 .map(|s| s.trim().to_string())
661 .collect();
662
663 rows.push(row);
664 }
665
666 let mut metadata = HashMap::new();
667 metadata.insert("source".to_string(), "parsed_csv".to_string());
668 metadata.insert("delimiter".to_string(), delimiter.to_string());
669 metadata.insert("rows_count".to_string(), rows.len().to_string());
670 metadata.insert("columns_count".to_string(), headers.len().to_string());
671
672 TableData {
673 headers,
674 rows,
675 metadata,
676 }
677}
678
679pub fn parse_table_data_from_json(content: &str) -> Result<TableData, serde_json::Error> {
681 if let Ok(objects) = serde_json::from_str::<Vec<serde_json::Value>>(content) {
683 if objects.is_empty() {
684 return Ok(TableData {
685 headers: Vec::new(),
686 rows: Vec::new(),
687 metadata: HashMap::new(),
688 });
689 }
690
691 let mut headers = Vec::new();
693 if let Some(first_obj) = objects.first() {
694 if let serde_json::Value::Object(map) = first_obj {
695 for key in map.keys() {
696 headers.push(key.clone());
697 }
698 }
699 }
700
701 let mut rows = Vec::new();
703 for obj in &objects {
704 if let serde_json::Value::Object(map) = obj {
705 let mut row = Vec::new();
706 for header in &headers {
707 let value = map
708 .get(header)
709 .map(|v| match v {
710 serde_json::Value::String(s) => s.clone(),
711 serde_json::Value::Number(n) => n.to_string(),
712 serde_json::Value::Bool(b) => b.to_string(),
713 serde_json::Value::Null => "".to_string(),
714 _ => format!("{}", v),
715 })
716 .unwrap_or_default();
717 row.push(value);
718 }
719 rows.push(row);
720 }
721 }
722
723 let mut metadata = HashMap::new();
724 metadata.insert("source".to_string(), "parsed_json".to_string());
725 metadata.insert("format".to_string(), "array_of_objects".to_string());
726 metadata.insert("rows_count".to_string(), rows.len().to_string());
727 metadata.insert("columns_count".to_string(), headers.len().to_string());
728
729 return Ok(TableData {
730 headers,
731 rows,
732 metadata,
733 });
734 }
735
736 serde_json::from_str(content)
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
745 fn test_parse_csv_data() {
746 let csv_content = "Name,Age,City\nJohn,25,New York\nJane,30,Los Angeles\nBob,35,Chicago";
747 let table = parse_table_data(csv_content, None);
748
749 assert_eq!(table.headers, vec!["Name", "Age", "City"]);
750 assert_eq!(table.rows.len(), 3);
751 assert_eq!(table.rows[0], vec!["John", "25", "New York"]);
752 assert_eq!(table.rows[2], vec!["Bob", "35", "Chicago"]);
753 }
754
755 #[test]
756 fn test_table_rendering() {
757 let data = TableData {
758 headers: vec!["ID".to_string(), "Name".to_string(), "Status".to_string()],
759 rows: vec![
760 vec!["1".to_string(), "Alice".to_string(), "Active".to_string()],
761 vec!["2".to_string(), "Bob".to_string(), "Inactive".to_string()],
762 vec!["3".to_string(), "Charlie".to_string(), "Active".to_string()],
763 ],
764 metadata: HashMap::new(),
765 };
766
767 let config = TableConfig::default();
768 let result = render_table(&data, &config);
769
770 assert!(result.contains("ID"));
771 assert!(result.contains("Alice"));
772 assert!(result.contains("┌"));
773 assert!(result.contains("└"));
774 }
775
776 #[test]
777 fn test_table_filtering() {
778 let data = TableData {
779 headers: vec!["Name".to_string(), "Status".to_string()],
780 rows: vec![
781 vec!["Alice".to_string(), "Active".to_string()],
782 vec!["Bob".to_string(), "Inactive".to_string()],
783 vec!["Charlie".to_string(), "Active".to_string()],
784 ],
785 metadata: HashMap::new(),
786 };
787
788 let mut filters = HashMap::new();
789 filters.insert("Status".to_string(), "Active".to_string());
790
791 let filtered = apply_filters(&data, &filters);
792 assert_eq!(filtered.len(), 2);
793 assert!(filtered.iter().all(|row| row[1] == "Active"));
794 }
795
796 #[test]
797 fn test_table_sorting() {
798 let rows = vec![
799 vec!["Charlie".to_string(), "30".to_string()],
800 vec!["Alice".to_string(), "25".to_string()],
801 vec!["Bob".to_string(), "35".to_string()],
802 ];
803 let headers = vec!["Name".to_string(), "Age".to_string()];
804
805 let sorted = apply_sorting(&rows, &headers, &Some("Name".to_string()), true);
807 assert_eq!(sorted[0][0], "Alice");
808 assert_eq!(sorted[2][0], "Charlie");
809
810 let sorted = apply_sorting(&rows, &headers, &Some("Age".to_string()), true);
812 assert_eq!(sorted[0][1], "25");
813 assert_eq!(sorted[2][1], "35");
814 }
815
816 #[test]
817 fn test_json_parsing() {
818 let json_content = r#"[
819 {"id": 1, "name": "Alice", "active": true},
820 {"id": 2, "name": "Bob", "active": false}
821 ]"#;
822
823 let table = parse_table_data_from_json(json_content).unwrap();
824 assert_eq!(table.rows.len(), 2);
825 assert!(table.headers.contains(&"id".to_string()));
826 assert!(table.headers.contains(&"name".to_string()));
827 }
828
829 #[test]
830 fn test_pagination() {
831 let rows = vec![
832 vec!["1".to_string()],
833 vec!["2".to_string()],
834 vec!["3".to_string()],
835 vec!["4".to_string()],
836 vec!["5".to_string()],
837 ];
838
839 let pagination = TablePagination {
840 page_size: 2,
841 current_page: 1,
842 show_page_info: true,
843 };
844
845 let (paginated, info) = apply_pagination(&rows, &Some(pagination));
846 assert_eq!(paginated.len(), 2);
847 assert_eq!(paginated[0][0], "3");
848 assert_eq!(paginated[1][0], "4");
849 assert!(info.unwrap().contains("Page 2 of 3"));
850 }
851}