use serde_json::{Map, Value};
pub fn is_full_snapshot_message(message: &Value) -> bool {
let print = message.get("print");
let is_push_status = print
.and_then(|p| p.get("command"))
.and_then(|v| v.as_str())
== Some("push_status");
let msg = print.and_then(|p| p.get("msg")).and_then(|v| v.as_i64());
is_push_status && msg.is_none_or(|m| m == 0)
}
pub fn merge_into(target: &mut Value, delta: &Value) {
if let (Value::Object(t), Value::Object(d)) = (&mut *target, delta) {
for (key, value) in d {
match t.get_mut(key) {
Some(existing) => merge_into(existing, value),
None => {
t.insert(key.clone(), value.clone());
}
}
}
} else {
*target = delta.clone();
}
}
type PrintIdentity = (Option<String>, Option<String>, Option<String>);
fn print_identity(state: &Value) -> PrintIdentity {
let field = |key: &str| {
state
.pointer(&format!("/print/{key}"))
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_owned)
};
(field("task_id"), field("subtask_id"), field("gcode_file"))
}
fn is_meaningful(id: &PrintIdentity) -> bool {
id.0.is_some() || id.1.is_some() || id.2.is_some()
}
#[derive(Debug, Clone)]
pub struct ReportState {
state: Value,
}
impl Default for ReportState {
fn default() -> Self {
Self::new()
}
}
impl ReportState {
pub fn new() -> Self {
Self {
state: Value::Object(Map::new()),
}
}
pub fn apply(&mut self, message: Value) {
let before = print_identity(&self.state);
merge_into(&mut self.state, &message);
let after = print_identity(&self.state);
if after != before
&& is_meaningful(&after)
&& let Some(print) = self.state.get_mut("print").and_then(Value::as_object_mut)
{
for field in ["mc_percent", "layer_num"] {
if message.pointer(&format!("/print/{field}")).is_none() {
print.insert(field.to_string(), Value::from(0));
}
}
}
}
pub fn get(&self) -> &Value {
&self.state
}
pub fn pointer(&self, pointer: &str) -> Option<&Value> {
self.state.pointer(pointer)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use serde_json::json;
#[test]
fn objects_merge_recursively_keeping_unmentioned_keys() {
let mut state = json!({ "print": { "a": 1, "b": 2, "nested": { "x": 1 } } });
merge_into(
&mut state,
&json!({ "print": { "b": 3, "c": 4, "nested": { "y": 2 } } }),
);
assert_eq!(
state,
json!({ "print": { "a": 1, "b": 3, "c": 4, "nested": { "x": 1, "y": 2 } } })
);
}
#[test]
fn scalars_are_replaced() {
let mut state = json!({ "temp": 200 });
merge_into(&mut state, &json!({ "temp": 215 }));
assert_eq!(state, json!({ "temp": 215 }));
}
#[test]
fn arrays_are_replaced_wholesale_not_element_merged() {
let mut state = json!({ "trays": [1, 2, 3] });
merge_into(&mut state, &json!({ "trays": [9] }));
assert_eq!(state, json!({ "trays": [9] }));
}
#[test]
fn null_replaces_value_but_keeps_the_key() {
let mut state = json!({ "k": 5 });
merge_into(&mut state, &json!({ "k": null }));
assert_eq!(state, json!({ "k": null }));
assert!(state.as_object().unwrap().contains_key("k"));
}
#[test]
fn a_new_print_zeroes_progress_carried_over_from_the_last_job() {
let mut rs = ReportState::new();
rs.apply(json!({ "print": {
"task_id": "A", "gcode_file": "a.3mf", "mc_percent": 100, "layer_num": 60,
}}));
rs.apply(json!({ "print": {
"task_id": "B", "gcode_file": "b.3mf", "gcode_state": "PREPARE",
}}));
let p = rs.pointer("/print").unwrap();
assert_eq!(
p.get("mc_percent"),
Some(&json!(0)),
"stale 100% must reset on a new print"
);
assert_eq!(p.get("layer_num"), Some(&json!(0)));
}
#[test]
fn a_progress_delta_within_the_same_print_is_kept() {
let mut rs = ReportState::new();
rs.apply(json!({ "print": { "task_id": "A", "mc_percent": 30, "layer_num": 5 } }));
rs.apply(json!({ "print": { "mc_percent": 31 } })); let p = rs.pointer("/print").unwrap();
assert_eq!(
p.get("mc_percent"),
Some(&json!(31)),
"same-print progress must not be zeroed"
);
assert_eq!(p.get("layer_num"), Some(&json!(5)));
}
#[test]
fn a_new_print_that_brings_its_own_percent_keeps_it() {
let mut rs = ReportState::new();
rs.apply(json!({ "print": { "task_id": "A", "mc_percent": 100 } }));
rs.apply(json!({ "print": { "task_id": "B", "mc_percent": 7 } }));
assert_eq!(
rs.pointer("/print/mc_percent"),
Some(&json!(7)),
"trust a fresh percent"
);
}
#[test]
fn finishing_a_print_keeps_its_final_progress() {
let mut rs = ReportState::new();
rs.apply(json!({ "print": { "task_id": "A", "mc_percent": 100, "layer_num": 60 } }));
rs.apply(json!({ "print": { "task_id": "", "gcode_state": "FINISH" } }));
assert_eq!(
rs.pointer("/print/mc_percent"),
Some(&json!(100)),
"a finished print keeps 100%"
);
}
#[test]
fn new_keys_are_added() {
let mut state = json!({ "a": 1 });
merge_into(&mut state, &json!({ "b": 2 }));
assert_eq!(state, json!({ "a": 1, "b": 2 }));
}
#[test]
fn last_writer_wins_so_a_stale_delta_reverts_overlapping_fields() {
let mut state = json!({ "layer": 0 });
merge_into(&mut state, &json!({ "layer": 10 })); merge_into(&mut state, &json!({ "layer": 5 })); assert_eq!(state, json!({ "layer": 5 }));
}
#[test]
fn report_state_seeds_then_merges_deltas() {
let mut rs = ReportState::new();
rs.apply(json!({ "print": { "gcode_state": "RUNNING", "layer_num": 1 } }));
rs.apply(json!({ "print": { "layer_num": 2 } })); assert_eq!(rs.pointer("/print/gcode_state"), Some(&json!("RUNNING")));
assert_eq!(rs.pointer("/print/layer_num"), Some(&json!(2)));
}
#[test]
fn full_snapshot_is_msg_zero_not_just_push_status() {
assert!(is_full_snapshot_message(
&json!({ "print": { "command": "push_status", "msg": 0 } })
));
assert!(!is_full_snapshot_message(
&json!({ "print": { "command": "push_status", "msg": 1 } })
));
assert!(is_full_snapshot_message(
&json!({ "print": { "command": "push_status" } })
));
assert!(!is_full_snapshot_message(
&json!({ "print": { "command": "gcode_line" } })
));
}
fn arb_json() -> impl Strategy<Value = Value> {
let leaf = prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Bool),
any::<i64>().prop_map(|n| json!(n)),
"[a-z0-9]{0,5}".prop_map(Value::String),
];
leaf.prop_recursive(4, 24, 6, |inner| {
prop_oneof![
prop::collection::vec(inner.clone(), 0..5).prop_map(Value::Array),
prop::collection::vec(("[a-z]{1,4}", inner), 0..5)
.prop_map(|kvs| Value::Object(kvs.into_iter().collect())),
]
})
}
fn arb_object() -> impl Strategy<Value = Value> {
prop::collection::vec(("[a-z]{1,4}", arb_json()), 0..6)
.prop_map(|kvs| Value::Object(kvs.into_iter().collect()))
}
fn prefix_keys(v: &Value, prefix: &str) -> Value {
match v {
Value::Object(m) => Value::Object(
m.iter()
.map(|(k, val)| (format!("{prefix}{k}"), val.clone()))
.collect(),
),
other => other.clone(),
}
}
proptest! {
#[test]
fn merge_is_idempotent(base in arb_object(), delta in arb_object()) {
let mut once = base.clone();
merge_into(&mut once, &delta);
let mut twice = once.clone();
merge_into(&mut twice, &delta);
prop_assert_eq!(once, twice);
}
#[test]
fn base_keys_absent_from_delta_are_retained(base in arb_object(), delta in arb_object()) {
let mut merged = base.clone();
merge_into(&mut merged, &delta);
if let (Value::Object(b), Value::Object(d), Value::Object(m)) = (&base, &delta, &merged) {
for (k, v) in b {
if !d.contains_key(k) {
prop_assert_eq!(m.get(k), Some(v));
}
}
}
}
#[test]
fn disjoint_deltas_commute(base in arb_object(), a in arb_object(), b in arb_object()) {
let a = prefix_keys(&a, "a_");
let b = prefix_keys(&b, "b_");
let mut ab = base.clone();
merge_into(&mut ab, &a);
merge_into(&mut ab, &b);
let mut ba = base.clone();
merge_into(&mut ba, &b);
merge_into(&mut ba, &a);
prop_assert_eq!(ab, ba);
}
}
}