1use std::collections::HashMap;
2use std::error::Error;
3use std::fmt;
4
5#[derive(Debug)]
6pub enum DataviewError {
7 MissingRowHeader,
8 MissingValue,
9}
10
11impl fmt::Display for DataviewError {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 match self {
14 DataviewError::MissingRowHeader => write!(f, "The Dataview must have a row header"),
15 DataviewError::MissingValue => write!(f, "The Dataview must have at least one value"),
16 }
17 }
18}
19
20impl Error for DataviewError {}
21
22#[derive(Debug, Default, Clone, Eq, PartialEq)]
50pub struct Dataview {
51 row_header: String,
52 headlines: HashMap<String, String>,
53 headline_order: Vec<String>,
54 values: HashMap<(String, String), String>,
55 column_order: Vec<String>,
56 row_order: Vec<String>,
57}
58
59impl Dataview {
60 pub fn row_header(&self) -> &str {
73 &self.row_header
74 }
75
76 pub fn headline(&self, key: &str) -> Option<&String> {
78 self.headlines.get(key)
79 }
80
81 pub fn headline_order(&self) -> &[String] {
83 &self.headline_order
84 }
85
86 pub fn value(&self, row: &str, column: &str) -> Option<&String> {
88 self.values.get(&(row.to_string(), column.to_string()))
89 }
90
91 pub fn column_order(&self) -> &[String] {
93 &self.column_order
94 }
95
96 pub fn row_order(&self) -> &[String] {
98 &self.row_order
99 }
100}
101
102fn escape_commas(s: &str) -> String {
103 s.replace(",", "\\,")
104}
105
106fn write_header_row(
107 f: &mut fmt::Formatter<'_>,
108 row_header: &str,
109 columns: &[String],
110) -> fmt::Result {
111 write!(f, "{}", escape_commas(row_header))?;
112 for col in columns {
113 write!(f, ",{}", escape_commas(col))?;
114 }
115 writeln!(f)
116}
117
118fn write_headlines(
119 f: &mut fmt::Formatter<'_>,
120 headline_order: &[String],
121 headlines: &HashMap<String, String>,
122) -> fmt::Result {
123 for name in headline_order {
124 if let Some(value) = headlines.get(name) {
125 writeln!(f, "<!>{},{}", escape_commas(name), escape_commas(value))?;
126 }
127 }
128 Ok(())
129}
130
131fn write_data_rows(
132 f: &mut fmt::Formatter<'_>,
133 rows: &[String],
134 columns: &[String],
135 values: &HashMap<(String, String), String>,
136) -> fmt::Result {
137 let number_of_rows = rows.len();
138 for (i, row) in rows.iter().enumerate() {
139 write!(f, "{}", escape_commas(row))?;
140 for col in columns {
141 write!(f, ",")?;
142 if let Some(value) = values.get(&(row.to_string(), col.to_string())) {
143 write!(f, "{}", escape_commas(value))?;
144 }
145 }
146
147 if i < number_of_rows - 1 {
149 writeln!(f)?;
150 }
151 }
152
153 Ok(())
154}
155
156impl fmt::Display for Dataview {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 write_header_row(f, &self.row_header, &self.column_order)?;
159 write_headlines(f, &self.headline_order, &self.headlines)?;
160 write_data_rows(f, &self.row_order, &self.column_order, &self.values)
161 }
162}
163
164impl Dataview {
165 pub fn builder() -> DataviewBuilder {
180 DataviewBuilder::new()
181 }
182}
183
184#[derive(Debug, Clone, Default)]
188pub struct Row {
189 name: String,
190 cells: Vec<(String, String)>,
191}
192
193impl Row {
194 pub fn new(name: impl ToString) -> Self {
196 Self {
197 name: name.to_string(),
198 cells: Vec::new(),
199 }
200 }
201
202 pub fn add_cell(mut self, column: impl ToString, value: impl ToString) -> Self {
204 self.cells.push((column.to_string(), value.to_string()));
205 self
206 }
207}
208
209#[derive(Debug, Default, Clone)]
211pub struct DataviewBuilder {
212 row_header: Option<String>,
213 headlines: Option<HashMap<String, String>>,
214 values: Option<HashMap<(String, String), String>>,
215 headline_order: Vec<String>, column_order: Vec<String>, row_order: Vec<String>, }
219
220impl DataviewBuilder {
221 pub fn new() -> Self {
223 Self::default()
224 }
225
226 pub fn set_row_header(mut self, row_header: &str) -> Self {
228 self.row_header = Some(row_header.to_string());
229 self
230 }
231
232 pub fn add_headline<T: ToString>(mut self, key: &str, value: T) -> Self {
234 let mut headlines: HashMap<String, String> = self.headlines.unwrap_or_default();
235
236 let key_string = key.to_string();
237 if !self.headline_order.contains(&key_string) {
238 self.headline_order.push(key_string.clone());
239 }
240
241 headlines.insert(key_string, value.to_string());
242 self.headlines = Some(headlines);
243 self
244 }
245
246 pub fn add_value<T: ToString>(mut self, row: &str, column: &str, value: T) -> Self {
248 let mut values: HashMap<(String, String), String> = self.values.unwrap_or_default();
249
250 let column_string = column.to_string();
252 if !self.column_order.contains(&column_string) {
253 self.column_order.push(column_string.clone());
254 }
255
256 let row_string = row.to_string();
258 if !self.row_order.contains(&row_string) {
259 self.row_order.push(row_string.clone());
260 }
261
262 values.insert((row_string, column_string), value.to_string());
263 self.values = Some(values);
264 self
265 }
266
267 pub fn add_row(mut self, row: Row) -> Self {
285 for (col, val) in row.cells {
286 self = self.add_value(&row.name, &col, &val);
287 }
288 self
289 }
290
291 pub fn sort_rows(mut self) -> Self {
294 self.row_order.sort();
295 self
296 }
297
298 pub fn sort_rows_by<K, F>(mut self, mut f: F) -> Self
300 where
301 K: Ord,
302 F: FnMut(&str) -> K,
303 {
304 self.row_order.sort_by_key(|row| f(row));
305 self
306 }
307
308 pub fn sort_rows_with<F>(mut self, mut cmp: F) -> Self
310 where
311 F: FnMut(&str, &str) -> std::cmp::Ordering,
312 {
313 self.row_order.sort_by(|a, b| cmp(a, b));
314 self
315 }
316
317 pub fn build(self) -> Result<Dataview, DataviewError> {
344 let row_header = self.row_header.ok_or(DataviewError::MissingRowHeader)?;
345
346 let values = self.values.ok_or(DataviewError::MissingValue)?;
347
348 Ok(Dataview {
349 row_header,
350 headlines: self.headlines.unwrap_or_default(),
351 headline_order: self.headline_order,
352 values,
353 column_order: self.column_order,
354 row_order: self.row_order,
355 })
356 }
357}
358
359pub fn print_result_and_exit(dataview: Result<Dataview, DataviewError>) -> ! {
381 match dataview {
382 Ok(v) => {
383 println!("{v}");
384 std::process::exit(0)
385 }
386 Err(e) => {
387 eprintln!("ERROR: {e}");
388 std::process::exit(1)
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use pretty_assertions::assert_eq;
397
398 fn create_basic_dataview() -> Result<Dataview, DataviewError> {
400 DataviewBuilder::new()
401 .set_row_header("ID")
402 .add_headline("AverageAge", "30")
403 .add_value("1", "Name", "Alice")
404 .add_value("1", "Age", "30")
405 .build()
406 }
407
408 #[test]
409 fn test_dataview_builder_single_row() -> Result<(), DataviewError> {
410 let dataview = create_basic_dataview()?;
411
412 assert_eq!(dataview.row_header(), "ID");
414
415 assert_eq!(dataview.headline("AverageAge"), Some(&"30".to_string()));
417
418 assert_eq!(dataview.value("1", "Name"), Some(&"Alice".to_string()));
420 assert_eq!(dataview.value("1", "Age"), Some(&"30".to_string()));
421
422 assert_eq!(dataview.row_order().len(), 1);
424 assert_eq!(dataview.column_order().len(), 2);
425 assert!(dataview.column_order().contains(&"Name".to_string()));
426 assert!(dataview.column_order().contains(&"Age".to_string()));
427
428 Ok(())
429 }
430
431 #[test]
432 fn test_dataview_display_format() -> Result<(), DataviewError> {
433 let dataview = create_basic_dataview()?;
435 assert_eq!(
436 dataview.to_string(),
437 "\
438ID,Name,Age
439<!>AverageAge,30
4401,Alice,30"
441 );
442
443 let multi_row_dataview = DataviewBuilder::new()
445 .set_row_header("id")
446 .add_headline("Baz", "Foo")
448 .add_headline("AlertDetails", "this is red alert")
449 .add_value("001", "name", "agila")
450 .add_value("001", "status", "up")
451 .add_value("001", "Value", "97")
452 .add_value("002", "name", "lawin")
453 .add_value("002", "status", "down")
454 .add_value("002", "Value", "85")
455 .build()?;
456
457 let expected_output = "\
458id,name,status,Value
459<!>Baz,Foo
460<!>AlertDetails,this is red alert
461001,agila,up,97
462002,lawin,down,85";
463
464 assert_eq!(multi_row_dataview.to_string(), expected_output);
465
466 Ok(())
467 }
468
469 #[test]
470 fn test_special_characters_escaping() -> Result<(), DataviewError> {
471 let dataview = DataviewBuilder::new()
473 .set_row_header("queue,id")
474 .add_value("queue3", "number,code", "7,331")
475 .add_value("queue3", "count", "45,000")
476 .add_value("queue3", "ratio", "0.16")
477 .add_value("queue3", "status", "online")
478 .build()?;
479
480 let expected_output = "\
481queue\\,id,number\\,code,count,ratio,status
482queue3,7\\,331,45\\,000,0.16,online";
483
484 assert_eq!(dataview.to_string(), expected_output);
485
486 let dataview_special = DataviewBuilder::new()
488 .set_row_header("special")
489 .add_headline("special,headline", "headline value with, comma")
490 .add_value("special_case", "state", "testing: \"quotes\" & <symbols>")
491 .add_value("special_case", "data", "multi-line\ntext")
492 .build()?;
493
494 let output = dataview_special.to_string();
495 assert!(output.contains("special"));
496 assert!(output.contains("<!>special\\,headline,headline value with\\, comma"));
497 assert!(output.contains("testing: \"quotes\" & <symbols>"));
498 assert!(output.contains("multi-line\ntext"));
499
500 Ok(())
501 }
502
503 #[test]
504 fn test_empty_and_missing_values() -> Result<(), DataviewError> {
505 let dataview = DataviewBuilder::new()
507 .set_row_header("item")
508 .add_value("item1", "col1", "value1")
509 .add_value("item1", "col2", "value2")
510 .add_value("item2", "col1", "value3")
511 .add_value("item3", "col3", "value4") .build()?;
514
515 let output = dataview.to_string();
516
517 assert!(output.contains("item1,value1,value2,"));
519 assert!(output.contains("item2,value3,,"));
520 assert!(output.contains("item3,,,value4"));
521
522 assert_eq!(dataview.value("item2", "col2"), None);
524 assert_eq!(dataview.value("nonexistent", "col1"), None);
525
526 Ok(())
527 }
528
529 #[test]
530 fn test_dataview_complex() -> Result<(), DataviewError> {
531 let dataview = DataviewBuilder::new()
533 .set_row_header("cpu")
534 .add_headline("numOnlineCpus", "4")
536 .add_headline("loadAverage1Min", "0.32")
537 .add_headline("loadAverage5Min", "0.45")
538 .add_headline("loadAverage15Min", "0.38")
539 .add_headline("HyperThreadingStatus", "ENABLED")
540 .add_value("Average_cpu", "percentUtilisation", "3.75 %")
542 .add_value("Average_cpu", "percentUserTime", "2.15 %")
543 .add_value("Average_cpu", "percentKernelTime", "1.25 %")
544 .add_value("Average_cpu", "percentIdle", "96.25 %")
545 .add_value("cpu_0", "type", "GenuineIntel Intel(R)")
547 .add_value("cpu_0", "state", "on-line")
548 .add_value("cpu_0", "clockSpeed", "2500.00 MHz")
549 .add_value("cpu_0", "percentUtilisation", "3.25 %")
550 .add_value("cpu_0", "percentUserTime", "1.95 %")
551 .add_value("cpu_0", "percentKernelTime", "1.30 %")
552 .add_value("cpu_0", "percentIdle", "96.75 %")
553 .add_value("cpu_1", "type", "GenuineIntel Intel(R)")
555 .add_value("cpu_1", "state", "on-line")
556 .add_value("cpu_1", "clockSpeed", "2500.00 MHz")
557 .add_value("cpu_1", "percentUtilisation", "4.25 %")
558 .add_value("cpu_1", "percentUserTime", "2.35 %")
559 .add_value("cpu_1", "percentKernelTime", "1.20 %")
560 .add_value("cpu_1", "percentIdle", "95.75 %")
561 .add_value("cpu_2", "type", "GenuineIntel, Intel(R)")
563 .add_value("cpu_2", "state", "on-line")
564 .add_value("cpu_2", "clockSpeed", "2,500.00 MHz")
565 .add_value("cpu_0_logical#1", "type", "logical")
567 .add_value("cpu_0_logical#1", "state", "on-line")
568 .add_value("cpu_0_logical#1", "clockSpeed", "2500.00 MHz")
569 .add_value("cpu_0_logical#1", "percentUtilisation", "2.54 %")
570 .build()?;
571
572 let output = dataview.to_string();
574
575 assert_eq!(dataview.row_order().len(), 5); assert_eq!(dataview.row_order()[0], "Average_cpu".to_string());
578 assert_eq!(dataview.row_order()[1], "cpu_0".to_string());
579 assert_eq!(dataview.row_order()[2], "cpu_1".to_string());
580 assert_eq!(dataview.row_order()[3], "cpu_2".to_string());
581 assert_eq!(dataview.row_order()[4], "cpu_0_logical#1".to_string());
582
583 assert_eq!(dataview.headlines.len(), 5); let expected_columns = [
587 "percentUtilisation",
588 "percentUserTime",
589 "percentKernelTime",
590 "percentIdle",
591 "type",
592 "state",
593 "clockSpeed",
594 ];
595 for (idx, col) in expected_columns.iter().enumerate() {
596 if idx < dataview.column_order().len() {
597 assert!(dataview.column_order().contains(&col.to_string()));
598 }
599 }
600
601 assert!(output.starts_with("cpu,"));
603 assert!(output.contains("<!>numOnlineCpus,4\n"));
604 assert!(output.contains("<!>loadAverage1Min,0.32\n"));
605 assert!(output.contains("<!>HyperThreadingStatus,ENABLED\n"));
606
607 assert!(output.contains("GenuineIntel\\, Intel(R)"));
609 assert!(output.contains("2\\,500.00 MHz"));
610
611 Ok(())
612 }
613
614 #[test]
615 fn test_error_conditions() -> Result<(), ()> {
616 let result = DataviewBuilder::new()
618 .add_value("row1", "col1", "value1")
619 .build();
620
621 assert!(matches!(result, Err(DataviewError::MissingRowHeader)));
622
623 let result = DataviewBuilder::new().set_row_header("header").build();
625
626 assert!(matches!(result, Err(DataviewError::MissingValue)));
627
628 let result = DataviewBuilder::new()
630 .set_row_header("header")
631 .add_headline("headline1", "value1")
632 .build();
633
634 assert!(matches!(result, Err(DataviewError::MissingValue)));
635
636 Ok(())
637 }
638
639 #[test]
640 fn test_row_builder() -> Result<(), DataviewError> {
641 let row1 = Row::new("process1")
642 .add_cell("Status", "Running")
643 .add_cell("CPU", "2.5%");
644
645 let row2 = Row::new("process2")
646 .add_cell("Status", "Stopped")
647 .add_cell("CPU", "0.0%");
648
649 let dataview = Dataview::builder()
650 .set_row_header("Process")
651 .add_row(row1)
652 .add_row(row2)
653 .build()?;
654
655 let output = dataview.to_string();
656
657 assert!(output.contains("Process,Status,CPU"));
658 assert!(output.contains("process1,Running,2.5%"));
659 assert!(output.contains("process2,Stopped,0.0%"));
660
661 Ok(())
662 }
663
664 #[test]
665 fn test_row_sorting_methods() -> Result<(), DataviewError> {
666 let default = Dataview::builder()
668 .set_row_header("id")
669 .add_value("b", "col", "1")
670 .add_value("a", "col", "1")
671 .add_value("c", "col", "1")
672 .build()?;
673 assert_eq!(default.row_order(), &["b", "a", "c"]);
674
675 let sorted = Dataview::builder()
677 .set_row_header("id")
678 .add_value("b", "col", "1")
679 .add_value("a", "col", "1")
680 .add_value("c", "col", "1")
681 .sort_rows()
682 .build()?;
683 assert_eq!(sorted.row_order(), &["a", "b", "c"]);
684
685 let by_len = Dataview::builder()
687 .set_row_header("id")
688 .add_row(Row::new("long").add_cell("v", "1"))
689 .add_row(Row::new("mid").add_cell("v", "1"))
690 .add_row(Row::new("s").add_cell("v", "1"))
691 .sort_rows_by(|name| name.len())
692 .build()?;
693 assert_eq!(by_len.row_order(), &["s", "mid", "long"]);
694
695 let reversed = Dataview::builder()
697 .set_row_header("id")
698 .add_row(Row::new("alpha").add_cell("v", "1"))
699 .add_row(Row::new("beta").add_cell("v", "1"))
700 .add_row(Row::new("gamma").add_cell("v", "1"))
701 .sort_rows_with(|a, b| b.cmp(a))
702 .build()?;
703 assert_eq!(reversed.row_order(), &["gamma", "beta", "alpha"]);
704
705 Ok(())
706 }
707}