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 values: HashMap<(String, String), String>,
45 columns: Vec<String>,
46 rows: Vec<String>,
47}
48
49impl Dataview {
50 pub fn row_header(&self) -> &str {
51 &self.row_header
52 }
53
54 pub fn headline(&self, key: &str) -> Option<&String> {
55 self.headlines.get(key)
56 }
57
58 pub fn value(&self, row: &str, column: &str) -> Option<&String> {
59 self.values.get(&(row.to_string(), column.to_string()))
60 }
61
62 pub fn columns(&self) -> &[String] {
63 &self.columns
64 }
65
66 pub fn rows(&self) -> &[String] {
67 &self.rows
68 }
69}
70
71fn escape_commas(s: &str) -> String {
72 s.replace(",", "\\,")
73}
74
75fn write_header_row(
76 f: &mut fmt::Formatter<'_>,
77 row_header: &str,
78 columns: &[String],
79) -> fmt::Result {
80 write!(f, "{}", escape_commas(row_header))?;
81 for col in columns {
82 write!(f, ",{}", escape_commas(col))?;
83 }
84 writeln!(f)
85}
86
87fn write_headlines(f: &mut fmt::Formatter<'_>, headlines: &HashMap<String, String>) -> fmt::Result {
88 for (name, value) in headlines {
89 writeln!(f, "<!>{},{}", escape_commas(name), escape_commas(value))?;
90 }
91 Ok(())
92}
93
94fn write_data_rows(
95 f: &mut fmt::Formatter<'_>,
96 rows: &[String],
97 columns: &[String],
98 values: &HashMap<(String, String), String>,
99) -> fmt::Result {
100 let number_of_rows = rows.len();
101 for (i, row) in rows.iter().enumerate() {
102 write!(f, "{}", escape_commas(row))?;
103 for col in columns {
104 write!(f, ",")?;
105 if let Some(value) = values.get(&(row.to_string(), col.to_string())) {
106 write!(f, "{}", escape_commas(value))?;
107 }
108 }
109
110 if i < number_of_rows - 1 {
112 writeln!(f)?;
113 }
114 }
115
116 Ok(())
117}
118
119impl fmt::Display for Dataview {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 write_header_row(f, &self.row_header, &self.columns)?;
122 write_headlines(f, &self.headlines)?;
123 write_data_rows(f, &self.rows, &self.columns, &self.values)
124 }
125}
126
127impl Dataview {
128 pub fn builder() -> DataviewBuilder {
143 DataviewBuilder::new()
144 }
145}
146
147#[derive(Debug, Default, Clone)]
149pub struct DataviewBuilder {
150 row_header: Option<String>,
151 headlines: Option<HashMap<String, String>>,
152 values: Option<HashMap<(String, String), String>>,
153 columns: Vec<String>, rows: Vec<String>, }
156
157impl DataviewBuilder {
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 pub fn set_row_header(mut self, row_header: impl ToString) -> Self {
163 self.row_header = Some(row_header.to_string());
164 self
165 }
166
167 pub fn add_headline<S: ToString>(mut self, key: S, value: S) -> Self {
168 let mut headlines: HashMap<String, String> = self.headlines.unwrap_or_default();
169 headlines.insert(key.to_string(), value.to_string());
170 self.headlines = Some(headlines);
171 self
172 }
173
174 pub fn add_value<S: ToString>(mut self, row: S, column: S, value: S) -> Self {
175 let mut values: HashMap<(String, String), String> = self.values.unwrap_or_default();
176
177 let column_string = column.to_string();
179 if !self.columns.contains(&column_string) {
180 self.columns.push(column_string.clone());
181 }
182
183 let row_string = row.to_string();
185 if !self.rows.contains(&row_string) {
186 self.rows.push(row_string.clone());
187 }
188
189 values.insert((row_string, column_string), value.to_string());
190 self.values = Some(values);
191 self
192 }
193
194 pub fn build(self) -> Result<Dataview, DataviewError> {
218 let row_header = self.row_header.ok_or(DataviewError::MissingRowHeader)?;
219
220 let values = self.values.ok_or(DataviewError::MissingValue)?;
221
222 Ok(Dataview {
223 row_header,
224 headlines: self.headlines.unwrap_or_default(),
225 values,
226 columns: self.columns,
227 rows: self.rows,
228 })
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use pretty_assertions::assert_eq;
236
237 fn create_basic_dataview() -> Result<Dataview, DataviewError> {
239 DataviewBuilder::new()
240 .set_row_header("ID")
241 .add_headline("AverageAge", "30")
242 .add_value("1", "Name", "Alice")
243 .add_value("1", "Age", "30")
244 .build()
245 }
246
247 #[test]
248 fn test_dataview_builder_single_row() -> Result<(), DataviewError> {
249 let dataview = create_basic_dataview()?;
250
251 assert_eq!(dataview.row_header(), "ID");
253
254 assert_eq!(dataview.headline("AverageAge"), Some(&"30".to_string()));
256
257 assert_eq!(dataview.value("1", "Name"), Some(&"Alice".to_string()));
259 assert_eq!(dataview.value("1", "Age"), Some(&"30".to_string()));
260
261 assert_eq!(dataview.rows().len(), 1);
263 assert_eq!(dataview.columns().len(), 2);
264 assert!(dataview.columns().contains(&"Name".to_string()));
265 assert!(dataview.columns().contains(&"Age".to_string()));
266
267 Ok(())
268 }
269
270 #[test]
271 fn test_dataview_display_format() -> Result<(), DataviewError> {
272 let dataview = create_basic_dataview()?;
274 assert_eq!(
275 dataview.to_string(),
276 "\
277ID,Name,Age
278<!>AverageAge,30
2791,Alice,30"
280 );
281
282 let multi_row_dataview = DataviewBuilder::new()
284 .set_row_header("id")
285 .add_headline("AlertDetails", "this is red alert")
286 .add_value("001", "name", "agila")
287 .add_value("001", "status", "up")
288 .add_value("001", "Value", "97")
289 .add_value("002", "name", "lawin")
290 .add_value("002", "status", "down")
291 .add_value("002", "Value", "85")
292 .build()?;
293
294 let expected_output = "\
295id,name,status,Value
296<!>AlertDetails,this is red alert
297001,agila,up,97
298002,lawin,down,85";
299
300 assert_eq!(multi_row_dataview.to_string(), expected_output);
301
302 Ok(())
303 }
304
305 #[test]
306 fn test_special_characters_escaping() -> Result<(), DataviewError> {
307 let dataview = DataviewBuilder::new()
309 .set_row_header("queue,id")
310 .add_value("queue3", "number,code", "7,331")
311 .add_value("queue3", "count", "45,000")
312 .add_value("queue3", "ratio", "0.16")
313 .add_value("queue3", "status", "online")
314 .build()?;
315
316 let expected_output = "\
317queue\\,id,number\\,code,count,ratio,status
318queue3,7\\,331,45\\,000,0.16,online";
319
320 assert_eq!(dataview.to_string(), expected_output);
321
322 let dataview_special = DataviewBuilder::new()
324 .set_row_header("special")
325 .add_headline("special,headline", "headline value with, comma")
326 .add_value("special_case", "state", "testing: \"quotes\" & <symbols>")
327 .add_value("special_case", "data", "multi-line\ntext")
328 .build()?;
329
330 let output = dataview_special.to_string();
331 assert!(output.contains("special"));
332 assert!(output.contains("<!>special\\,headline,headline value with\\, comma"));
333 assert!(output.contains("testing: \"quotes\" & <symbols>"));
334 assert!(output.contains("multi-line\ntext"));
335
336 Ok(())
337 }
338
339 #[test]
340 fn test_empty_and_missing_values() -> Result<(), DataviewError> {
341 let dataview = DataviewBuilder::new()
343 .set_row_header("item")
344 .add_value("item1", "col1", "value1")
345 .add_value("item1", "col2", "value2")
346 .add_value("item2", "col1", "value3")
347 .add_value("item3", "col3", "value4") .build()?;
350
351 let output = dataview.to_string();
352
353 assert!(output.contains("item1,value1,value2,"));
355 assert!(output.contains("item2,value3,,"));
356 assert!(output.contains("item3,,,value4"));
357
358 assert_eq!(dataview.value("item2", "col2"), None);
360 assert_eq!(dataview.value("nonexistent", "col1"), None);
361
362 Ok(())
363 }
364
365 #[test]
366 fn test_dataview_complex() -> Result<(), DataviewError> {
367 let dataview = DataviewBuilder::new()
369 .set_row_header("cpu")
370 .add_headline("numOnlineCpus", "4")
372 .add_headline("loadAverage1Min", "0.32")
373 .add_headline("loadAverage5Min", "0.45")
374 .add_headline("loadAverage15Min", "0.38")
375 .add_headline("HyperThreadingStatus", "ENABLED")
376 .add_value("Average_cpu", "percentUtilisation", "3.75 %")
378 .add_value("Average_cpu", "percentUserTime", "2.15 %")
379 .add_value("Average_cpu", "percentKernelTime", "1.25 %")
380 .add_value("Average_cpu", "percentIdle", "96.25 %")
381 .add_value("cpu_0", "type", "GenuineIntel Intel(R)")
383 .add_value("cpu_0", "state", "on-line")
384 .add_value("cpu_0", "clockSpeed", "2500.00 MHz")
385 .add_value("cpu_0", "percentUtilisation", "3.25 %")
386 .add_value("cpu_0", "percentUserTime", "1.95 %")
387 .add_value("cpu_0", "percentKernelTime", "1.30 %")
388 .add_value("cpu_0", "percentIdle", "96.75 %")
389 .add_value("cpu_1", "type", "GenuineIntel Intel(R)")
391 .add_value("cpu_1", "state", "on-line")
392 .add_value("cpu_1", "clockSpeed", "2500.00 MHz")
393 .add_value("cpu_1", "percentUtilisation", "4.25 %")
394 .add_value("cpu_1", "percentUserTime", "2.35 %")
395 .add_value("cpu_1", "percentKernelTime", "1.20 %")
396 .add_value("cpu_1", "percentIdle", "95.75 %")
397 .add_value("cpu_2", "type", "GenuineIntel, Intel(R)")
399 .add_value("cpu_2", "state", "on-line")
400 .add_value("cpu_2", "clockSpeed", "2,500.00 MHz")
401 .add_value("cpu_0_logical#1", "type", "logical")
403 .add_value("cpu_0_logical#1", "state", "on-line")
404 .add_value("cpu_0_logical#1", "clockSpeed", "2500.00 MHz")
405 .add_value("cpu_0_logical#1", "percentUtilisation", "2.54 %")
406 .build()?;
407
408 let output = dataview.to_string();
410
411 assert_eq!(dataview.rows().len(), 5); assert!(dataview.rows().contains(&"Average_cpu".to_string()));
414 assert!(dataview.rows().contains(&"cpu_0".to_string()));
415 assert!(dataview.rows().contains(&"cpu_1".to_string()));
416 assert!(dataview.rows().contains(&"cpu_2".to_string()));
417 assert!(dataview.rows().contains(&"cpu_0_logical#1".to_string()));
418
419 assert_eq!(dataview.headlines.len(), 5); let expected_columns = [
423 "percentUtilisation",
424 "percentUserTime",
425 "percentKernelTime",
426 "percentIdle",
427 "type",
428 "state",
429 "clockSpeed",
430 ];
431 for (idx, col) in expected_columns.iter().enumerate() {
432 if idx < dataview.columns().len() {
433 assert!(dataview.columns().contains(&col.to_string()));
434 }
435 }
436
437 assert!(output.starts_with("cpu,"));
439 assert!(output.contains("<!>numOnlineCpus,4\n"));
440 assert!(output.contains("<!>loadAverage1Min,0.32\n"));
441 assert!(output.contains("<!>HyperThreadingStatus,ENABLED\n"));
442
443 assert!(output.contains("GenuineIntel\\, Intel(R)"));
445 assert!(output.contains("2\\,500.00 MHz"));
446
447 Ok(())
448 }
449
450 #[test]
451 fn test_error_conditions() -> Result<(), ()> {
452 let result = DataviewBuilder::new()
454 .add_value("row1", "col1", "value1")
455 .build();
456
457 assert!(matches!(result, Err(DataviewError::MissingRowHeader)));
458
459 let result = DataviewBuilder::new().set_row_header("header").build();
461
462 assert!(matches!(result, Err(DataviewError::MissingValue)));
463
464 let result = DataviewBuilder::new()
466 .set_row_header("header")
467 .add_headline("headline1", "value1")
468 .build();
469
470 assert!(matches!(result, Err(DataviewError::MissingValue)));
471
472 Ok(())
473 }
474}