use crate::types::{PatchOp, PatchOpKind, SlopNode};
pub fn diff_nodes(old: &SlopNode, new: &SlopNode, base_path: &str) -> Vec<PatchOp> {
let mut ops = Vec::new();
diff_properties(old, new, base_path, &mut ops);
let old_aff = old.affordances.as_ref().map(|a| serde_json::to_value(a).unwrap());
let new_aff = new.affordances.as_ref().map(|a| serde_json::to_value(a).unwrap());
if old_aff != new_aff {
match (&old_aff, &new_aff) {
(_, Some(val)) => ops.push(PatchOp {
op: if old_aff.is_some() { PatchOpKind::Replace } else { PatchOpKind::Add },
path: format!("{base_path}/affordances"),
value: Some(val.clone()),
}),
(Some(_), None) => ops.push(PatchOp {
op: PatchOpKind::Remove,
path: format!("{base_path}/affordances"),
value: None,
}),
(None, None) => {}
}
}
let old_meta = old.meta.as_ref().map(|m| serde_json::to_value(m).unwrap());
let new_meta = new.meta.as_ref().map(|m| serde_json::to_value(m).unwrap());
if old_meta != new_meta {
match (&old_meta, &new_meta) {
(_, Some(val)) => ops.push(PatchOp {
op: if old_meta.is_some() { PatchOpKind::Replace } else { PatchOpKind::Add },
path: format!("{base_path}/meta"),
value: Some(val.clone()),
}),
(Some(_), None) => ops.push(PatchOp {
op: PatchOpKind::Remove,
path: format!("{base_path}/meta"),
value: None,
}),
(None, None) => {}
}
}
let old_children = old.children.as_deref().unwrap_or(&[]);
let new_children = new.children.as_deref().unwrap_or(&[]);
let old_ids: std::collections::HashMap<&str, &SlopNode> =
old_children.iter().map(|c| (c.id.as_str(), c)).collect();
let new_ids: std::collections::HashMap<&str, &SlopNode> =
new_children.iter().map(|c| (c.id.as_str(), c)).collect();
for child in old_children {
if !new_ids.contains_key(child.id.as_str()) {
ops.push(PatchOp {
op: PatchOpKind::Remove,
path: format!("{base_path}/{}", child.id),
value: None,
});
}
}
for child in new_children {
if !old_ids.contains_key(child.id.as_str()) {
ops.push(PatchOp {
op: PatchOpKind::Add,
path: format!("{base_path}/{}", child.id),
value: Some(serde_json::to_value(child).unwrap()),
});
}
}
for child in new_children {
if let Some(old_child) = old_ids.get(child.id.as_str()) {
let child_path = format!("{base_path}/{}", child.id);
ops.extend(diff_nodes(old_child, child, &child_path));
}
}
ops
}
fn diff_properties(old: &SlopNode, new: &SlopNode, base_path: &str, ops: &mut Vec<PatchOp>) {
let empty_map = serde_json::Map::new();
let old_props = old.properties.as_ref().unwrap_or(&empty_map);
let new_props = new.properties.as_ref().unwrap_or(&empty_map);
let mut all_keys: Vec<&String> = old_props.keys().chain(new_props.keys()).collect();
all_keys.sort();
all_keys.dedup();
for key in all_keys {
let old_val = old_props.get(key);
let new_val = new_props.get(key);
match (old_val, new_val) {
(None, Some(v)) => ops.push(PatchOp {
op: PatchOpKind::Add,
path: format!("{base_path}/properties/{key}"),
value: Some(v.clone()),
}),
(Some(_), None) => ops.push(PatchOp {
op: PatchOpKind::Remove,
path: format!("{base_path}/properties/{key}"),
value: None,
}),
(Some(old_v), Some(new_v)) if old_v != new_v => ops.push(PatchOp {
op: PatchOpKind::Replace,
path: format!("{base_path}/properties/{key}"),
value: Some(new_v.clone()),
}),
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Affordance, NodeMeta};
use serde_json::{json, Value};
fn node(id: &str) -> SlopNode {
SlopNode::new(id, "group")
}
fn node_with_props(id: &str, props: serde_json::Map<String, Value>) -> SlopNode {
SlopNode {
properties: Some(props),
..SlopNode::new(id, "group")
}
}
fn props(pairs: Vec<(&str, Value)>) -> serde_json::Map<String, Value> {
pairs
.into_iter()
.map(|(k, v): (&str, Value)| (k.to_string(), v))
.collect()
}
#[test]
fn test_no_changes() {
let n = node_with_props("x", props(vec![("a", json!(1))]));
let ops = diff_nodes(&n, &n, "");
assert!(ops.is_empty());
}
#[test]
fn test_property_added() {
let old = node_with_props("x", props(vec![("a", json!(1))]));
let new = node_with_props("x", props(vec![("a", json!(1)), ("b", json!(2))]));
let ops = diff_nodes(&old, &new, "");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].op, PatchOpKind::Add);
assert_eq!(ops[0].path, "/properties/b");
}
#[test]
fn test_property_removed() {
let old = node_with_props("x", props(vec![("a", json!(1)), ("b", json!(2))]));
let new = node_with_props("x", props(vec![("a", json!(1))]));
let ops = diff_nodes(&old, &new, "");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].op, PatchOpKind::Remove);
assert_eq!(ops[0].path, "/properties/b");
}
#[test]
fn test_property_changed() {
let old = node_with_props("x", props(vec![("a", json!(1))]));
let new = node_with_props("x", props(vec![("a", json!(2))]));
let ops = diff_nodes(&old, &new, "");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].op, PatchOpKind::Replace);
assert_eq!(ops[0].value, Some(json!(2)));
}
#[test]
fn test_child_added() {
let old = SlopNode { children: Some(vec![]), ..node("x") };
let child = node("c1");
let new = SlopNode { children: Some(vec![child]), ..node("x") };
let ops = diff_nodes(&old, &new, "");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].op, PatchOpKind::Add);
assert_eq!(ops[0].path, "/c1");
}
#[test]
fn test_child_removed() {
let child = node("c1");
let old = SlopNode { children: Some(vec![child]), ..node("x") };
let new = SlopNode { children: Some(vec![]), ..node("x") };
let ops = diff_nodes(&old, &new, "");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].op, PatchOpKind::Remove);
assert_eq!(ops[0].path, "/c1");
}
#[test]
fn test_nested_diff() {
let old = SlopNode {
children: Some(vec![SlopNode {
children: Some(vec![node_with_props("b", props(vec![("x", json!(1))]))]),
..node("a")
}]),
..node("root")
};
let new = SlopNode {
children: Some(vec![SlopNode {
children: Some(vec![node_with_props("b", props(vec![("x", json!(2))]))]),
..node("a")
}]),
..node("root")
};
let ops = diff_nodes(&old, &new, "");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].path, "/a/b/properties/x");
assert_eq!(ops[0].value, Some(json!(2)));
}
#[test]
fn test_meta_changed() {
let old = SlopNode {
meta: Some(NodeMeta { salience: Some(0.5), ..NodeMeta::new() }),
..node("x")
};
let new = SlopNode {
meta: Some(NodeMeta { salience: Some(0.9), ..NodeMeta::new() }),
..node("x")
};
let ops = diff_nodes(&old, &new, "");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].op, PatchOpKind::Replace);
assert_eq!(ops[0].path, "/meta");
}
#[test]
fn test_affordances_changed() {
let old = SlopNode {
affordances: Some(vec![Affordance::new("open")]),
..node("x")
};
let new = SlopNode {
affordances: Some(vec![Affordance::new("open"), Affordance::new("delete")]),
..node("x")
};
let ops = diff_nodes(&old, &new, "");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].op, PatchOpKind::Replace);
assert_eq!(ops[0].path, "/affordances");
}
}