use std::{collections::VecDeque, mem};
use ahash::{HashMap, HashMapExt, HashSet};
#[derive(Debug)]
pub(crate) struct DepGraph<'a>(HashMap<&'a str, NodeData<'a>>);
#[derive(Default)]
#[derive(Debug)]
struct NodeData<'a> {
dependencies: HashSet<&'a str>,
rev_dependencies: HashSet<&'a str>,
}
impl<'a> DepGraph<'a> {
pub(crate) fn new() -> Self {
Self(HashMap::new())
}
pub(crate) fn add_asset(&mut self, id: &'a str) {
self.0.entry(id).or_default();
}
pub(crate) fn add_dependency(&mut self, depender: &'a str, dependee: &'a str) {
self.0.entry(depender).or_default().dependencies.insert(dependee);
self.0.entry(dependee).or_default().rev_dependencies.insert(depender);
}
pub(crate) fn topological_sort(mut self) -> Result<Vec<&'a str>, Vec<&'a str>> {
let mut queue: VecDeque<_> = self.0.iter()
.filter(|(_, data)| data.dependencies.is_empty())
.map(|(id, _)| *id)
.collect();
let mut pos = 0;
while let Some(&dependee_id) = queue.get(pos) {
pos += 1;
let rev_deps = mem::take(&mut self.0.get_mut(&dependee_id).unwrap().rev_dependencies).into_iter();
for depender_id in rev_deps {
let depender = self.0.get_mut(&depender_id).unwrap();
let was_removed = depender.dependencies.remove(&dependee_id);
debug_assert!(was_removed);
if depender.dependencies.is_empty() {
queue.push_back(depender_id);
}
}
}
if queue.len() == self.0.len() {
Ok(queue.into())
} else {
let (&start_id, _) = self.0.iter()
.find(|(_, data)| !data.dependencies.is_empty())
.expect("can't find node with edges, but there should be a cycle");
let mut out = vec![start_id];
let mut id = start_id;
loop {
let next = *self.0[&id].dependencies.iter().next().unwrap();
if let Some(pos) = out.iter().position(|&visited| visited == next) {
out.drain(..pos);
return Err(out);
}
out.push(next);
id = next;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn graph<'a>(edges: &[(&'a str, &'a str)]) -> DepGraph<'a> {
let mut g = DepGraph::new();
for &(from, to) in edges {
g.add_dependency(from, to);
}
g
}
macro_rules! assert_topsort {
(
[$($from:literal <- $to:literal),* $(,)?]
=> $res:ident($( [$($id:literal),*] ),* $(,)?)
) => {
let actual = graph(&[$( ($from, $to) ),*]).topological_sort();
let valid = [
$( $res(vec![$($id),*]) ),*,
];
if !valid.contains(&actual) {
panic!("`assert_topsort` failed: {:?} is not in valid solutions: {:#?}", actual, valid);
}
};
}
#[test]
fn topological_sort_empty() {
assert_topsort!([] => Ok([]));
}
#[test]
fn topological_sort_dag() {
assert_topsort!(["a" <- "b"] => Ok(["b", "a"]));
assert_topsort!(["b" <- "a"] => Ok(["a", "b"]));
assert_topsort!(["b" <- "a", "c" <- "b"] => Ok(["a", "b", "c"]));
assert_topsort!(["c" <- "f", "a" <- "c"] => Ok(["f", "c", "a"]));
assert_topsort!(["a" <- "b", "a" <- "c"] => Ok(["b", "c", "a"], ["c", "b", "a"]));
}
#[test]
fn topological_sort_cycles() {
assert_topsort!(
["a" <- "b", "b" <- "c", "c" <- "a", "a" <- "e"]
=> Err(["a", "b", "c"], ["b", "c", "a"], ["c", "a", "b"])
);
assert_topsort!(
[
"a" <- "b", "b" <- "c", "c" <- "a",
"b" <- "d", "d" <- "c",
"d" <- "e", "e" <- "f",
]
=> Err(
["a", "b", "c"], ["b", "c", "a"], ["c", "a", "b"],
["d", "b", "c"], ["b", "c", "d"], ["c", "d", "b"],
["a", "b", "d", "c"], ["b", "d", "c", "a"],
["d", "c", "a", "b"], ["c", "a", "b", "d"],
)
);
}
}