#![forbid(unsafe_code)]
use crate::models::DataPoint;
use anyhow::Result;
use csv::WriterBuilder;
use serde::Serialize;
use std::borrow::Cow;
use std::path::Path;
use tempfile::NamedTempFile;
fn csv_safe_cell(s: &str) -> Cow<'_, str> {
match s.as_bytes().first() {
Some(b'=' | b'+' | b'-' | b'@') => {
let mut t = String::with_capacity(s.len() + 1);
t.push('\'');
t.push_str(s);
Cow::Owned(t)
}
_ => Cow::Borrowed(s),
}
}
fn finite_or_none(x: Option<f64>) -> Option<f64> {
match x {
Some(v) if v.is_finite() => Some(v),
_ => None,
}
}
pub fn save_csv<P: AsRef<Path>>(points: &[DataPoint], path: P) -> Result<()> {
let path = path.as_ref();
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let mut tmp = NamedTempFile::new_in(parent)?;
{
let mut wtr = WriterBuilder::new().from_writer(tmp.as_file_mut());
wtr.serialize((
"indicator_id",
"indicator_name",
"country_id",
"country_name",
"country_iso3",
"year",
"value",
"unit",
"obs_status",
"decimal",
))?;
for p in points {
let indicator_id = csv_safe_cell(&p.indicator_id);
let indicator_name = csv_safe_cell(&p.indicator_name);
let country_id = csv_safe_cell(&p.country_id);
let country_name = csv_safe_cell(&p.country_name);
let country_iso3 = csv_safe_cell(&p.country_iso3);
let unit: Option<String> = p.unit.as_deref().map(|s| csv_safe_cell(s).into_owned());
let obs_status: Option<String> = p
.obs_status
.as_deref()
.map(|s| csv_safe_cell(s).into_owned());
wtr.serialize((
indicator_id.as_ref(),
indicator_name.as_ref(),
country_id.as_ref(),
country_name.as_ref(),
country_iso3.as_ref(),
p.year, p.value, &unit, &obs_status, &p.decimal, ))?;
}
wtr.flush()?;
}
tmp.persist(path)?;
Ok(())
}
pub fn save_json<P: AsRef<Path>>(points: &[DataPoint], path: P) -> Result<()> {
let path = path.as_ref();
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let mut tmp = NamedTempFile::new_in(parent)?;
#[derive(Serialize)]
struct DataPointOut<'a> {
indicator_id: &'a str,
indicator_name: &'a str,
country_id: &'a str,
country_name: &'a str,
country_iso3: &'a str,
year: i32,
value: Option<f64>, unit: Option<&'a str>,
obs_status: Option<&'a str>,
decimal: Option<i64>, }
let out: Vec<DataPointOut<'_>> = points
.iter()
.map(|p| DataPointOut {
indicator_id: &p.indicator_id,
indicator_name: &p.indicator_name,
country_id: &p.country_id,
country_name: &p.country_name,
country_iso3: &p.country_iso3,
year: p.year,
value: finite_or_none(p.value),
unit: p.unit.as_deref(),
obs_status: p.obs_status.as_deref(),
decimal: p.decimal.map(|d| d as i64),
})
.collect();
{
let file = tmp.as_file_mut();
serde_json::to_writer_pretty(file, &out)?;
}
tmp.persist(path)?;
Ok(())
}