use std::collections::{HashMap, HashSet};
use crate::domain::passport::PassportId;
pub type ComponentEdges = HashMap<PassportId, Vec<PassportId>>;
pub const DEFAULT_DEPTH_CAP: usize = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeRejection {
Cycle,
DepthExceeded,
}
pub fn check_edge(
edges: &ComponentEdges,
parent: PassportId,
child: PassportId,
depth_cap: usize,
) -> Result<(), EdgeRejection> {
if parent == child {
return Err(EdgeRejection::Cycle);
}
let mut visited = HashSet::new();
let mut stack = vec![(child, 1usize)];
while let Some((node, depth)) = stack.pop() {
if node == parent {
return Err(EdgeRejection::Cycle);
}
if depth > depth_cap {
return Err(EdgeRejection::DepthExceeded);
}
if !visited.insert(node) {
continue;
}
if let Some(children) = edges.get(&node) {
for &c in children {
stack.push((c, depth + 1));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn id() -> PassportId {
PassportId::new()
}
#[test]
fn independent_child_is_accepted() {
let edges = ComponentEdges::new();
let (parent, child) = (id(), id());
assert_eq!(check_edge(&edges, parent, child, DEFAULT_DEPTH_CAP), Ok(()));
}
#[test]
fn self_edge_is_a_cycle() {
let edges = ComponentEdges::new();
let p = id();
assert_eq!(
check_edge(&edges, p, p, DEFAULT_DEPTH_CAP),
Err(EdgeRejection::Cycle)
);
}
#[test]
fn direct_back_edge_is_a_cycle() {
let (parent, child) = (id(), id());
let mut edges = ComponentEdges::new();
edges.insert(child, vec![parent]);
assert_eq!(
check_edge(&edges, parent, child, DEFAULT_DEPTH_CAP),
Err(EdgeRejection::Cycle)
);
}
#[test]
fn transitive_back_edge_is_a_cycle() {
let (parent, child, mid) = (id(), id(), id());
let mut edges = ComponentEdges::new();
edges.insert(child, vec![mid]);
edges.insert(mid, vec![parent]);
assert_eq!(
check_edge(&edges, parent, child, DEFAULT_DEPTH_CAP),
Err(EdgeRejection::Cycle)
);
}
#[test]
fn shared_subcomponent_diamond_is_not_a_cycle() {
let (parent, child, a, b, leaf) = (id(), id(), id(), id(), id());
let mut edges = ComponentEdges::new();
edges.insert(child, vec![a, b]);
edges.insert(a, vec![leaf]);
edges.insert(b, vec![leaf]);
assert_eq!(check_edge(&edges, parent, child, DEFAULT_DEPTH_CAP), Ok(()));
}
#[test]
fn subtree_deeper_than_cap_is_refused() {
let parent = id();
let chain: Vec<PassportId> = (0..8).map(|_| id()).collect();
let mut edges = ComponentEdges::new();
for pair in chain.windows(2) {
edges.insert(pair[0], vec![pair[1]]);
}
assert_eq!(
check_edge(&edges, parent, chain[0], 3),
Err(EdgeRejection::DepthExceeded)
);
assert_eq!(check_edge(&edges, parent, chain[0], 32), Ok(()));
}
#[test]
fn pre_existing_cycle_in_edges_still_terminates() {
let (parent, x, y) = (id(), id(), id());
let mut edges = ComponentEdges::new();
edges.insert(x, vec![y]);
edges.insert(y, vec![x]);
assert_eq!(check_edge(&edges, parent, x, 32), Ok(()));
}
}