use std::collections::{HashMap, HashSet, VecDeque};
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum DagError {
#[error("cycle detected involving node '{0}'")]
CycleDetected(String),
#[error("node '{0}' not found in graph")]
NodeNotFound(String),
#[error("self-loop on node '{0}' is not allowed")]
SelfLoop(String),
}
#[derive(Debug, Default, Clone)]
pub struct JobDag {
edges: HashMap<String, Vec<String>>,
}
impl JobDag {
pub fn new() -> Self {
Self::default()
}
pub fn add_node(&mut self, id: impl Into<String>) {
self.edges.entry(id.into()).or_default();
}
pub fn add_edge(
&mut self,
from: impl Into<String>,
to: impl Into<String>,
) -> Result<(), DagError> {
let from = from.into();
let to = to.into();
if from == to {
return Err(DagError::SelfLoop(from));
}
if !self.edges.contains_key(&from) {
return Err(DagError::NodeNotFound(from));
}
if !self.edges.contains_key(&to) {
return Err(DagError::NodeNotFound(to));
}
self.edges.entry(from.clone()).or_default().push(to.clone());
if self.has_cycle() {
let succs = self.edges.get_mut(&from).expect("just inserted");
succs.retain(|s| s != &to);
return Err(DagError::CycleDetected(from));
}
Ok(())
}
pub fn has_cycle(&self) -> bool {
let mut visited: HashSet<&str> = HashSet::new();
let mut rec_stack: HashSet<&str> = HashSet::new();
for node in self.edges.keys() {
if !visited.contains(node.as_str()) {
if self.dfs_cycle(node, &mut visited, &mut rec_stack) {
return true;
}
}
}
false
}
fn dfs_cycle<'a>(
&'a self,
node: &'a str,
visited: &mut HashSet<&'a str>,
rec_stack: &mut HashSet<&'a str>,
) -> bool {
visited.insert(node);
rec_stack.insert(node);
if let Some(succs) = self.edges.get(node) {
for succ in succs {
if !visited.contains(succ.as_str()) {
if self.dfs_cycle(succ, visited, rec_stack) {
return true;
}
} else if rec_stack.contains(succ.as_str()) {
return true;
}
}
}
rec_stack.remove(node);
false
}
pub fn topological_order(&self) -> Result<Vec<String>, DagError> {
let mut in_degree: HashMap<&str, usize> =
self.edges.keys().map(|k| (k.as_str(), 0)).collect();
for succs in self.edges.values() {
for s in succs {
*in_degree.entry(s.as_str()).or_insert(0) += 1;
}
}
let mut queue: VecDeque<&str> = in_degree
.iter()
.filter_map(|(&n, &d)| if d == 0 { Some(n) } else { None })
.collect();
let mut queue_vec: Vec<&str> = queue.drain(..).collect();
queue_vec.sort_unstable();
queue.extend(queue_vec);
let mut order: Vec<String> = Vec::with_capacity(self.edges.len());
while let Some(node) = queue.pop_front() {
order.push(node.to_owned());
if let Some(succs) = self.edges.get(node) {
let mut next: Vec<&str> = Vec::new();
for s in succs {
let deg = in_degree.entry(s.as_str()).or_insert(0);
*deg = deg.saturating_sub(1);
if *deg == 0 {
next.push(s.as_str());
}
}
next.sort_unstable();
queue.extend(next);
}
}
if order.len() != self.edges.len() {
return Err(DagError::CycleDetected("(unknown)".to_owned()));
}
Ok(order)
}
pub fn unreachable_nodes(&self) -> Vec<String> {
let mut in_degree: HashMap<&str, usize> =
self.edges.keys().map(|k| (k.as_str(), 0)).collect();
for succs in self.edges.values() {
for s in succs {
*in_degree.entry(s.as_str()).or_insert(0) += 1;
}
}
let roots: Vec<&str> = in_degree
.iter()
.filter_map(|(&n, &d)| if d == 0 { Some(n) } else { None })
.collect();
let mut reachable: HashSet<&str> = HashSet::new();
let mut queue: VecDeque<&str> = roots.into_iter().collect();
while let Some(n) = queue.pop_front() {
if reachable.insert(n) {
if let Some(succs) = self.edges.get(n) {
for s in succs {
queue.push_back(s.as_str());
}
}
}
}
let mut unreachable: Vec<String> = self
.edges
.keys()
.filter(|k| !reachable.contains(k.as_str()))
.cloned()
.collect();
unreachable.sort();
unreachable
}
pub fn predecessors(&self, node: &str) -> Result<Vec<String>, DagError> {
if !self.edges.contains_key(node) {
return Err(DagError::NodeNotFound(node.to_owned()));
}
let mut preds: Vec<String> = self
.edges
.iter()
.filter_map(|(k, succs)| {
if succs.iter().any(|s| s == node) {
Some(k.clone())
} else {
None
}
})
.collect();
preds.sort();
Ok(preds)
}
pub fn node_count(&self) -> usize {
self.edges.len()
}
pub fn edge_count(&self) -> usize {
self.edges.values().map(|v| v.len()).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn linear_dag() -> JobDag {
let mut g = JobDag::new();
g.add_node("a");
g.add_node("b");
g.add_node("c");
g.add_edge("a", "b").unwrap();
g.add_edge("b", "c").unwrap();
g
}
#[test]
fn test_add_node_and_edge() {
let g = linear_dag();
assert_eq!(g.node_count(), 3);
assert_eq!(g.edge_count(), 2);
}
#[test]
fn test_topological_order_linear() {
let g = linear_dag();
let order = g.topological_order().unwrap();
assert_eq!(order, vec!["a", "b", "c"]);
}
#[test]
fn test_no_cycle_in_linear_dag() {
assert!(!linear_dag().has_cycle());
}
#[test]
fn test_cycle_detection() {
let mut g = JobDag::new();
g.add_node("x");
g.add_node("y");
g.add_node("z");
g.add_edge("x", "y").unwrap();
g.add_edge("y", "z").unwrap();
let err = g.add_edge("z", "x").unwrap_err();
assert!(matches!(err, DagError::CycleDetected(_)));
assert!(!g.has_cycle());
}
#[test]
fn test_self_loop_rejected() {
let mut g = JobDag::new();
g.add_node("a");
assert!(matches!(
g.add_edge("a", "a").unwrap_err(),
DagError::SelfLoop(_)
));
}
#[test]
fn test_missing_node_rejected() {
let mut g = JobDag::new();
g.add_node("a");
assert!(matches!(
g.add_edge("a", "ghost").unwrap_err(),
DagError::NodeNotFound(_)
));
assert!(matches!(
g.add_edge("ghost", "a").unwrap_err(),
DagError::NodeNotFound(_)
));
}
#[test]
fn test_predecessors() {
let mut g = linear_dag();
g.add_node("d");
g.add_edge("a", "d").unwrap();
let preds = g.predecessors("b").unwrap();
assert_eq!(preds, vec!["a"]);
}
#[test]
fn test_unreachable_nodes_empty_for_dag() {
assert!(linear_dag().unreachable_nodes().is_empty());
}
#[test]
fn test_topological_order_diamond() {
let mut g = JobDag::new();
for n in ["a", "b", "c", "d"] {
g.add_node(n);
}
g.add_edge("a", "b").unwrap();
g.add_edge("a", "c").unwrap();
g.add_edge("b", "d").unwrap();
g.add_edge("c", "d").unwrap();
let order = g.topological_order().unwrap();
assert_eq!(order[0], "a");
assert_eq!(order[3], "d");
}
#[test]
fn test_empty_dag_topological_order() {
let g = JobDag::new();
assert!(g.topological_order().unwrap().is_empty());
}
}