poop-graph/src/serialize.rs

145 lines
4.3 KiB
Rust

use std::{fmt::Write, marker::PhantomData, sync::Arc};
use crate::extract::{DataPoint, DataSet};
const DATE_DISPLAY_FORMAT: &[time::format_description::FormatItem] =
time::macros::format_description!("[year]-[month]-[day]");
type SerializationChunk = Result<String, std::fmt::Error>;
pub trait DataFormat {
fn header(dataset: &DataSet) -> SerializationChunk;
fn row(dataset: &DataSet, row: &DataPoint) -> SerializationChunk;
const ROW_SEPARATOR: &'static str;
const END: &'static str;
}
pub struct DataSerializer<F: DataFormat> {
dataset: Arc<DataSet>,
index: Option<usize>,
serializer: PhantomData<F>,
}
impl<F: DataFormat> DataSerializer<F> {
pub fn new(dataset: Arc<DataSet>, _: F) -> Self {
Self {
dataset,
index: None,
serializer: PhantomData,
}
}
}
impl<F: DataFormat> Iterator for DataSerializer<F> {
type Item = SerializationChunk;
fn next(&mut self) -> Option<Self::Item> {
match self.index {
None => {
self.index = Some(0);
let header = F::header(&self.dataset);
if self.dataset.rows.is_empty() {
Some(header.map(|s| s + F::END))
} else {
Some(header)
}
}
Some(i) => {
if let Some(row) = self.dataset.rows.get(i) {
self.index = Some(i + 1);
let serialized_row = F::row(&self.dataset, row);
let suffix = if i == self.dataset.rows.len() - 1 {
F::END
} else {
F::ROW_SEPARATOR
};
Some(serialized_row.map(|s| s + suffix))
} else {
None
}
}
}
}
}
pub struct Csv;
impl DataFormat for Csv {
fn header(dataset: &DataSet) -> SerializationChunk {
let mut header = String::from("Date");
for column in dataset.columns.iter() {
write!(&mut header, r#","{}""#, column.replace('"', r#""""#))?;
}
writeln!(&mut header)?;
Ok(header)
}
fn row(dataset: &DataSet, datapoint: &DataPoint) -> SerializationChunk {
let mut csv_row = datapoint
.date
.format(DATE_DISPLAY_FORMAT)
.expect("Failed to format date!");
for column in dataset.columns.iter() {
if let Some(val) = datapoint.values.get(column) {
write!(&mut csv_row, ",{}", val)?;
} else {
write!(&mut csv_row, ",")?;
}
}
writeln!(&mut csv_row)?;
Ok(csv_row)
}
const ROW_SEPARATOR: &'static str = "";
const END: &'static str = "";
}
pub struct Json;
impl DataFormat for Json {
fn header(dataset: &DataSet) -> SerializationChunk {
let mut header = String::from(r#"{"columns":["Date""#);
for column in dataset.columns.iter() {
write!(&mut header, ",{}", escaped_json_string(column.as_str()))?;
}
write!(&mut header, r#"],"rows":["#)?;
Ok(header)
}
fn row(dataset: &DataSet, datapoint: &DataPoint) -> SerializationChunk {
let mut row = String::from(r#"{"Date":"#);
write!(
&mut row,
r#""{}""#,
datapoint
.date
.format(DATE_DISPLAY_FORMAT)
.expect("Failed to format date!")
)?;
for column in dataset.columns.iter() {
if let Some(val) = datapoint.values.get(column) {
write!(&mut row, ",{}:{}", escaped_json_string(column.as_str()), val)?;
}
}
row += "}";
Ok(row)
}
const ROW_SEPARATOR: &'static str = ",";
const END: &'static str = "]}";
}
fn escaped_json_string(s: &str) -> String {
s.chars().fold(String::from(r#"""#), |mut acc, c| {
match c {
'"' => acc.push_str(r#"\""#),
'\\' => acc.push_str(r"\\"),
'\t' => acc.push_str(r"\t"),
'\n' => acc.push_str(r"\n"),
_ => if c.is_ascii_control() {
acc.push_str(&format!(r"\u{:04X}", c as u32))
} else {
acc.push(c)
}
}
acc
}) + r#"""#
}