use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(untagged)]
pub enum FileMapping {
Path(String),
Sum {
sum: Vec<Box<FileMapping>>,
},
Optional {
optional: Box<FileMapping>,
#[serde(default)]
dtype: Option<String>,
},
}
impl FileMapping {
pub fn apply_mapping(&self, json: &serde_json::Value) -> Result<serde_json::Value, String> {
match self {
FileMapping::Path(p) => {
let split_path = p.split('.').collect_vec();
traverse(json, &split_path)
}
FileMapping::Sum { sum } => {
let (ok, err): (Vec<_>, Vec<_>) =
sum.iter().map(|m| m.apply_mapping(json)).partition_result();
if !err.is_empty() {
let valid = if ok.is_empty() {
String::from("")
} else {
format!(" with these valid paths: {}", ok.iter().join(", "))
};
let invalid = err.join(", ");
let msg = format!("unable to sum invalid paths: {invalid}{valid}");
Err(msg)
} else {
let (nums, num_errs): (Vec<_>, Vec<_>) = ok
.iter()
.map(|v| match v {
serde_json::Value::Null => Ok(0.0),
serde_json::Value::Number(n) => {
n.as_f64().ok_or_else(|| format!("invalid number {v}"))
}
_ => Err(format!("expected a number, found {v}")),
})
.partition_result();
if !num_errs.is_empty() {
let valid = if nums.is_empty() {
String::from("")
} else {
format!(" with these valid numbers: {}", nums.iter().join(", "))
};
let invalid = num_errs.join(", ");
let msg = format!("unable to sum invalid numbers: {invalid}{valid}");
Err(msg)
} else {
Ok(json![nums.into_iter().sum::<f64>()])
}
}
}
FileMapping::Optional { optional, .. } => match optional.apply_mapping(json).ok() {
Some(value) => Ok(value),
None => Ok(serde_json::Value::Null),
},
}
}
}
fn traverse(value: &serde_json::Value, path: &Vec<&str>) -> Result<serde_json::Value, String> {
let mut cursor = value;
let mut remaining = path.as_slice();
while let Some(next) = remaining.first() {
match cursor.get(*next) {
None => {
let path_str = path.iter().join(".");
return Err(format!("could not find object {next} in path {path_str}"));
}
Some(child) => {
cursor = child;
remaining = &remaining[1..];
}
}
}
Ok(cursor.clone())
}