#[cfg(feature = "dev-tools")]
use std::collections::{HashMap, HashSet};
#[cfg(feature = "dev-tools")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraphNode {
pub name: String,
pub scope: String,
pub type_name: Option<String>,
}
#[cfg(feature = "dev-tools")]
#[derive(Debug, Default)]
pub struct DependencyGraph {
nodes: HashMap<String, GraphNode>,
edges: Vec<(String, String)>,
}
#[cfg(feature = "dev-tools")]
impl DependencyGraph {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
}
}
pub fn add_node(&mut self, name: impl Into<String>, scope: impl Into<String>) {
let name = name.into();
self.nodes.insert(
name.clone(),
GraphNode {
name,
scope: scope.into(),
type_name: None,
},
);
}
pub fn add_typed_node(
&mut self,
name: impl Into<String>,
scope: impl Into<String>,
type_name: impl Into<String>,
) {
let name = name.into();
self.nodes.insert(
name.clone(),
GraphNode {
name,
scope: scope.into(),
type_name: Some(type_name.into()),
},
);
}
pub fn add_dependency(&mut self, from: impl Into<String>, to: impl Into<String>) {
self.edges.push((from.into(), to.into()));
}
pub fn to_dot(&self) -> String {
let mut output = String::from("digraph DependencyGraph {\n");
output.push_str(" rankdir=LR;\n");
output.push_str(" node [shape=box, style=rounded];\n\n");
for node in self.nodes.values() {
let color = match node.scope.as_str() {
"singleton" => "lightblue",
"request" => "lightgreen",
"transient" => "lightyellow",
_ => "white",
};
let label = if let Some(ref type_name) = node.type_name {
format!("{}\\n({})", node.name, type_name)
} else {
node.name.clone()
};
output.push_str(&format!(
" \"{}\" [label=\"{}\", fillcolor={}, style=filled];\n",
node.name, label, color
));
}
output.push('\n');
for (from, to) in &self.edges {
output.push_str(&format!(" \"{}\" -> \"{}\";\n", from, to));
}
output.push_str("}\n");
output
}
pub fn detect_cycles(&self) -> Vec<Vec<String>> {
let mut cycles = Vec::new();
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
for node_name in self.nodes.keys() {
if !visited.contains(node_name) {
let mut path = Vec::new();
self.dfs_detect_cycles(
node_name,
&mut visited,
&mut rec_stack,
&mut path,
&mut cycles,
);
}
}
cycles
}
fn dfs_detect_cycles(
&self,
node: &str,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
path: &mut Vec<String>,
cycles: &mut Vec<Vec<String>>,
) {
visited.insert(node.to_string());
rec_stack.insert(node.to_string());
path.push(node.to_string());
let dependencies: Vec<_> = self
.edges
.iter()
.filter_map(|(from, to)| {
if from == node {
Some(to.as_str())
} else {
None
}
})
.collect();
for dep in dependencies {
if !visited.contains(dep) {
self.dfs_detect_cycles(dep, visited, rec_stack, path, cycles);
} else if rec_stack.contains(dep)
&& let Some(cycle_start) = path.iter().position(|p| p == dep)
{
cycles.push(path[cycle_start..].to_vec());
}
}
path.pop();
rec_stack.remove(node);
}
pub fn statistics(&self) -> GraphStatistics {
let node_count = self.nodes.len();
let edge_count = self.edges.len();
let singleton_count = self
.nodes
.values()
.filter(|n| n.scope == "singleton")
.count();
let request_count = self.nodes.values().filter(|n| n.scope == "request").count();
let transient_count = self
.nodes
.values()
.filter(|n| n.scope == "transient")
.count();
GraphStatistics {
node_count,
edge_count,
singleton_count,
request_count,
transient_count,
}
}
}
#[cfg(feature = "dev-tools")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraphStatistics {
pub node_count: usize,
pub edge_count: usize,
pub singleton_count: usize,
pub request_count: usize,
pub transient_count: usize,
}