use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;
use crawk::Edge;
#[must_use]
pub(crate) fn render_paths_plain(paths: &[Vec<String>]) -> String {
let mut out = String::new();
for path in paths {
let _ = writeln!(out, "{}", path.join(" -> "));
}
out
}
#[must_use]
pub(crate) fn render_paths_grouped(paths: &[Vec<String>]) -> String {
let mut groups: BTreeMap<usize, Vec<&Vec<String>>> = BTreeMap::new();
for path in paths {
let len = path.len().saturating_sub(1);
groups.entry(len).or_default().push(path);
}
let mut out = String::new();
let mut first = true;
for (len, group) in &groups {
if !first {
out.push('\n');
}
first = false;
let _ = writeln!(out, "length {len}:");
for path in group {
let _ = writeln!(out, " {}", path.join(" -> "));
}
}
out
}
#[must_use]
pub(crate) fn render_paths_dot(edges: &BTreeSet<Edge>, paths: &[Vec<String>]) -> String {
if paths.is_empty() {
return String::new();
}
let mut path_edge_set: BTreeSet<(&str, &str)> = BTreeSet::new();
for path in paths {
for w in path.windows(2) {
path_edge_set.insert((w[0].as_str(), w[1].as_str()));
}
}
let mut out = String::new();
out.push_str("digraph dependencies {\n");
out.push_str(" rankdir=LR;\n");
out.push_str(" node [shape=box, style=rounded, fontname=\"monospace\", fontsize=10];\n");
out.push_str(" edge [color=\"#444444\"];\n");
if !edges.is_empty() {
let mut nodes: BTreeSet<&str> = BTreeSet::new();
for (source, target) in edges {
nodes.insert(source.as_str());
nodes.insert(target.as_str());
}
out.push('\n');
for node in &nodes {
let _ = writeln!(out, " \"{node}\";");
}
out.push('\n');
for (source, target) in edges {
let on_path = path_edge_set.contains(&(source.as_str(), target.as_str()));
let _ = write!(out, " \"{source}\" -> \"{target}\"");
if on_path {
out.push_str(" [color=red, style=bold, penwidth=2.0]");
}
out.push_str(";\n");
}
}
out.push_str("}\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
fn paths(paths: Vec<Vec<&str>>) -> Vec<Vec<String>> {
paths
.into_iter()
.map(|p| p.into_iter().map(str::to_owned).collect())
.collect()
}
fn edges(pairs: &[(&str, &str)]) -> BTreeSet<Edge> {
pairs
.iter()
.map(|(s, t)| ((*s).to_owned(), (*t).to_owned()))
.collect()
}
#[test]
fn plain_empty_returns_empty() {
assert_eq!(render_paths_plain(&[]), "");
}
#[test]
fn plain_single_path() {
let p = paths(vec![vec!["a", "b", "c"]]);
assert_eq!(render_paths_plain(&p), "a -> b -> c\n");
}
#[test]
fn plain_two_paths_sorted() {
let p = paths(vec![vec!["lib", "a", "leaf"], vec!["lib", "b", "leaf"]]);
let out = render_paths_plain(&p);
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], "lib -> a -> leaf");
assert_eq!(lines[1], "lib -> b -> leaf");
}
#[test]
fn plain_source_equals_target() {
let p = paths(vec![vec!["a"]]);
assert_eq!(render_paths_plain(&p), "a\n");
}
#[test]
fn grouped_empty_returns_empty() {
assert_eq!(render_paths_grouped(&[]), "");
}
#[test]
fn grouped_has_length_header() {
let p = paths(vec![vec!["a", "b", "c"]]);
let out = render_paths_grouped(&p);
assert!(out.contains("length 2:"));
assert!(out.contains(" a -> b -> c"));
}
#[test]
fn grouped_same_length_paths_one_header() {
let p = paths(vec![vec!["lib", "a", "leaf"], vec!["lib", "b", "leaf"]]);
let out = render_paths_grouped(&p);
let headers: Vec<&str> = out.lines().filter(|l| l.starts_with("length")).collect();
assert_eq!(headers.len(), 1);
assert_eq!(headers[0], "length 2:");
}
#[test]
fn dot_empty_paths_returns_empty() {
let e = edges(&[("a", "b")]);
assert_eq!(render_paths_dot(&e, &[]), "");
}
#[test]
fn dot_path_edge_colored_red() {
let e = edges(&[("lib", "a"), ("lib", "b"), ("a", "leaf"), ("b", "leaf")]);
let p = paths(vec![vec!["lib", "a", "leaf"]]);
let out = render_paths_dot(&e, &p);
assert!(out.contains("\"lib\" -> \"a\" [color=red, style=bold, penwidth=2.0];"));
assert!(out.contains("\"a\" -> \"leaf\" [color=red, style=bold, penwidth=2.0];"));
assert!(out.contains("\"lib\" -> \"b\";"));
assert!(out.contains("\"b\" -> \"leaf\";"));
}
#[test]
fn dot_starts_and_ends_correctly() {
let e = edges(&[("a", "b")]);
let p = paths(vec![vec!["a", "b"]]);
let out = render_paths_dot(&e, &p);
assert!(out.starts_with("digraph dependencies {"));
assert!(out.ends_with("}\n"));
}
}