use crate::{
Added, Allowance, Allowlist, Basis, Kind, Reason, Removed, Renamed, Report, Snapshot, Unused,
allow::Intent, snapshot::Fingerprint,
};
use std::collections::BTreeMap;
const PAIRING_ORDER: [Kind; 7] = [
Kind::Plane,
Kind::Anchor,
Kind::Definition,
Kind::Object,
Kind::RenderLayer,
Kind::Stairwell,
Kind::Zone,
];
type Groups<'a> = BTreeMap<(&'a str, &'a Fingerprint), (Vec<&'a str>, Vec<&'a str>)>;
#[must_use]
pub fn diff(old: &Snapshot, new: &Snapshot, allowlist: &Allowlist) -> Report {
let mut removed = Vec::new();
let mut renamed = Vec::new();
let mut added = Vec::new();
let mut planes: BTreeMap<&str, &str> = BTreeMap::new();
for kind in PAIRING_ORDER {
let mut gone: BTreeMap<&str, &Fingerprint> = old
.entries(kind)
.filter(|(id, _)| !new.contains(kind, id))
.collect();
let mut fresh: BTreeMap<&str, &Fingerprint> = new
.entries(kind)
.filter(|(id, _)| !old.contains(kind, id))
.collect();
let mut pairs = Vec::new();
for from in gone.keys() {
if let Some(Intent::Rename(to)) = allowlist.get(kind, from)
&& new.contains(kind, to)
{
pairs.push((*from, to, Basis::Declared));
}
}
for (from, to, _) in &pairs {
gone.remove(from);
fresh.remove(to);
}
if kind.pairs_by_content() {
for (from, to) in content_pairs(kind, &gone, &fresh, &planes) {
gone.remove(from);
fresh.remove(to);
pairs.push((from, to, Basis::Content));
}
}
if kind == Kind::Plane {
planes = pairs.iter().map(|(from, to, _)| (*from, *to)).collect();
}
removed.extend(gone.into_keys().map(|id| Removed {
kind,
id: id.to_owned(),
allowed: allowed(kind, id, allowlist, old, new),
}));
renamed.extend(pairs.into_iter().map(|(from, to, basis)| Renamed {
kind,
from: from.to_owned(),
to: to.to_owned(),
basis,
allowed: allowed(kind, from, allowlist, old, new),
}));
added.extend(fresh.into_keys().map(|id| Added {
kind,
id: id.to_owned(),
}));
}
removed.sort();
renamed.sort();
added.sort();
let unused = allowlist
.allowances()
.filter_map(|allowance| {
let kind = allowance.kind();
let reason =
if !old.contains(kind, allowance.id()) || new.contains(kind, allowance.id()) {
Reason::NotRemoved
} else {
match &allowance {
Allowance::Rename { to, .. } if !new.contains(kind, to) => {
Reason::TargetMissing
}
_ => return None,
}
};
Some(Unused { allowance, reason })
})
.collect();
Report {
old: old.side().clone(),
new: new.side().clone(),
removed,
renamed,
added,
unused,
}
}
fn content_pairs<'a>(
kind: Kind,
gone: &BTreeMap<&'a str, &'a Fingerprint>,
fresh: &BTreeMap<&'a str, &'a Fingerprint>,
planes: &BTreeMap<&'a str, &'a str>,
) -> Vec<(&'a str, &'a str)> {
let mut groups = Groups::new();
for (id, fingerprint) in gone {
let scope = kind.scope(id);
let scope = if kind.in_plane() {
planes.get(scope).copied().unwrap_or(scope)
} else {
scope
};
groups.entry((scope, fingerprint)).or_default().0.push(id);
}
for (id, fingerprint) in fresh {
groups
.entry((kind.scope(id), fingerprint))
.or_default()
.1
.push(id);
}
groups
.into_values()
.filter_map(|group| match group {
(from, to) if from.len() == 1 && to.len() == 1 => Some((from[0], to[0])),
_ => None,
})
.collect()
}
fn allowed(kind: Kind, id: &str, allowlist: &Allowlist, old: &Snapshot, new: &Snapshot) -> bool {
match allowlist.get(kind, id) {
Some(Intent::Remove) => return true,
Some(Intent::Rename(to)) if new.contains(kind, to) => return true,
_ => {}
}
if kind == Kind::Object {
return old.object_plane(id).is_some_and(|plane| {
!new.contains(Kind::Plane, plane)
&& matches!(allowlist.get(Kind::Plane, plane), Some(Intent::Remove))
});
}
let Some((plane, local)) = id.split_once('/').filter(|_| kind.in_plane()) else {
return false;
};
if new.contains(Kind::Plane, plane) {
return false;
}
match allowlist.get(Kind::Plane, plane) {
Some(Intent::Remove) => true,
Some(Intent::Rename(to)) => {
new.contains(Kind::Plane, to) && new.contains(kind, &format!("{to}/{local}"))
}
None => false,
}
}