use std::collections::{BTreeMap, BTreeSet};
use crawk::AnnotatedEdges;
use super::format_api_suffix;
#[must_use]
pub(crate) fn render_plain(edges: &AnnotatedEdges) -> String {
if edges.is_empty() {
return String::new();
}
let mut out = String::new();
for ((source, target), apis) in edges {
out.push_str(source);
out.push_str(" -> ");
out.push_str(target);
out.push_str(&format_api_suffix(apis));
out.push('\n');
}
out
}
#[must_use]
pub(crate) fn render_grouped(edges: &AnnotatedEdges) -> String {
if edges.is_empty() {
return String::new();
}
let mut groups: BTreeMap<&str, Vec<(&str, &BTreeSet<String>)>> = BTreeMap::new();
for ((source, target), apis) in edges {
groups
.entry(source.as_str())
.or_default()
.push((target.as_str(), apis));
}
let mut out = String::new();
let mut first = true;
for (source, targets) in &groups {
if !first {
out.push('\n');
}
first = false;
out.push_str(source);
out.push_str(" (");
out.push_str(&targets.len().to_string());
out.push_str(")\n");
for (target, apis) in targets {
out.push_str(" -> ");
out.push_str(target);
out.push_str(&format_api_suffix(apis));
out.push('\n');
}
}
out
}
#[must_use]
pub(crate) fn render_dot(edges: &AnnotatedEdges) -> String {
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.keys() {
nodes.insert(source.as_str());
nodes.insert(target.as_str());
}
out.push('\n');
for node in &nodes {
out.push_str(" \"");
out.push_str(node);
out.push_str("\";\n");
}
out.push('\n');
for ((source, target), apis) in edges {
out.push_str(" \"");
out.push_str(source);
out.push_str("\" -> \"");
out.push_str(target);
if apis.is_empty() {
out.push_str("\";\n");
} else {
let items: Vec<&str> = apis.iter().map(String::as_str).collect();
out.push_str("\" [label=\"");
out.push_str(&items.join(", "));
out.push_str("\", fontcolor=\"#c96a00\"];\n");
}
}
}
out.push_str("}\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
fn edges(pairs: &[(&str, &str)]) -> AnnotatedEdges {
pairs
.iter()
.map(|(s, t)| ((s.to_string(), t.to_string()), BTreeSet::new()))
.collect()
}
fn edges_with_apis(pairs: &[(&str, &str, &[&str])]) -> AnnotatedEdges {
pairs
.iter()
.map(|(s, t, apis)| {
let api_set: BTreeSet<String> = apis.iter().map(ToString::to_string).collect();
((s.to_string(), t.to_string()), api_set)
})
.collect()
}
#[test]
fn render_grouped_empty() {
assert_eq!(render_grouped(&BTreeMap::new()), "");
}
#[test]
fn render_grouped_single_source() {
let e = edges(&[("parser", "constants"), ("parser", "reference")]);
assert_eq!(
render_grouped(&e),
"parser (2)\n -> constants\n -> reference\n"
);
}
#[test]
fn render_grouped_multiple_sources_separated_by_blank_line() {
let e = edges(&[("analyzer", "cache"), ("parser", "reference")]);
assert_eq!(
render_grouped(&e),
"analyzer (1)\n -> cache\n\nparser (1)\n -> reference\n"
);
}
#[test]
fn render_grouped_targets_sorted_alphabetically() {
let e = edges(&[("cli", "model"), ("cli", "analyzer"), ("cli", "format")]);
assert_eq!(
render_grouped(&e),
"cli (3)\n -> analyzer\n -> format\n -> model\n"
);
}
#[test]
fn render_grouped_ends_with_newline() {
let e = edges(&[("a", "b")]);
assert!(render_grouped(&e).ends_with('\n'));
}
#[test]
fn render_grouped_with_apis() {
let e = edges_with_apis(&[
("analyzer", "cache", &["ParseCache"]),
("analyzer", "reference", &["PathPrefix", "TypeReference"]),
]);
assert_eq!(
render_grouped(&e),
"analyzer (2)\n -> cache [ParseCache]\n -> reference [PathPrefix, TypeReference]\n"
);
}
#[test]
fn render_dot_empty_edges_produces_valid_dot() {
let out = render_dot(&BTreeMap::new());
assert!(out.starts_with("digraph dependencies {"));
assert!(out.ends_with("}\n"));
assert!(!out.contains("\" -> \""));
}
#[test]
fn render_dot_single_edge() {
let e = edges(&[("parser", "reference")]);
let out = render_dot(&e);
assert!(out.contains("\"parser\";"));
assert!(out.contains("\"reference\";"));
assert!(out.contains("\"parser\" -> \"reference\";"));
}
#[test]
fn render_dot_nodes_listed_before_edges() {
let e = edges(&[("cli", "model")]);
let out = render_dot(&e);
let node_pos = out.find("\"cli\";").unwrap();
let edge_pos = out.find("\"cli\" -> \"model\";").unwrap();
assert!(node_pos < edge_pos);
}
#[test]
fn render_dot_colons_quoted_correctly() {
let e = edges(&[("parser::visitor", "discover::module_tree")]);
let out = render_dot(&e);
assert!(out.contains("\"parser::visitor\";"));
assert!(out.contains("\"discover::module_tree\";"));
assert!(out.contains("\"parser::visitor\" -> \"discover::module_tree\";"));
}
#[test]
fn render_dot_ends_with_newline() {
let e = edges(&[("a", "b")]);
assert!(render_dot(&e).ends_with('\n'));
}
#[test]
fn render_dot_with_apis() {
let e = edges_with_apis(&[("parser", "reference", &["PathPrefix", "TypeReference"])]);
let out = render_dot(&e);
assert!(out.contains("\"parser\" -> \"reference\" [label=\"PathPrefix, TypeReference\", fontcolor=\"#c96a00\"];"));
}
#[test]
fn render_dot_without_apis_no_label() {
let e = edges(&[("parser", "reference")]);
let out = render_dot(&e);
assert!(!out.contains("label="));
assert!(out.contains("\"parser\" -> \"reference\";"));
}
#[test]
fn render_plain_empty() {
assert_eq!(render_plain(&BTreeMap::new()), "");
}
#[test]
fn render_plain_single_edge() {
let e = edges(&[("analyzer", "cache")]);
assert_eq!(render_plain(&e), "analyzer -> cache\n");
}
#[test]
fn render_plain_sorted_alphabetically() {
let e = edges(&[("z_mod", "target"), ("a_mod", "target")]);
assert_eq!(render_plain(&e), "a_mod -> target\nz_mod -> target\n");
}
#[test]
fn render_plain_ends_with_newline() {
let e = edges(&[("a", "b")]);
assert!(render_plain(&e).ends_with('\n'));
}
#[test]
fn render_plain_with_apis() {
let e = edges_with_apis(&[
("analyzer", "cache", &["ParseCache"]),
("analyzer", "reference", &["PathPrefix", "TypeReference"]),
]);
assert_eq!(
render_plain(&e),
"analyzer -> cache [ParseCache]\nanalyzer -> reference [PathPrefix, TypeReference]\n"
);
}
}