fakecloud_iotdata/
shared.rs1use serde_json::{json, Map, Value};
9
10pub fn now_secs() -> i64 {
13 chrono::Utc::now().timestamp()
14}
15
16pub fn now_millis() -> i64 {
19 chrono::Utc::now().timestamp_millis()
20}
21
22pub fn merge_into(target: &mut Value, patch: &Value) {
26 let (Some(target_obj), Some(patch_obj)) = (target.as_object_mut(), patch.as_object()) else {
27 *target = patch.clone();
29 return;
30 };
31 for (k, v) in patch_obj {
32 if v.is_null() {
33 target_obj.remove(k);
34 } else if v.is_object() && target_obj.get(k).map(Value::is_object).unwrap_or(false) {
35 merge_into(target_obj.get_mut(k).expect("present"), v);
36 } else {
37 target_obj.insert(k.clone(), v.clone());
38 }
39 }
40}
41
42pub fn compute_delta(desired: &Value, reported: &Value) -> Option<Value> {
46 let desired_obj = desired.as_object()?;
47 let empty = Map::new();
48 let reported_obj = reported.as_object().unwrap_or(&empty);
49 let mut out = Map::new();
50 for (k, dv) in desired_obj {
51 match reported_obj.get(k) {
52 None => {
53 out.insert(k.clone(), dv.clone());
54 }
55 Some(rv) => {
56 if dv.is_object() && rv.is_object() {
57 if let Some(sub) = compute_delta(dv, rv) {
58 out.insert(k.clone(), sub);
59 }
60 } else if dv != rv {
61 out.insert(k.clone(), dv.clone());
62 }
63 }
64 }
65 }
66 if out.is_empty() {
67 None
68 } else {
69 Some(Value::Object(out))
70 }
71}
72
73pub fn stamp_metadata(meta: &mut Value, patch: &Value, ts: i64) {
78 let Some(patch_obj) = patch.as_object() else {
79 return;
80 };
81 if !meta.is_object() {
82 *meta = json!({});
83 }
84 let meta_obj = meta.as_object_mut().expect("meta is object");
85 for (k, v) in patch_obj {
86 if v.is_null() {
87 meta_obj.remove(k);
88 } else if v.is_object() {
89 let entry = meta_obj.entry(k.clone()).or_insert_with(|| json!({}));
90 stamp_metadata(entry, v, ts);
91 } else {
92 meta_obj.insert(k.clone(), metadata_leaf(v, ts));
93 }
94 }
95}
96
97fn metadata_leaf(v: &Value, ts: i64) -> Value {
100 match v {
101 Value::Object(_) => {
102 let mut m = json!({});
103 stamp_metadata(&mut m, v, ts);
104 m
105 }
106 Value::Array(items) => Value::Array(items.iter().map(|it| metadata_leaf(it, ts)).collect()),
107 _ => json!({ "timestamp": ts }),
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn merge_overwrites_and_recurses() {
117 let mut t = json!({"a": 1, "nested": {"x": 1, "y": 2}});
118 merge_into(&mut t, &json!({"a": 5, "nested": {"y": 9, "z": 3}}));
119 assert_eq!(t, json!({"a": 5, "nested": {"x": 1, "y": 9, "z": 3}}));
120 }
121
122 #[test]
123 fn merge_null_deletes_leaf() {
124 let mut t = json!({"a": 1, "b": 2});
125 merge_into(&mut t, &json!({"b": null}));
126 assert_eq!(t, json!({"a": 1}));
127 }
128
129 #[test]
130 fn delta_is_desired_minus_reported() {
131 let d = json!({"color": "red", "power": "on", "nested": {"a": 1}});
132 let r = json!({"color": "blue", "power": "on", "nested": {"a": 1}});
133 let delta = compute_delta(&d, &r).unwrap();
134 assert_eq!(delta, json!({"color": "red"}));
135 }
136
137 #[test]
138 fn delta_none_when_equal() {
139 let d = json!({"color": "red"});
140 assert!(compute_delta(&d, &d).is_none());
141 }
142
143 #[test]
144 fn delta_includes_missing_reported_key() {
145 let delta = compute_delta(&json!({"a": 1, "b": 2}), &json!({"a": 1})).unwrap();
146 assert_eq!(delta, json!({"b": 2}));
147 }
148
149 #[test]
150 fn metadata_stamped_at_leaves() {
151 let mut meta = json!({});
152 stamp_metadata(
153 &mut meta,
154 &json!({"color": "red", "nested": {"a": 1}}),
155 1000,
156 );
157 assert_eq!(meta["color"], json!({"timestamp": 1000}));
158 assert_eq!(meta["nested"]["a"], json!({"timestamp": 1000}));
159 }
160
161 #[test]
162 fn metadata_null_removes_leaf() {
163 let mut meta = json!({"color": {"timestamp": 1}, "power": {"timestamp": 1}});
164 stamp_metadata(&mut meta, &json!({"power": null}), 2000);
165 assert!(meta.get("power").is_none());
166 assert_eq!(meta["color"], json!({"timestamp": 1}));
167 }
168}