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, Default, Clone)]
160pub struct DataviewBuilder {
161 row_header: Option<String>,
162 headlines: Option<HashMap<String, String>>,
163 values: Option<HashMap<(String, String), String>>,
164 headline_order: Vec<String>, column_order: Vec<String>, row_order: Vec<String>, }
168
169impl DataviewBuilder {
170 pub fn new() -> Self {
171 Self::default()
172 }
173
174 pub fn set_row_header(mut self, row_header: &str) -> Self {
175 self.row_header = Some(row_header.to_string());
176 self
177 }
178
179 pub fn add_headline<T: ToString>(mut self, key: &str, value: T) -> Self {
180 let mut headlines: HashMap<String, String> = self.headlines.unwrap_or_default();
181
182 let key_string = key.to_string();
183 if !self.headline_order.contains(&key_string) {
184 self.headline_order.push(key_string.clone());
185 }
186
187 headlines.insert(key_string, value.to_string());
188 self.headlines = Some(headlines);
189 self
190 }
191
192 pub fn add_value<T: ToString>(mut self, row: &str, column: &str, value: T) -> Self {
193 let mut values: HashMap<(String, String), String> = self.values.unwrap_or_default();
194
195 let column_string = column.to_string();
197 if !self.column_order.contains(&column_string) {
198 self.column_order.push(column_string.clone());
199 }
200
201 let row_string = row.to_string();
203 if !self.row_order.contains(&row_string) {
204 self.row_order.push(row_string.clone());
205 }
206
207 values.insert((row_string, column_string), value.to_string());
208 self.values = Some(values);
209 self
210 }
211
212 pub fn build(self) -> Result<Dataview, DataviewError> {
239 let row_header = self.row_header.ok_or(DataviewError::MissingRowHeader)?;
240
241 let values = self.values.ok_or(DataviewError::MissingValue)?;
242
243 Ok(Dataview {
244 row_header,
245 headlines: self.headlines.unwrap_or_default(),
246 headline_order: self.headline_order,
247 values,
248 column_order: self.column_order,
249 row_order: self.row_order,
250 })
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use pretty_assertions::assert_eq;
258
259 fn create_basic_dataview() -> Result<Dataview, DataviewError> {
261 DataviewBuilder::new()
262 .set_row_header("ID")
263 .add_headline("AverageAge", "30")
264 .add_value("1", "Name", "Alice")
265 .add_value("1", "Age", "30")
266 .build()
267 }
268
269 #[test]
270 fn test_dataview_builder_single_row() -> Result<(), DataviewError> {
271 let dataview = create_basic_dataview()?;
272
273 assert_eq!(dataview.row_header(), "ID");
275
276 assert_eq!(dataview.headline("AverageAge"), Some(&"30".to_string()));
278
279 assert_eq!(dataview.value("1", "Name"), Some(&"Alice".to_string()));
281 assert_eq!(dataview.value("1", "Age"), Some(&"30".to_string()));
282
283 assert_eq!(dataview.row_order().len(), 1);
285 assert_eq!(dataview.column_order().len(), 2);
286 assert!(dataview.column_order().contains(&"Name".to_string()));
287 assert!(dataview.column_order().contains(&"Age".to_string()));
288
289 Ok(())
290 }
291
292 #[test]
293 fn test_dataview_display_format() -> Result<(), DataviewError> {
294 let dataview = create_basic_dataview()?;
296 assert_eq!(
297 dataview.to_string(),
298 "\
299ID,Name,Age
300<!>AverageAge,30
3011,Alice,30"
302 );
303
304 let multi_row_dataview = DataviewBuilder::new()
306 .set_row_header("id")
307 .add_headline("Baz", "Foo")
309 .add_headline("AlertDetails", "this is red alert")
310 .add_value("001", "name", "agila")
311 .add_value("001", "status", "up")
312 .add_value("001", "Value", "97")
313 .add_value("002", "name", "lawin")
314 .add_value("002", "status", "down")
315 .add_value("002", "Value", "85")
316 .build()?;
317
318 let expected_output = "\
319id,name,status,Value
320<!>Baz,Foo
321<!>AlertDetails,this is red alert
322001,agila,up,97
323002,lawin,down,85";
324
325 assert_eq!(multi_row_dataview.to_string(), expected_output);
326
327 Ok(())
328 }
329
330 #[test]
331 fn test_special_characters_escaping() -> Result<(), DataviewError> {
332 let dataview = DataviewBuilder::new()
334 .set_row_header("queue,id")
335 .add_value("queue3", "number,code", "7,331")
336 .add_value("queue3", "count", "45,000")
337 .add_value("queue3", "ratio", "0.16")
338 .add_value("queue3", "status", "online")
339 .build()?;
340
341 let expected_output = "\
342queue\\,id,number\\,code,count,ratio,status
343queue3,7\\,331,45\\,000,0.16,online";
344
345 assert_eq!(dataview.to_string(), expected_output);
346
347 let dataview_special = DataviewBuilder::new()
349 .set_row_header("special")
350 .add_headline("special,headline", "headline value with, comma")
351 .add_value("special_case", "state", "testing: \"quotes\" & <symbols>")
352 .add_value("special_case", "data", "multi-line\ntext")
353 .build()?;
354
355 let output = dataview_special.to_string();
356 assert!(output.contains("special"));
357 assert!(output.contains("<!>special\\,headline,headline value with\\, comma"));
358 assert!(output.contains("testing: \"quotes\" & <symbols>"));
359 assert!(output.contains("multi-line\ntext"));
360
361 Ok(())
362 }
363
364 #[test]
365 fn test_empty_and_missing_values() -> Result<(), DataviewError> {
366 let dataview = DataviewBuilder::new()
368 .set_row_header("item")
369 .add_value("item1", "col1", "value1")
370 .add_value("item1", "col2", "value2")
371 .add_value("item2", "col1", "value3")
372 .add_value("item3", "col3", "value4") .build()?;
375
376 let output = dataview.to_string();
377
378 assert!(output.contains("item1,value1,value2,"));
380 assert!(output.contains("item2,value3,,"));
381 assert!(output.contains("item3,,,value4"));
382
383 assert_eq!(dataview.value("item2", "col2"), None);
385 assert_eq!(dataview.value("nonexistent", "col1"), None);
386
387 Ok(())
388 }
389
390 #[test]
391 fn test_dataview_complex() -> Result<(), DataviewError> {
392 let dataview = DataviewBuilder::new()
394 .set_row_header("cpu")
395 .add_headline("numOnlineCpus", "4")
397 .add_headline("loadAverage1Min", "0.32")
398 .add_headline("loadAverage5Min", "0.45")
399 .add_headline("loadAverage15Min", "0.38")
400 .add_headline("HyperThreadingStatus", "ENABLED")
401 .add_value("Average_cpu", "percentUtilisation", "3.75 %")
403 .add_value("Average_cpu", "percentUserTime", "2.15 %")
404 .add_value("Average_cpu", "percentKernelTime", "1.25 %")
405 .add_value("Average_cpu", "percentIdle", "96.25 %")
406 .add_value("cpu_0", "type", "GenuineIntel Intel(R)")
408 .add_value("cpu_0", "state", "on-line")
409 .add_value("cpu_0", "clockSpeed", "2500.00 MHz")
410 .add_value("cpu_0", "percentUtilisation", "3.25 %")
411 .add_value("cpu_0", "percentUserTime", "1.95 %")
412 .add_value("cpu_0", "percentKernelTime", "1.30 %")
413 .add_value("cpu_0", "percentIdle", "96.75 %")
414 .add_value("cpu_1", "type", "GenuineIntel Intel(R)")
416 .add_value("cpu_1", "state", "on-line")
417 .add_value("cpu_1", "clockSpeed", "2500.00 MHz")
418 .add_value("cpu_1", "percentUtilisation", "4.25 %")
419 .add_value("cpu_1", "percentUserTime", "2.35 %")
420 .add_value("cpu_1", "percentKernelTime", "1.20 %")
421 .add_value("cpu_1", "percentIdle", "95.75 %")
422 .add_value("cpu_2", "type", "GenuineIntel, Intel(R)")
424 .add_value("cpu_2", "state", "on-line")
425 .add_value("cpu_2", "clockSpeed", "2,500.00 MHz")
426 .add_value("cpu_0_logical#1", "type", "logical")
428 .add_value("cpu_0_logical#1", "state", "on-line")
429 .add_value("cpu_0_logical#1", "clockSpeed", "2500.00 MHz")
430 .add_value("cpu_0_logical#1", "percentUtilisation", "2.54 %")
431 .build()?;
432
433 let output = dataview.to_string();
435
436 assert_eq!(dataview.row_order().len(), 5); assert_eq!(dataview.row_order()[0], "Average_cpu".to_string());
439 assert_eq!(dataview.row_order()[1], "cpu_0".to_string());
440 assert_eq!(dataview.row_order()[2], "cpu_1".to_string());
441 assert_eq!(dataview.row_order()[3], "cpu_2".to_string());
442 assert_eq!(dataview.row_order()[4], "cpu_0_logical#1".to_string());
443
444 assert_eq!(dataview.headlines.len(), 5); let expected_columns = [
448 "percentUtilisation",
449 "percentUserTime",
450 "percentKernelTime",
451 "percentIdle",
452 "type",
453 "state",
454 "clockSpeed",
455 ];
456 for (idx, col) in expected_columns.iter().enumerate() {
457 if idx < dataview.column_order().len() {
458 assert!(dataview.column_order().contains(&col.to_string()));
459 }
460 }
461
462 assert!(output.starts_with("cpu,"));
464 assert!(output.contains("<!>numOnlineCpus,4\n"));
465 assert!(output.contains("<!>loadAverage1Min,0.32\n"));
466 assert!(output.contains("<!>HyperThreadingStatus,ENABLED\n"));
467
468 assert!(output.contains("GenuineIntel\\, Intel(R)"));
470 assert!(output.contains("2\\,500.00 MHz"));
471
472 Ok(())
473 }
474
475 #[test]
476 fn test_error_conditions() -> Result<(), ()> {
477 let result = DataviewBuilder::new()
479 .add_value("row1", "col1", "value1")
480 .build();
481
482 assert!(matches!(result, Err(DataviewError::MissingRowHeader)));
483
484 let result = DataviewBuilder::new().set_row_header("header").build();
486
487 assert!(matches!(result, Err(DataviewError::MissingValue)));
488
489 let result = DataviewBuilder::new()
491 .set_row_header("header")
492 .add_headline("headline1", "value1")
493 .build();
494
495 assert!(matches!(result, Err(DataviewError::MissingValue)));
496
497 Ok(())
498 }
499}