1use serde_json::{Map, Value};
33
34pub fn is_full_snapshot_message(message: &Value) -> bool {
43 let print = message.get("print");
44 let is_push_status = print
45 .and_then(|p| p.get("command"))
46 .and_then(|v| v.as_str())
47 == Some("push_status");
48 let msg = print.and_then(|p| p.get("msg")).and_then(|v| v.as_i64());
49 is_push_status && msg.is_none_or(|m| m == 0)
50}
51
52pub fn merge_into(target: &mut Value, delta: &Value) {
54 if let (Value::Object(t), Value::Object(d)) = (&mut *target, delta) {
55 for (key, value) in d {
56 match t.get_mut(key) {
57 Some(existing) => merge_into(existing, value),
58 None => {
59 t.insert(key.clone(), value.clone());
60 }
61 }
62 }
63 } else {
64 *target = delta.clone();
65 }
66}
67
68type PrintIdentity = (Option<String>, Option<String>, Option<String>);
72
73fn print_identity(state: &Value) -> PrintIdentity {
74 let field = |key: &str| {
75 state
76 .pointer(&format!("/print/{key}"))
77 .and_then(Value::as_str)
78 .filter(|s| !s.is_empty())
79 .map(str::to_owned)
80 };
81 (field("task_id"), field("subtask_id"), field("gcode_file"))
82}
83
84fn is_meaningful(id: &PrintIdentity) -> bool {
87 id.0.is_some() || id.1.is_some() || id.2.is_some()
88}
89
90#[derive(Debug, Clone)]
96pub struct ReportState {
97 state: Value,
98}
99
100impl Default for ReportState {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl ReportState {
107 pub fn new() -> Self {
109 Self {
110 state: Value::Object(Map::new()),
111 }
112 }
113
114 pub fn apply(&mut self, message: Value) {
123 let before = print_identity(&self.state);
124 merge_into(&mut self.state, &message);
125 let after = print_identity(&self.state);
126 if after != before
127 && is_meaningful(&after)
128 && let Some(print) = self.state.get_mut("print").and_then(Value::as_object_mut)
129 {
130 for field in ["mc_percent", "layer_num"] {
131 if message.pointer(&format!("/print/{field}")).is_none() {
132 print.insert(field.to_string(), Value::from(0));
133 }
134 }
135 }
136 }
137
138 pub fn get(&self) -> &Value {
140 &self.state
141 }
142
143 pub fn pointer(&self, pointer: &str) -> Option<&Value> {
145 self.state.pointer(pointer)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use proptest::prelude::*;
153 use serde_json::json;
154
155 #[test]
156 fn objects_merge_recursively_keeping_unmentioned_keys() {
157 let mut state = json!({ "print": { "a": 1, "b": 2, "nested": { "x": 1 } } });
158 merge_into(
159 &mut state,
160 &json!({ "print": { "b": 3, "c": 4, "nested": { "y": 2 } } }),
161 );
162 assert_eq!(
163 state,
164 json!({ "print": { "a": 1, "b": 3, "c": 4, "nested": { "x": 1, "y": 2 } } })
165 );
166 }
167
168 #[test]
169 fn scalars_are_replaced() {
170 let mut state = json!({ "temp": 200 });
171 merge_into(&mut state, &json!({ "temp": 215 }));
172 assert_eq!(state, json!({ "temp": 215 }));
173 }
174
175 #[test]
176 fn arrays_are_replaced_wholesale_not_element_merged() {
177 let mut state = json!({ "trays": [1, 2, 3] });
178 merge_into(&mut state, &json!({ "trays": [9] }));
179 assert_eq!(state, json!({ "trays": [9] }));
180 }
181
182 #[test]
183 fn null_replaces_value_but_keeps_the_key() {
184 let mut state = json!({ "k": 5 });
185 merge_into(&mut state, &json!({ "k": null }));
186 assert_eq!(state, json!({ "k": null }));
187 assert!(state.as_object().unwrap().contains_key("k"));
188 }
189
190 #[test]
197 fn a_new_print_zeroes_progress_carried_over_from_the_last_job() {
198 let mut rs = ReportState::new();
199 rs.apply(json!({ "print": {
200 "task_id": "A", "gcode_file": "a.3mf", "mc_percent": 100, "layer_num": 60,
201 }}));
202 rs.apply(json!({ "print": {
204 "task_id": "B", "gcode_file": "b.3mf", "gcode_state": "PREPARE",
205 }}));
206 let p = rs.pointer("/print").unwrap();
207 assert_eq!(
208 p.get("mc_percent"),
209 Some(&json!(0)),
210 "stale 100% must reset on a new print"
211 );
212 assert_eq!(p.get("layer_num"), Some(&json!(0)));
213 }
214
215 #[test]
216 fn a_progress_delta_within_the_same_print_is_kept() {
217 let mut rs = ReportState::new();
218 rs.apply(json!({ "print": { "task_id": "A", "mc_percent": 30, "layer_num": 5 } }));
219 rs.apply(json!({ "print": { "mc_percent": 31 } })); let p = rs.pointer("/print").unwrap();
221 assert_eq!(
222 p.get("mc_percent"),
223 Some(&json!(31)),
224 "same-print progress must not be zeroed"
225 );
226 assert_eq!(p.get("layer_num"), Some(&json!(5)));
227 }
228
229 #[test]
230 fn a_new_print_that_brings_its_own_percent_keeps_it() {
231 let mut rs = ReportState::new();
232 rs.apply(json!({ "print": { "task_id": "A", "mc_percent": 100 } }));
233 rs.apply(json!({ "print": { "task_id": "B", "mc_percent": 7 } }));
234 assert_eq!(
235 rs.pointer("/print/mc_percent"),
236 Some(&json!(7)),
237 "trust a fresh percent"
238 );
239 }
240
241 #[test]
242 fn finishing_a_print_keeps_its_final_progress() {
243 let mut rs = ReportState::new();
246 rs.apply(json!({ "print": { "task_id": "A", "mc_percent": 100, "layer_num": 60 } }));
247 rs.apply(json!({ "print": { "task_id": "", "gcode_state": "FINISH" } }));
248 assert_eq!(
249 rs.pointer("/print/mc_percent"),
250 Some(&json!(100)),
251 "a finished print keeps 100%"
252 );
253 }
254
255 #[test]
256 fn new_keys_are_added() {
257 let mut state = json!({ "a": 1 });
258 merge_into(&mut state, &json!({ "b": 2 }));
259 assert_eq!(state, json!({ "a": 1, "b": 2 }));
260 }
261
262 #[test]
263 fn last_writer_wins_so_a_stale_delta_reverts_overlapping_fields() {
264 let mut state = json!({ "layer": 0 });
268 merge_into(&mut state, &json!({ "layer": 10 })); merge_into(&mut state, &json!({ "layer": 5 })); assert_eq!(state, json!({ "layer": 5 }));
271 }
272
273 #[test]
274 fn report_state_seeds_then_merges_deltas() {
275 let mut rs = ReportState::new();
276 rs.apply(json!({ "print": { "gcode_state": "RUNNING", "layer_num": 1 } }));
277 rs.apply(json!({ "print": { "layer_num": 2 } })); assert_eq!(rs.pointer("/print/gcode_state"), Some(&json!("RUNNING")));
279 assert_eq!(rs.pointer("/print/layer_num"), Some(&json!(2)));
280 }
281
282 #[test]
283 fn full_snapshot_is_msg_zero_not_just_push_status() {
284 assert!(is_full_snapshot_message(
286 &json!({ "print": { "command": "push_status", "msg": 0 } })
287 ));
288 assert!(!is_full_snapshot_message(
290 &json!({ "print": { "command": "push_status", "msg": 1 } })
291 ));
292 assert!(is_full_snapshot_message(
294 &json!({ "print": { "command": "push_status" } })
295 ));
296 assert!(!is_full_snapshot_message(
298 &json!({ "print": { "command": "gcode_line" } })
299 ));
300 }
301
302 fn arb_json() -> impl Strategy<Value = Value> {
305 let leaf = prop_oneof![
306 Just(Value::Null),
307 any::<bool>().prop_map(Value::Bool),
308 any::<i64>().prop_map(|n| json!(n)),
309 "[a-z0-9]{0,5}".prop_map(Value::String),
310 ];
311 leaf.prop_recursive(4, 24, 6, |inner| {
312 prop_oneof![
313 prop::collection::vec(inner.clone(), 0..5).prop_map(Value::Array),
314 prop::collection::vec(("[a-z]{1,4}", inner), 0..5)
315 .prop_map(|kvs| Value::Object(kvs.into_iter().collect())),
316 ]
317 })
318 }
319
320 fn arb_object() -> impl Strategy<Value = Value> {
321 prop::collection::vec(("[a-z]{1,4}", arb_json()), 0..6)
322 .prop_map(|kvs| Value::Object(kvs.into_iter().collect()))
323 }
324
325 fn prefix_keys(v: &Value, prefix: &str) -> Value {
326 match v {
327 Value::Object(m) => Value::Object(
328 m.iter()
329 .map(|(k, val)| (format!("{prefix}{k}"), val.clone()))
330 .collect(),
331 ),
332 other => other.clone(),
333 }
334 }
335
336 proptest! {
337 #[test]
339 fn merge_is_idempotent(base in arb_object(), delta in arb_object()) {
340 let mut once = base.clone();
341 merge_into(&mut once, &delta);
342 let mut twice = once.clone();
343 merge_into(&mut twice, &delta);
344 prop_assert_eq!(once, twice);
345 }
346
347 #[test]
349 fn base_keys_absent_from_delta_are_retained(base in arb_object(), delta in arb_object()) {
350 let mut merged = base.clone();
351 merge_into(&mut merged, &delta);
352 if let (Value::Object(b), Value::Object(d), Value::Object(m)) = (&base, &delta, &merged) {
353 for (k, v) in b {
354 if !d.contains_key(k) {
355 prop_assert_eq!(m.get(k), Some(v));
356 }
357 }
358 }
359 }
360
361 #[test]
363 fn disjoint_deltas_commute(base in arb_object(), a in arb_object(), b in arb_object()) {
364 let a = prefix_keys(&a, "a_");
365 let b = prefix_keys(&b, "b_");
366
367 let mut ab = base.clone();
368 merge_into(&mut ab, &a);
369 merge_into(&mut ab, &b);
370
371 let mut ba = base.clone();
372 merge_into(&mut ba, &b);
373 merge_into(&mut ba, &a);
374
375 prop_assert_eq!(ab, ba);
376 }
377 }
378}