use std::collections::BTreeMap;
use super::{ProgramError, Value};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Data {
vars: BTreeMap<String, Value>,
}
impl Data {
pub fn new() -> Data {
Data::default()
}
pub fn from_json(json: &str) -> Result<Data, ProgramError> {
let trimmed = json.trim();
let mut data = Data::new();
if trimmed.is_empty() || trimmed == "null" {
return Ok(data);
}
let parsed: serde_json::Value = serde_json::from_str(trimmed)
.map_err(|e| ProgramError::Data(format!("data is not valid JSON: {e}")))?;
match parsed {
serde_json::Value::Array(items) => {
data.vars.insert("data".to_string(), array("data", &items)?);
}
serde_json::Value::Object(map) => {
for (name, v) in map {
let value = match &v {
serde_json::Value::Array(items) => array(&name, items)?,
serde_json::Value::Bool(b) => Value::Bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Int(i)
} else if let Some(u) = n.as_u64() {
Value::U64(u)
} else {
Value::F64(n.as_f64().unwrap_or(f64::NAN))
}
}
_ => {
return Err(ProgramError::Data(format!(
"data `{name}` must be a number, a boolean, or an array of them"
)))
}
};
data.vars.insert(name, value);
}
}
_ => {
return Err(ProgramError::Data(
"data must be a JSON object (of arrays or scalars) or a bare array".to_string(),
))
}
}
Ok(data)
}
pub fn with(mut self, name: impl Into<String>, value: impl Into<Value>) -> Data {
self.insert(name, value);
self
}
pub fn insert(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
self.vars.insert(name.into(), value.into())
}
pub fn get(&self, name: &str) -> Option<&Value> {
self.vars.get(name)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
self.vars.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn len(&self) -> usize {
self.vars.len()
}
pub fn is_empty(&self) -> bool {
self.vars.is_empty()
}
}
fn array(name: &str, items: &[serde_json::Value]) -> Result<Value, ProgramError> {
items
.iter()
.map(|x| {
x.as_f64()
.or(x.as_bool().map(|b| if b { 1.0 } else { 0.0 }))
.ok_or_else(|| {
ProgramError::Data(format!("data array `{name}` must hold numbers or booleans"))
})
})
.collect::<Result<Vec<f64>, _>>()
.map(Value::from)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_shapes() {
assert!(Data::from_json("").unwrap().is_empty());
assert!(Data::from_json(" null ").unwrap().is_empty());
let d = Data::from_json(r#"{"x": [1, true, 2.5], "k": 18446744073709551615, "f": 0.5}"#)
.unwrap();
assert_eq!(d.get("x"), Some(&Value::from(vec![1.0, 1.0, 2.5])));
assert_eq!(d.get("k"), Some(&Value::U64(u64::MAX)));
assert_eq!(d.get("f"), Some(&Value::F64(0.5)));
assert_eq!(d.len(), 3);
assert_eq!(
d.iter().map(|(k, _)| k).collect::<Vec<_>>(),
["f", "k", "x"]
);
for bad in [
"{",
"3",
r#"{"x": "s"}"#,
r#"{"x": [null]}"#,
r#"{"x": [[1]]}"#,
] {
assert!(
matches!(Data::from_json(bad), Err(ProgramError::Data(_))),
"{bad}"
);
}
}
}