use crate::iq::IqError;
use serde::Serialize;
use serde_json::Value;
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct BatchResult<T> {
pub index: usize,
pub input: String,
pub outcome: Result<T, String>,
}
fn to_value<T: Serialize>(v: &T) -> Result<Value, IqError> {
serde_json::to_value(v).map_err(|e| IqError::Format(format!("cannot serialise row: {e}")))
}
fn cell(v: &Value) -> String {
match v {
Value::Null => String::new(),
Value::String(s) => s.clone(),
Value::Bool(_) | Value::Number(_) => v.to_string(),
_ => v.to_string(),
}
}
fn quote(s: &str) -> String {
if s.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
}
fn csv_from_objects(rows: &[serde_json::Map<String, Value>], lead: &[&str]) -> String {
let mut cols: Vec<String> = lead.iter().map(|s| s.to_string()).collect();
let rest: std::collections::BTreeSet<&String> = rows
.iter()
.flat_map(|r| r.keys())
.filter(|k| !lead.contains(&k.as_str()))
.collect();
cols.extend(rest.into_iter().cloned());
let mut out = cols.iter().map(|c| quote(c)).collect::<Vec<_>>().join(",");
out.push('\n');
for r in rows {
let line = cols
.iter()
.map(|c| quote(&r.get(c).map(cell).unwrap_or_default()))
.collect::<Vec<_>>()
.join(",");
out.push_str(&line);
out.push('\n');
}
out
}
pub fn rows_to_csv<T: Serialize>(rows: &[T]) -> Result<String, IqError> {
let objs = rows
.iter()
.map(|r| {
Ok(match to_value(r)? {
Value::Object(m) => m,
v => {
let mut m = serde_json::Map::new();
m.insert("value".into(), v);
m
}
})
})
.collect::<Result<Vec<_>, IqError>>()?;
Ok(csv_from_objects(&objs, &[]))
}
pub fn rows_to_json<T: Serialize>(rows: &[T]) -> Result<String, IqError> {
serde_json::to_string_pretty(rows).map_err(|e| IqError::Format(e.to_string()))
}
fn result_object<T: Serialize>(
r: &BatchResult<T>,
) -> Result<serde_json::Map<String, Value>, IqError> {
let mut m = serde_json::Map::new();
m.insert("index".into(), Value::from(r.index));
m.insert("input".into(), Value::from(r.input.clone()));
m.insert("ok".into(), Value::from(r.outcome.is_ok()));
match &r.outcome {
Ok(v) => {
m.insert("error".into(), Value::Null);
match to_value(v)? {
Value::Object(o) => {
for (k, v) in o {
m.entry(format!("result.{k}")).or_insert(v);
}
}
v => {
m.insert("result".into(), v);
}
}
}
Err(e) => {
m.insert("error".into(), Value::from(e.clone()));
}
}
Ok(m)
}
pub fn results_to_csv<T: Serialize>(results: &[BatchResult<T>]) -> Result<String, IqError> {
let objs = results
.iter()
.map(result_object)
.collect::<Result<Vec<_>, _>>()?;
Ok(csv_from_objects(&objs, &["index", "input", "ok", "error"]))
}
pub fn results_to_json<T: Serialize>(results: &[BatchResult<T>]) -> Result<String, IqError> {
let arr = results
.iter()
.map(|r| {
let mut m = serde_json::Map::new();
m.insert("index".into(), Value::from(r.index));
m.insert("input".into(), Value::from(r.input.clone()));
m.insert("ok".into(), Value::from(r.outcome.is_ok()));
match &r.outcome {
Ok(v) => {
m.insert("error".into(), Value::Null);
m.insert("result".into(), to_value(v)?);
}
Err(e) => {
m.insert("error".into(), Value::from(e.clone()));
m.insert("result".into(), Value::Null);
}
}
Ok(Value::Object(m))
})
.collect::<Result<Vec<_>, IqError>>()?;
serde_json::to_string_pretty(&arr).map_err(|e| IqError::Format(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize)]
struct Row {
name: String,
n: u32,
}
#[test]
fn csv_quotes_per_rfc_4180_and_orders_columns() {
let rows = vec![
BatchResult {
index: 0,
input: "a,b".into(),
outcome: Ok(Row {
name: "say \"hi\"".into(),
n: 3,
}),
},
BatchResult {
index: 1,
input: "c".into(),
outcome: Err("broken".into()),
},
];
let csv = results_to_csv(&rows).unwrap();
let lines: Vec<&str> = csv.lines().collect();
assert_eq!(lines[0], "index,input,ok,error,result.n,result.name");
assert_eq!(lines[1], "0,\"a,b\",true,,3,\"say \"\"hi\"\"\"");
assert_eq!(lines[2], "1,c,false,broken,,");
let json: Value = serde_json::from_str(&results_to_json(&rows).unwrap()).unwrap();
assert_eq!(json[0]["result"]["n"], 3);
assert_eq!(json[1]["error"], "broken");
}
}