use std::collections::{HashMap, HashSet};
use serde_json::{Map, Value};
pub const DELTA_SHARE_CEILING: f64 = 0.5;
fn nodes_of(tree: &Value) -> &[Value] {
tree.get("nodes")
.and_then(Value::as_array)
.map_or(&[], Vec::as_slice)
}
fn id_of(node: &Value) -> &str {
node.get("id").and_then(Value::as_str).unwrap_or_default()
}
pub fn build_delta(base: &Value, next: &Value) -> Option<Value> {
let (changed, removed, root_ids, cursor_changed) = diff_trees(base, next);
let count = nodes_of(next).len().max(1);
if changed.len() as f64 > count as f64 * DELTA_SHARE_CEILING {
return None;
}
if base.get("cursor").is_some() && next.get("cursor").is_none() {
return None;
}
let mut delta = Map::new();
delta.insert("type".into(), Value::from("tree-delta"));
delta.insert("baseRevision".into(), base.get("revision").cloned()?);
delta.insert("revision".into(), next.get("revision").cloned()?);
delta.insert("changed".into(), Value::Array(changed));
delta.insert("removed".into(), Value::Array(removed));
if let Some(root_ids) = root_ids {
delta.insert("rootIds".into(), Value::Array(root_ids));
}
if cursor_changed {
if let Some(cursor) = next.get("cursor") {
delta.insert("cursor".into(), cursor.clone());
}
}
Some(Value::Object(delta))
}
pub fn diff_trees(
base: &Value,
next: &Value,
) -> (Vec<Value>, Vec<Value>, Option<Vec<Value>>, bool) {
let base_nodes = nodes_of(base);
let next_nodes = nodes_of(next);
let mut base_by_id: HashMap<&str, &Value> = HashMap::with_capacity(base_nodes.len());
let mut children_of: HashMap<&str, Vec<&str>> = HashMap::new();
for node in base_nodes {
base_by_id.insert(id_of(node), node);
if let Some(parent) = node.get("parentId").and_then(Value::as_str) {
children_of.entry(parent).or_default().push(id_of(node));
}
}
let next_by_id: HashMap<&str, &Value> =
next_nodes.iter().map(|node| (id_of(node), node)).collect();
let gone: HashSet<&str> = base_by_id
.keys()
.copied()
.filter(|id| !next_by_id.contains_key(id))
.collect();
let mut removal_roots: Vec<&str> = gone
.iter()
.copied()
.filter(
|id| match base_by_id[id].get("parentId").and_then(Value::as_str) {
Some(parent) => !gone.contains(parent),
None => true,
},
)
.collect();
removal_roots.sort_unstable();
let mut swept: HashSet<&str> = HashSet::new();
let mut pending: Vec<&str> = removal_roots.clone();
while let Some(current) = pending.pop() {
if !swept.insert(current) {
continue;
}
if let Some(children) = children_of.get(current) {
pending.extend(children.iter().copied());
}
}
let changed: Vec<Value> = next_nodes
.iter()
.filter(|node| {
let id = id_of(node);
match base_by_id.get(id) {
None => true,
Some(previous) => swept.contains(id) || *previous != *node,
}
})
.cloned()
.collect();
let removed: Vec<Value> = removal_roots.iter().map(|id| Value::from(*id)).collect();
let survivors: HashSet<&str> = base_by_id
.keys()
.copied()
.filter(|id| !swept.contains(id))
.chain(next_by_id.keys().copied())
.collect();
let inherited: Vec<&str> =
base.get("rootIds")
.and_then(Value::as_array)
.map_or(Vec::new(), |roots| {
roots
.iter()
.filter_map(Value::as_str)
.filter(|id| survivors.contains(id))
.collect()
});
let wanted: Vec<&str> = next
.get("rootIds")
.and_then(Value::as_array)
.map_or(Vec::new(), |roots| {
roots.iter().filter_map(Value::as_str).collect()
});
let root_ids = if inherited == wanted {
None
} else {
Some(wanted.iter().map(|id| Value::from(*id)).collect())
};
let cursor_changed = base.get("cursor") != next.get("cursor");
(changed, removed, root_ids, cursor_changed)
}