use serde_json::{json, Map, Value};
pub fn now_secs() -> i64 {
chrono::Utc::now().timestamp()
}
pub fn now_millis() -> i64 {
chrono::Utc::now().timestamp_millis()
}
pub fn merge_into(target: &mut Value, patch: &Value) {
let (Some(target_obj), Some(patch_obj)) = (target.as_object_mut(), patch.as_object()) else {
*target = patch.clone();
return;
};
for (k, v) in patch_obj {
if v.is_null() {
target_obj.remove(k);
} else if v.is_object() && target_obj.get(k).map(Value::is_object).unwrap_or(false) {
merge_into(target_obj.get_mut(k).expect("present"), v);
} else {
target_obj.insert(k.clone(), v.clone());
}
}
}
pub fn compute_delta(desired: &Value, reported: &Value) -> Option<Value> {
let desired_obj = desired.as_object()?;
let empty = Map::new();
let reported_obj = reported.as_object().unwrap_or(&empty);
let mut out = Map::new();
for (k, dv) in desired_obj {
match reported_obj.get(k) {
None => {
out.insert(k.clone(), dv.clone());
}
Some(rv) => {
if dv.is_object() && rv.is_object() {
if let Some(sub) = compute_delta(dv, rv) {
out.insert(k.clone(), sub);
}
} else if dv != rv {
out.insert(k.clone(), dv.clone());
}
}
}
}
if out.is_empty() {
None
} else {
Some(Value::Object(out))
}
}
pub fn stamp_metadata(meta: &mut Value, patch: &Value, ts: i64) {
let Some(patch_obj) = patch.as_object() else {
return;
};
if !meta.is_object() {
*meta = json!({});
}
let meta_obj = meta.as_object_mut().expect("meta is object");
for (k, v) in patch_obj {
if v.is_null() {
meta_obj.remove(k);
} else if v.is_object() {
let entry = meta_obj.entry(k.clone()).or_insert_with(|| json!({}));
stamp_metadata(entry, v, ts);
} else {
meta_obj.insert(k.clone(), metadata_leaf(v, ts));
}
}
}
fn metadata_leaf(v: &Value, ts: i64) -> Value {
match v {
Value::Object(_) => {
let mut m = json!({});
stamp_metadata(&mut m, v, ts);
m
}
Value::Array(items) => Value::Array(items.iter().map(|it| metadata_leaf(it, ts)).collect()),
_ => json!({ "timestamp": ts }),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_overwrites_and_recurses() {
let mut t = json!({"a": 1, "nested": {"x": 1, "y": 2}});
merge_into(&mut t, &json!({"a": 5, "nested": {"y": 9, "z": 3}}));
assert_eq!(t, json!({"a": 5, "nested": {"x": 1, "y": 9, "z": 3}}));
}
#[test]
fn merge_null_deletes_leaf() {
let mut t = json!({"a": 1, "b": 2});
merge_into(&mut t, &json!({"b": null}));
assert_eq!(t, json!({"a": 1}));
}
#[test]
fn delta_is_desired_minus_reported() {
let d = json!({"color": "red", "power": "on", "nested": {"a": 1}});
let r = json!({"color": "blue", "power": "on", "nested": {"a": 1}});
let delta = compute_delta(&d, &r).unwrap();
assert_eq!(delta, json!({"color": "red"}));
}
#[test]
fn delta_none_when_equal() {
let d = json!({"color": "red"});
assert!(compute_delta(&d, &d).is_none());
}
#[test]
fn delta_includes_missing_reported_key() {
let delta = compute_delta(&json!({"a": 1, "b": 2}), &json!({"a": 1})).unwrap();
assert_eq!(delta, json!({"b": 2}));
}
#[test]
fn metadata_stamped_at_leaves() {
let mut meta = json!({});
stamp_metadata(
&mut meta,
&json!({"color": "red", "nested": {"a": 1}}),
1000,
);
assert_eq!(meta["color"], json!({"timestamp": 1000}));
assert_eq!(meta["nested"]["a"], json!({"timestamp": 1000}));
}
#[test]
fn metadata_null_removes_leaf() {
let mut meta = json!({"color": {"timestamp": 1}, "power": {"timestamp": 1}});
stamp_metadata(&mut meta, &json!({"power": null}), 2000);
assert!(meta.get("power").is_none());
assert_eq!(meta["color"], json!({"timestamp": 1}));
}
}