use super::helpers::*;
use crate::core::{resolver, types};
use std::path::Path;
#[allow(clippy::type_complexity)]
fn compute_depth_map<'a>(
order: &'a [String],
config: &'a types::ForjarConfig,
) -> (
std::collections::HashMap<&'a str, usize>,
std::collections::HashMap<&'a str, Vec<&'a str>>,
std::collections::HashSet<&'a str>,
) {
let mut children: std::collections::HashMap<&str, Vec<&str>> = std::collections::HashMap::new();
let mut has_parent = std::collections::HashSet::new();
for (name, res) in &config.resources {
for dep in &res.depends_on {
children
.entry(dep.as_str())
.or_default()
.push(name.as_str());
has_parent.insert(name.as_str());
}
}
let mut depth_map: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
let mut queue = std::collections::VecDeque::new();
for name in order {
if !has_parent.contains(name.as_str()) {
depth_map.insert(name.as_str(), 0);
queue.push_back(name.as_str());
}
}
while let Some(node) = queue.pop_front() {
let d = depth_map[node];
if let Some(kids) = children.get(node) {
for &kid in kids {
if !depth_map.contains_key(kid) {
depth_map.insert(kid, d + 1);
queue.push_back(kid);
}
}
}
}
(depth_map, children, has_parent)
}
fn print_depth_mermaid(
config: &types::ForjarConfig,
order: &[String],
included: &std::collections::HashSet<&str>,
children: &std::collections::HashMap<&str, Vec<&str>>,
) {
println!("graph TD");
for (name, res) in &config.resources {
if !included.contains(name.as_str()) {
continue;
}
for dep in &res.depends_on {
if included.contains(dep.as_str()) {
println!(" {dep} --> {name}");
}
}
}
for name in order {
if included.contains(name.as_str()) {
let res = &config.resources[name];
if res.depends_on.is_empty() && !children.contains_key(name.as_str()) {
println!(" {name}");
}
}
}
}
fn print_depth_dot(config: &types::ForjarConfig, included: &std::collections::HashSet<&str>) {
println!("digraph G {{");
for (name, res) in &config.resources {
if !included.contains(name.as_str()) {
continue;
}
for dep in &res.depends_on {
if included.contains(dep.as_str()) {
println!(" \"{dep}\" -> \"{name}\"");
}
}
}
println!("}}");
}
fn print_cluster_mermaid(
by_machine: &std::collections::HashMap<String, Vec<String>>,
config: &types::ForjarConfig,
) {
println!("graph TD");
for (machine, resources) in by_machine {
println!(" subgraph {machine}");
for name in resources {
println!(" {name}");
}
println!(" end");
}
for (name, res) in &config.resources {
for dep in &res.depends_on {
println!(" {dep} --> {name}");
}
}
}
fn print_cluster_dot(
by_machine: &std::collections::HashMap<String, Vec<String>>,
config: &types::ForjarConfig,
) {
println!("digraph G {{");
for (machine, resources) in by_machine {
println!(" subgraph cluster_{} {{", machine.replace('-', "_"));
println!(" label=\"{machine}\";");
for name in resources {
println!(" \"{name}\";");
}
println!(" }}");
}
for (name, res) in &config.resources {
for dep in &res.depends_on {
println!(" \"{dep}\" -> \"{name}\";");
}
}
println!("}}");
}
fn print_highlight_dot(
order: &[String],
config: &types::ForjarConfig,
highlighted: &std::collections::HashSet<String>,
) {
println!("digraph {{");
println!(" rankdir=LR;");
for name in order {
if highlighted.contains(name) {
println!(" \"{name}\" [style=filled, fillcolor=yellow];");
} else {
println!(" \"{name}\";");
}
if let Some(res) = config.resources.get(name) {
for dep in &res.depends_on {
let style = if highlighted.contains(name) && highlighted.contains(dep) {
" [color=red, penwidth=2]"
} else {
""
};
println!(" \"{name}\" -> \"{dep}\"{style};");
}
}
}
println!("}}");
}
fn print_highlight_mermaid(
order: &[String],
config: &types::ForjarConfig,
highlighted: &std::collections::HashSet<String>,
) {
println!("graph LR");
for name in order {
if highlighted.contains(name) {
println!(" {name}[\"⚡ {name}\"]");
println!(" style {name} fill:#ffeb3b,stroke:#f44336,stroke-width:2px");
}
if let Some(res) = config.resources.get(name) {
for dep in &res.depends_on {
if highlighted.contains(name) && highlighted.contains(dep) {
println!(" {name} ==>|dep| {dep}");
} else {
println!(" {name} --> {dep}");
}
}
}
}
}
pub(crate) fn cmd_graph_depth(file: &Path, format: &str, max_depth: usize) -> Result<(), String> {
let config = parse_and_validate(file)?;
let order = resolver::build_execution_order(&config)?;
let (depth_map, children, _has_parent) = compute_depth_map(&order, &config);
let included: std::collections::HashSet<&str> = depth_map
.iter()
.filter(|(_, &d)| d <= max_depth)
.map(|(&n, _)| n)
.collect();
if format == "mermaid" {
print_depth_mermaid(&config, &order, &included, &children);
} else {
print_depth_dot(&config, &included);
}
Ok(())
}
pub(crate) fn cmd_graph_cluster(file: &Path, format: &str) -> Result<(), String> {
let config = parse_and_validate(file)?;
let mut by_machine: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for (name, res) in &config.resources {
let m = match &res.machine {
types::MachineTarget::Single(m) => m.clone(),
types::MachineTarget::Multiple(ms) => {
ms.first().cloned().unwrap_or_else(|| "unknown".to_string())
}
};
by_machine.entry(m).or_default().push(name.clone());
}
if format == "mermaid" {
print_cluster_mermaid(&by_machine, &config);
} else {
print_cluster_dot(&by_machine, &config);
}
Ok(())
}
pub(crate) fn cmd_graph_orphans(file: &Path) -> Result<(), String> {
let config = parse_and_validate(file)?;
let mut has_dependents = std::collections::HashSet::new();
let mut has_deps = std::collections::HashSet::new();
for (name, res) in &config.resources {
for dep in &res.depends_on {
has_dependents.insert(dep.clone());
has_deps.insert(name.clone());
}
}
let mut orphans = Vec::new();
for name in config.resources.keys() {
if !has_dependents.contains(name) && !has_deps.contains(name) {
orphans.push(name.clone());
}
}
if orphans.is_empty() {
println!("{} No orphan resources found", green("✓"));
} else {
println!(
"{} {} orphan resource(s) (no deps, no dependents):",
yellow("⚠"),
orphans.len()
);
for name in &orphans {
let res = &config.resources[name];
println!(" {} (type: {:?})", name, res.resource_type);
}
}
Ok(())
}
pub(crate) fn cmd_graph_stats(file: &Path) -> Result<(), String> {
let config = parse_and_validate(file)?;
let order = resolver::build_execution_order(&config)?;
let nodes = config.resources.len();
let mut edges = 0usize;
let mut max_deps = 0usize;
for res in config.resources.values() {
edges += res.depends_on.len();
max_deps = max_deps.max(res.depends_on.len());
}
let (depth_map, children, has_parent) = compute_depth_map(&order, &config);
let max_depth = depth_map.values().copied().max().unwrap_or(0);
let mut width_map: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
for &d in depth_map.values() {
*width_map.entry(d).or_insert(0) += 1;
}
let max_width = width_map.values().copied().max().unwrap_or(0);
let roots = order
.iter()
.filter(|n| !has_parent.contains(n.as_str()))
.count();
let leaves = order
.iter()
.filter(|n| !children.contains_key(n.as_str()))
.count();
println!("{}", bold("Graph Statistics"));
println!(" Nodes: {nodes}");
println!(" Edges: {edges}");
println!(" Depth: {max_depth}");
println!(" Width: {max_width}");
println!(" Roots: {roots}");
println!(" Leaves: {leaves}");
println!(" Max deps: {max_deps}");
if nodes > 0 {
println!(" Density: {:.2}", edges as f64 / nodes as f64);
}
Ok(())
}
pub(crate) fn cmd_graph_json(file: &Path) -> Result<(), String> {
let config = parse_and_validate(file)?;
let order = resolver::build_execution_order(&config)?;
let mut adjacency: Vec<String> = Vec::new();
for name in &order {
let deps = config
.resources
.get(name)
.map(|r| {
r.depends_on
.iter()
.map(|s| format!("\"{s}\""))
.collect::<Vec<_>>()
.join(",")
})
.unwrap_or_default();
adjacency.push(format!("\"{name}\":[{deps}]"));
}
println!("{{{}}}", adjacency.join(","));
Ok(())
}
pub(crate) fn cmd_graph_highlight(file: &Path, format: &str, resource: &str) -> Result<(), String> {
let config = parse_and_validate(file)?;
let order = resolver::build_execution_order(&config)?;
let mut highlighted = std::collections::HashSet::new();
highlighted.insert(resource.to_string());
let mut queue = vec![resource.to_string()];
while let Some(current) = queue.pop() {
if let Some(res) = config.resources.get(¤t) {
for dep in &res.depends_on {
if highlighted.insert(dep.clone()) {
queue.push(dep.clone());
}
}
}
}
if format == "dot" {
print_highlight_dot(&order, &config, &highlighted);
} else {
print_highlight_mermaid(&order, &config, &highlighted);
}
Ok(())
}