1use std::collections::HashMap;
2use std::fmt;
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum DataviewError {
7 #[error("The Dataview must have a row header")]
8 MissingRowHeader,
9 #[error("The Dataview must have at least one value")]
10 MissingValue,
11}
12
13#[derive(Debug, Default, Clone, Eq, PartialEq)]
41pub struct Dataview {
42 row_header: String,
43 headlines: HashMap<String, String>,
44 headline_order: Vec<String>,
45 values: HashMap<(String, String), String>,
46 column_order: Vec<String>,
47 row_order: Vec<String>,
48}
49
50impl Dataview {
51 pub fn row_header(&self) -> &str {
52 &self.row_header
53 }
54
55 pub fn headline(&self, key: &str) -> Option<&String> {
56 self.headlines.get(key)
57 }
58
59 pub fn headline_order(&self) -> &[String] {
60 &self.headline_order
61 }
62
63 pub fn value(&self, row: &str, column: &str) -> Option<&String> {
64 self.values.get(&(row.to_string(), column.to_string()))
65 }
66
67 pub fn column_order(&self) -> &[String] {
68 &self.column_order
69 }
70
71 pub fn row_order(&self) -> &[String] {
72 &self.row_order
73 }
74}
75
76fn escape_commas(s: &str) -> String {
77 s.replace(",", "\\,")
78}
79
80fn write_header_row(
81 f: &mut fmt::Formatter<'_>,
82 row_header: &str,
83 columns: &[String],
84) -> fmt::Result {
85 write!(f, "{}", escape_commas(row_header))?;
86 for col in columns {
87 write!(f, ",{}", escape_commas(col))?;
88 }
89 writeln!(f)
90}
91
92fn write_headlines(
93 f: &mut fmt::Formatter<'_>,
94 headline_order: &[String],
95 headlines: &HashMap<String, String>,
96) -> fmt::Result {
97 for name in headline_order {
98 if let Some(value) = headlines.get(name) {
99 writeln!(f, "<!>{},{}", escape_commas(name), escape_commas(value))?;
100 }
101 }
102 Ok(())
103}
104
105fn write_data_rows(
106 f: &mut fmt::Formatter<'_>,
107 rows: &[String],
108 columns: &[String],
109 values: &HashMap<(String, String), String>,
110) -> fmt::Result {
111 let number_of_rows = rows.len();
112 for (i, row) in rows.iter().enumerate() {
113 write!(f, "{}", escape_commas(row))?;
114 for col in columns {
115 write!(f, ",")?;
116 if let Some(value) = values.get(&(row.to_string(), col.to_string())) {
117 write!(f, "{}", escape_commas(value))?;
118 }
119 }
120
121 if i < number_of_rows - 1 {
123 writeln!(f)?;
124 }
125 }
126
127 Ok(())
128}
129
130impl fmt::Display for Dataview {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write_header_row(f, &self.row_header, &self.column_order)?;
133 write_headlines(f, &self.headline_order, &self.headlines)?;
134 write_data_rows(f, &self.row_order, &self.column_order, &self.values)
135 }
136}
137
138impl Dataview {
139 pub fn builder() -> DataviewBuilder {
154 DataviewBuilder::new()
155 }
156}
157
158#[derive(Debug, Clone, Default)]
162pub struct Row {
163 name: String,
164 cells: Vec<(String, String)>,
165}
166
167impl Row {
168 pub fn new(name: impl ToString) -> Self {
170 Self {
171 name: name.to_string(),
172 cells: Vec::new(),
173 }
174 }
175
176 pub fn add_cell(mut self, column: impl ToString, value: impl ToString) -> Self {
178 self.cells.push((column.to_string(), value.to_string()));
179 self
180 }
181}
182
183#[derive(Debug, Default, Clone)]
185pub struct DataviewBuilder {
186 row_header: Option<String>,
187 headlines: Option<HashMap<String, String>>,
188 values: Option<HashMap<(String, String), String>>,
189 headline_order: Vec<String>, column_order: Vec<String>, row_order: Vec<String>, }
193
194impl DataviewBuilder {
195 pub fn new() -> Self {
196 Self::default()
197 }
198
199 pub fn set_row_header(mut self, row_header: &str) -> Self {
200 self.row_header = Some(row_header.to_string());
201 self
202 }
203
204 pub fn add_headline<T: ToString>(mut self, key: &str, value: T) -> Self {
205 let mut headlines: HashMap<String, String> = self.headlines.unwrap_or_default();
206
207 let key_string = key.to_string();
208 if !self.headline_order.contains(&key_string) {
209 self.headline_order.push(key_string.clone());
210 }
211
212 headlines.insert(key_string, value.to_string());
213 self.headlines = Some(headlines);
214 self
215 }
216
217 pub fn add_value<T: ToString>(mut self, row: &str, column: &str, value: T) -> Self {
218 let mut values: HashMap<(String, String), String> = self.values.unwrap_or_default();
219
220 let column_string = column.to_string();
222 if !self.column_order.contains(&column_string) {
223 self.column_order.push(column_string.clone());
224 }
225
226 let row_string = row.to_string();
228 if !self.row_order.contains(&row_string) {
229 self.row_order.push(row_string.clone());
230 }
231
232 values.insert((row_string, column_string), value.to_string());
233 self.values = Some(values);
234 self
235 }
236
237 pub fn add_row(mut self, row: Row) -> Self {
255 for (col, val) in row.cells {
256 self = self.add_value(&row.name, &col, &val);
257 }
258 self
259 }
260
261 pub fn sort_rows(mut self) -> Self {
263 self.row_order.sort();
264 self
265 }
266
267 pub fn sort_rows_by<K, F>(mut self, mut f: F) -> Self
269 where
270 K: Ord,
271 F: FnMut(&str) -> K,
272 {
273 self.row_order.sort_by_key(|row| f(row));
274 self
275 }
276
277 pub fn sort_rows_with<F>(mut self, mut cmp: F) -> Self
279 where
280 F: FnMut(&str, &str) -> std::cmp::Ordering,
281 {
282 self.row_order.sort_by(|a, b| cmp(a, b));
283 self
284 }
285
286 pub fn build(self) -> Result<Dataview, DataviewError> {
313 let row_header = self.row_header.ok_or(DataviewError::MissingRowHeader)?;
314
315 let values = self.values.ok_or(DataviewError::MissingValue)?;
316
317 Ok(Dataview {
318 row_header,
319 headlines: self.headlines.unwrap_or_default(),
320 headline_order: self.headline_order,
321 values,
322 column_order: self.column_order,
323 row_order: self.row_order,
324 })
325 }
326}
327
328pub fn print_result_and_exit(dataview: Result<Dataview, DataviewError>) -> ! {
349 match dataview {
350 Ok(v) => {
351 println!("{v}");
352 std::process::exit(0)
353 }
354 Err(e) => {
355 eprintln!("ERROR: {e}");
356 std::process::exit(1)
357 }
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use pretty_assertions::assert_eq;
365
366 fn create_basic_dataview() -> Result<Dataview, DataviewError> {
368 DataviewBuilder::new()
369 .set_row_header("ID")
370 .add_headline("AverageAge", "30")
371 .add_value("1", "Name", "Alice")
372 .add_value("1", "Age", "30")
373 .build()
374 }
375
376 #[test]
377 fn test_dataview_builder_single_row() -> Result<(), DataviewError> {
378 let dataview = create_basic_dataview()?;
379
380 assert_eq!(dataview.row_header(), "ID");
382
383 assert_eq!(dataview.headline("AverageAge"), Some(&"30".to_string()));
385
386 assert_eq!(dataview.value("1", "Name"), Some(&"Alice".to_string()));
388 assert_eq!(dataview.value("1", "Age"), Some(&"30".to_string()));
389
390 assert_eq!(dataview.row_order().len(), 1);
392 assert_eq!(dataview.column_order().len(), 2);
393 assert!(dataview.column_order().contains(&"Name".to_string()));
394 assert!(dataview.column_order().contains(&"Age".to_string()));
395
396 Ok(())
397 }
398
399 #[test]
400 fn test_dataview_display_format() -> Result<(), DataviewError> {
401 let dataview = create_basic_dataview()?;
403 assert_eq!(
404 dataview.to_string(),
405 "\
406ID,Name,Age
407<!>AverageAge,30
4081,Alice,30"
409 );
410
411 let multi_row_dataview = DataviewBuilder::new()
413 .set_row_header("id")
414 .add_headline("Baz", "Foo")
416 .add_headline("AlertDetails", "this is red alert")
417 .add_value("001", "name", "agila")
418 .add_value("001", "status", "up")
419 .add_value("001", "Value", "97")
420 .add_value("002", "name", "lawin")
421 .add_value("002", "status", "down")
422 .add_value("002", "Value", "85")
423 .build()?;
424
425 let expected_output = "\
426id,name,status,Value
427<!>Baz,Foo
428<!>AlertDetails,this is red alert
429001,agila,up,97
430002,lawin,down,85";
431
432 assert_eq!(multi_row_dataview.to_string(), expected_output);
433
434 Ok(())
435 }
436
437 #[test]
438 fn test_special_characters_escaping() -> Result<(), DataviewError> {
439 let dataview = DataviewBuilder::new()
441 .set_row_header("queue,id")
442 .add_value("queue3", "number,code", "7,331")
443 .add_value("queue3", "count", "45,000")
444 .add_value("queue3", "ratio", "0.16")
445 .add_value("queue3", "status", "online")
446 .build()?;
447
448 let expected_output = "\
449queue\\,id,number\\,code,count,ratio,status
450queue3,7\\,331,45\\,000,0.16,online";
451
452 assert_eq!(dataview.to_string(), expected_output);
453
454 let dataview_special = DataviewBuilder::new()
456 .set_row_header("special")
457 .add_headline("special,headline", "headline value with, comma")
458 .add_value("special_case", "state", "testing: \"quotes\" & <symbols>")
459 .add_value("special_case", "data", "multi-line\ntext")
460 .build()?;
461
462 let output = dataview_special.to_string();
463 assert!(output.contains("special"));
464 assert!(output.contains("<!>special\\,headline,headline value with\\, comma"));
465 assert!(output.contains("testing: \"quotes\" & <symbols>"));
466 assert!(output.contains("multi-line\ntext"));
467
468 Ok(())
469 }
470
471 #[test]
472 fn test_empty_and_missing_values() -> Result<(), DataviewError> {
473 let dataview = DataviewBuilder::new()
475 .set_row_header("item")
476 .add_value("item1", "col1", "value1")
477 .add_value("item1", "col2", "value2")
478 .add_value("item2", "col1", "value3")
479 .add_value("item3", "col3", "value4") .build()?;
482
483 let output = dataview.to_string();
484
485 assert!(output.contains("item1,value1,value2,"));
487 assert!(output.contains("item2,value3,,"));
488 assert!(output.contains("item3,,,value4"));
489
490 assert_eq!(dataview.value("item2", "col2"), None);
492 assert_eq!(dataview.value("nonexistent", "col1"), None);
493
494 Ok(())
495 }
496
497 #[test]
498 fn test_dataview_complex() -> Result<(), DataviewError> {
499 let dataview = DataviewBuilder::new()
501 .set_row_header("cpu")
502 .add_headline("numOnlineCpus", "4")
504 .add_headline("loadAverage1Min", "0.32")
505 .add_headline("loadAverage5Min", "0.45")
506 .add_headline("loadAverage15Min", "0.38")
507 .add_headline("HyperThreadingStatus", "ENABLED")
508 .add_value("Average_cpu", "percentUtilisation", "3.75 %")
510 .add_value("Average_cpu", "percentUserTime", "2.15 %")
511 .add_value("Average_cpu", "percentKernelTime", "1.25 %")
512 .add_value("Average_cpu", "percentIdle", "96.25 %")
513 .add_value("cpu_0", "type", "GenuineIntel Intel(R)")
515 .add_value("cpu_0", "state", "on-line")
516 .add_value("cpu_0", "clockSpeed", "2500.00 MHz")
517 .add_value("cpu_0", "percentUtilisation", "3.25 %")
518 .add_value("cpu_0", "percentUserTime", "1.95 %")
519 .add_value("cpu_0", "percentKernelTime", "1.30 %")
520 .add_value("cpu_0", "percentIdle", "96.75 %")
521 .add_value("cpu_1", "type", "GenuineIntel Intel(R)")
523 .add_value("cpu_1", "state", "on-line")
524 .add_value("cpu_1", "clockSpeed", "2500.00 MHz")
525 .add_value("cpu_1", "percentUtilisation", "4.25 %")
526 .add_value("cpu_1", "percentUserTime", "2.35 %")
527 .add_value("cpu_1", "percentKernelTime", "1.20 %")
528 .add_value("cpu_1", "percentIdle", "95.75 %")
529 .add_value("cpu_2", "type", "GenuineIntel, Intel(R)")
531 .add_value("cpu_2", "state", "on-line")
532 .add_value("cpu_2", "clockSpeed", "2,500.00 MHz")
533 .add_value("cpu_0_logical#1", "type", "logical")
535 .add_value("cpu_0_logical#1", "state", "on-line")
536 .add_value("cpu_0_logical#1", "clockSpeed", "2500.00 MHz")
537 .add_value("cpu_0_logical#1", "percentUtilisation", "2.54 %")
538 .build()?;
539
540 let output = dataview.to_string();
542
543 assert_eq!(dataview.row_order().len(), 5); assert_eq!(dataview.row_order()[0], "Average_cpu".to_string());
546 assert_eq!(dataview.row_order()[1], "cpu_0".to_string());
547 assert_eq!(dataview.row_order()[2], "cpu_1".to_string());
548 assert_eq!(dataview.row_order()[3], "cpu_2".to_string());
549 assert_eq!(dataview.row_order()[4], "cpu_0_logical#1".to_string());
550
551 assert_eq!(dataview.headlines.len(), 5); let expected_columns = [
555 "percentUtilisation",
556 "percentUserTime",
557 "percentKernelTime",
558 "percentIdle",
559 "type",
560 "state",
561 "clockSpeed",
562 ];
563 for (idx, col) in expected_columns.iter().enumerate() {
564 if idx < dataview.column_order().len() {
565 assert!(dataview.column_order().contains(&col.to_string()));
566 }
567 }
568
569 assert!(output.starts_with("cpu,"));
571 assert!(output.contains("<!>numOnlineCpus,4\n"));
572 assert!(output.contains("<!>loadAverage1Min,0.32\n"));
573 assert!(output.contains("<!>HyperThreadingStatus,ENABLED\n"));
574
575 assert!(output.contains("GenuineIntel\\, Intel(R)"));
577 assert!(output.contains("2\\,500.00 MHz"));
578
579 Ok(())
580 }
581
582 #[test]
583 fn test_error_conditions() -> Result<(), ()> {
584 let result = DataviewBuilder::new()
586 .add_value("row1", "col1", "value1")
587 .build();
588
589 assert!(matches!(result, Err(DataviewError::MissingRowHeader)));
590
591 let result = DataviewBuilder::new().set_row_header("header").build();
593
594 assert!(matches!(result, Err(DataviewError::MissingValue)));
595
596 let result = DataviewBuilder::new()
598 .set_row_header("header")
599 .add_headline("headline1", "value1")
600 .build();
601
602 assert!(matches!(result, Err(DataviewError::MissingValue)));
603
604 Ok(())
605 }
606
607 #[test]
608 fn test_row_builder() -> Result<(), DataviewError> {
609 let row1 = Row::new("process1")
610 .add_cell("Status", "Running")
611 .add_cell("CPU", "2.5%");
612
613 let row2 = Row::new("process2")
614 .add_cell("Status", "Stopped")
615 .add_cell("CPU", "0.0%");
616
617 let dataview = Dataview::builder()
618 .set_row_header("Process")
619 .add_row(row1)
620 .add_row(row2)
621 .build()?;
622
623 let output = dataview.to_string();
624
625 assert!(output.contains("Process,Status,CPU"));
626 assert!(output.contains("process1,Running,2.5%"));
627 assert!(output.contains("process2,Stopped,0.0%"));
628
629 Ok(())
630 }
631
632 #[test]
633 fn test_row_sorting_methods() -> Result<(), DataviewError> {
634 let default = Dataview::builder()
636 .set_row_header("id")
637 .add_value("b", "col", "1")
638 .add_value("a", "col", "1")
639 .add_value("c", "col", "1")
640 .build()?;
641 assert_eq!(default.row_order(), &["b", "a", "c"]);
642
643 let sorted = Dataview::builder()
645 .set_row_header("id")
646 .add_value("b", "col", "1")
647 .add_value("a", "col", "1")
648 .add_value("c", "col", "1")
649 .sort_rows()
650 .build()?;
651 assert_eq!(sorted.row_order(), &["a", "b", "c"]);
652
653 let by_len = Dataview::builder()
655 .set_row_header("id")
656 .add_row(Row::new("long").add_cell("v", "1"))
657 .add_row(Row::new("mid").add_cell("v", "1"))
658 .add_row(Row::new("s").add_cell("v", "1"))
659 .sort_rows_by(|name| name.len())
660 .build()?;
661 assert_eq!(by_len.row_order(), &["s", "mid", "long"]);
662
663 let reversed = Dataview::builder()
665 .set_row_header("id")
666 .add_row(Row::new("alpha").add_cell("v", "1"))
667 .add_row(Row::new("beta").add_cell("v", "1"))
668 .add_row(Row::new("gamma").add_cell("v", "1"))
669 .sort_rows_with(|a, b| b.cmp(a))
670 .build()?;
671 assert_eq!(reversed.row_order(), &["gamma", "beta", "alpha"]);
672
673 Ok(())
674 }
675}