use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct PartialGraph {
pub nodes: HashMap<String, NodeInfo>,
pub edges: Vec<(String, String, EdgeKind)>,
pub center: String,
pub max_depth: usize,
}
#[derive(Debug, Clone)]
pub struct NodeInfo {
pub file: String,
pub kind: String,
pub depth: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EdgeKind {
Calls,
CalledBy,
Imports,
Implements,
}
impl PartialGraph {
pub fn render(&self) -> String {
let Some(center) = self.nodes.get(&self.center) else {
return String::new();
};
let mut output = format!("Center: {} ({})", self.center, center.file);
for depth in 1..=self.max_depth {
let mut nodes: Vec<(&str, &NodeInfo)> = self
.nodes
.iter()
.filter(|(_, info)| info.depth == depth)
.map(|(name, info)| (name.as_str(), info))
.collect();
nodes.sort_unstable_by_key(|(name, _)| *name);
if nodes.is_empty() {
continue;
}
output.push_str(&format!("\nDepth {depth}: "));
let rendered = nodes
.into_iter()
.map(|(name, info)| {
let direction = self.direction_for(name, depth);
format!("{direction}{name} ({})", info.file)
})
.collect::<Vec<_>>()
.join(", ");
output.push_str(&rendered);
}
output
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn at_depth(&self, depth: usize) -> Vec<&str> {
let mut symbols: Vec<&str> = self
.nodes
.iter()
.filter(|(_, info)| info.depth == depth)
.map(|(name, _)| name.as_str())
.collect();
symbols.sort_unstable();
symbols
}
fn direction_for(&self, name: &str, depth: usize) -> String {
let relation = self.edges.iter().find_map(|(from, to, relation)| {
if to == name {
self.nodes
.get(from)
.filter(|info| info.depth + 1 == depth)
.map(|_| *relation)
} else {
None
}
});
let arrow = match relation {
Some(EdgeKind::CalledBy) => "← ",
Some(EdgeKind::Calls | EdgeKind::Imports | EdgeKind::Implements) | None => "→ ",
};
arrow.repeat(depth)
}
}
#[cfg(test)]
mod tests {
use super::{NodeInfo, PartialGraph};
#[test]
fn empty_graph_renders_empty() {
assert_eq!(PartialGraph::default().render(), "");
}
#[test]
fn single_node_graph() {
let mut graph = PartialGraph {
center: "root".to_string(),
max_depth: 2,
..PartialGraph::default()
};
graph.nodes.insert(
"root".to_string(),
NodeInfo {
file: "src/root.rs".to_string(),
kind: "function".to_string(),
depth: 0,
},
);
assert_eq!(graph.node_count(), 1);
assert_eq!(graph.render(), "Center: root (src/root.rs)");
}
#[test]
fn at_depth_filters_correctly() {
let mut graph = PartialGraph::default();
for (name, depth) in [("root", 0), ("beta", 1), ("alpha", 1), ("leaf", 2)] {
graph.nodes.insert(
name.to_string(),
NodeInfo {
file: format!("{name}.rs"),
kind: "function".to_string(),
depth,
},
);
}
assert_eq!(graph.at_depth(1), vec!["alpha", "beta"]);
assert_eq!(graph.at_depth(2), vec!["leaf"]);
assert!(graph.at_depth(3).is_empty());
}
}