use serde_json::Value;
use std::collections::BTreeMap;
pub const BUILTIN_REDUCERS: &[&str] = &["lww", "append", "sum", "max", "min"];
pub fn is_builtin(name: &str) -> bool {
BUILTIN_REDUCERS.contains(&name)
}
pub fn reduce_with(name: &str, prev: Option<&Value>, new: &Value) -> Value {
match name {
"append" => {
let mut acc = match prev {
Some(Value::Array(a)) => a.clone(),
_ => Vec::new(),
};
match new {
Value::Array(items) => acc.extend(items.iter().cloned()),
other => acc.push(other.clone()),
}
Value::Array(acc)
}
"sum" => match (prev.and_then(Value::as_f64), new.as_f64()) {
(Some(p), Some(n)) => number(p + n),
(None, Some(n)) => number(n),
_ => new.clone(),
},
"max" => match (prev.and_then(Value::as_f64), new.as_f64()) {
(Some(p), Some(n)) => number(if n > p { n } else { p }),
(None, Some(n)) => number(n),
_ => new.clone(),
},
"min" => match (prev.and_then(Value::as_f64), new.as_f64()) {
(Some(p), Some(n)) => number(if n < p { n } else { p }),
(None, Some(n)) => number(n),
_ => new.clone(),
},
_ => new.clone(), }
}
fn number(f: f64) -> Value {
if f.fract() == 0.0 && f.abs() < 9_007_199_254_740_992.0 {
Value::from(f as i64)
} else {
serde_json::Number::from_f64(f).map(Value::Number).unwrap_or(Value::Null)
}
}
pub fn make_reduce(
table: &BTreeMap<String, String>,
) -> impl Fn(&str, Option<&Value>, &Value) -> Value + '_ {
move |key: &str, prev: Option<&Value>, new: &Value| {
let name = table.get(key).map(String::as_str).unwrap_or("lww");
reduce_with(name, prev, new)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn append_accumulates_in_merge_order() {
let a = reduce_with("append", None, &json!([1, 2]));
let b = reduce_with("append", Some(&a), &json!(3));
let c = reduce_with("append", Some(&b), &json!([4]));
assert_eq!(c, json!([1, 2, 3, 4]));
}
#[test]
fn numeric_reducers_fold() {
let s = reduce_with("sum", Some(&json!(2)), &json!(3));
assert_eq!(s, json!(5));
assert_eq!(reduce_with("max", Some(&json!(2)), &json!(3)), json!(3));
assert_eq!(reduce_with("min", Some(&json!(2)), &json!(3)), json!(2));
assert_eq!(s.to_string(), "5");
}
#[test]
fn non_numeric_operands_fall_back_deterministically() {
assert_eq!(reduce_with("sum", Some(&json!("x")), &json!(3)), json!(3));
assert_eq!(reduce_with("sum", Some(&json!(3)), &json!("x")), json!("x"));
}
#[test]
fn reducers_are_batching_invariant() {
let seqs: Vec<Vec<Value>> = vec![
vec![json!(1), json!(2.5), json!(3), json!(-7)],
vec![json!([1]), json!(2), json!([3, 4])],
vec![json!("a"), json!("b"), json!("c")],
];
for name in BUILTIN_REDUCERS {
for seq in &seqs {
let one_at_a_time = seq.iter().fold(None::<Value>, |acc, v| {
Some(reduce_with(name, acc.as_ref(), v))
});
for split in 1..seq.len() {
let (l, r) = seq.split_at(split);
let left = l.iter().fold(None::<Value>, |acc, v| {
Some(reduce_with(name, acc.as_ref(), v))
});
let both = r.iter().fold(left, |acc, v| {
Some(reduce_with(name, acc.as_ref(), v))
});
assert_eq!(
both, one_at_a_time,
"{name} not batching-invariant over {seq:?} at {split}"
);
}
}
}
}
}