use super::helpers::*;
use crate::core::{resolver, types};
use std::path::Path;
fn print_prune_dot(
remaining: &[&String],
config: &types::ForjarConfig,
pruned: &std::collections::HashSet<String>,
) {
println!("digraph {{");
println!(" rankdir=LR;");
for name in remaining {
println!(" \"{name}\";");
if let Some(res) = config.resources.get(*name) {
for dep in &res.depends_on {
if !pruned.contains(dep) {
println!(" \"{name}\" -> \"{dep}\";");
}
}
}
}
println!("}}");
}
fn print_prune_mermaid(
remaining: &[&String],
config: &types::ForjarConfig,
pruned: &std::collections::HashSet<String>,
) {
println!("graph LR");
for name in remaining {
if let Some(res) = config.resources.get(*name) {
for dep in &res.depends_on {
if !pruned.contains(dep) {
println!(" {name} --> {dep}");
}
}
}
}
}
pub(crate) fn cmd_graph_prune(file: &Path, format: &str, resource: &str) -> Result<(), String> {
let config = parse_and_validate(file)?;
let order = resolver::build_execution_order(&config)?;
let mut pruned = std::collections::HashSet::new();
pruned.insert(resource.to_string());
let mut changed = true;
while changed {
changed = false;
for name in &order {
if pruned.contains(name) {
continue;
}
if let Some(res) = config.resources.get(name) {
for dep in &res.depends_on {
if pruned.contains(dep) {
pruned.insert(name.clone());
changed = true;
break;
}
}
}
}
}
let remaining: Vec<&String> = order.iter().filter(|n| !pruned.contains(*n)).collect();
if format == "dot" {
print_prune_dot(&remaining, &config, &pruned);
} else {
print_prune_mermaid(&remaining, &config, &pruned);
}
println!(
"\n{} Pruned {} and {} dependent(s)",
dim("──"),
resource,
pruned.len() - 1
);
Ok(())
}
pub(crate) fn cmd_graph_layers(file: &Path) -> Result<(), String> {
let config = parse_and_validate(file)?;
let order = resolver::build_execution_order(&config)?;
let mut layers: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for name in &order {
if let Some(res) = config.resources.get(name) {
if res.depends_on.is_empty() {
layers.insert(name.clone(), 0);
}
}
}
let mut changed = true;
while changed {
changed = false;
for name in &order {
if layers.contains_key(name) {
continue;
}
if let Some(res) = config.resources.get(name) {
let max_dep = res
.depends_on
.iter()
.filter_map(|d| layers.get(d))
.max()
.copied();
if let Some(max) = max_dep {
layers.insert(name.clone(), max + 1);
changed = true;
}
}
}
}
let max_layer = layers.values().copied().max().unwrap_or(0);
for layer in 0..=max_layer {
let resources: Vec<&String> = order
.iter()
.filter(|n| layers.get(*n) == Some(&layer))
.collect();
if !resources.is_empty() {
println!(
"Layer {} ({}): {}",
layer,
resources.len(),
resources
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}
}
Ok(())
}
pub(crate) fn cmd_graph_critical_resources(file: &Path) -> Result<(), String> {
let config = parse_and_validate(file)?;
let mut dependent_count: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for (name, res) in &config.resources {
dependent_count.entry(name.clone()).or_insert(0);
for dep in &res.depends_on {
*dependent_count.entry(dep.clone()).or_insert(0) += 1;
}
}
let mut ranked: Vec<(String, usize)> = dependent_count.into_iter().collect();
ranked.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
println!("Critical Resources (most dependents first)");
println!("{}", "─".repeat(50));
for (name, count) in &ranked {
if *count == 0 {
continue;
}
println!(" {name:30} {count} dependent(s)");
}
if ranked.iter().all(|(_, c)| *c == 0) {
println!(" (no resources have dependents)");
}
Ok(())
}
fn print_weight_dot(
config: &types::ForjarConfig,
weights: &std::collections::HashMap<String, usize>,
) {
println!("digraph forjar {{");
println!(" rankdir=LR;");
for (name, res) in &config.resources {
let w = weights.get(name).unwrap_or(&0);
println!(" \"{name}\" [label=\"{name} (w={w})\"];");
for dep in &res.depends_on {
println!(" \"{name}\" -> \"{dep}\";");
}
}
println!("}}");
}
fn print_weight_mermaid(
config: &types::ForjarConfig,
weights: &std::collections::HashMap<String, usize>,
) {
println!("graph LR");
for (name, res) in &config.resources {
let w = weights.get(name).unwrap_or(&0);
for dep in &res.depends_on {
let dw = weights.get(dep.as_str()).unwrap_or(&0);
println!(" {name}[\"{name}(w={w})\"] --> {dep}[\"{dep}(w={dw})\"]");
}
if res.depends_on.is_empty() {
println!(" {name}[\"{name}(w={w})\"]");
}
}
}
pub(crate) fn cmd_graph_weight(file: &Path, format: &str) -> Result<(), String> {
let config = parse_and_validate(file)?;
let order = resolver::build_execution_order(&config)?;
let mut weights: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for name in &order {
weights.entry(name.clone()).or_insert(0);
if let Some(res) = config.resources.get(name) {
for dep in &res.depends_on {
*weights.entry(dep.clone()).or_insert(0) += 1;
}
}
}
if format == "dot" {
print_weight_dot(&config, &weights);
} else {
print_weight_mermaid(&config, &weights);
}
Ok(())
}
fn print_subgraph_dot(
resource: &str,
visited: &std::collections::HashSet<String>,
config: &types::ForjarConfig,
) {
println!("digraph subgraph_{resource} {{");
println!(" rankdir=LR;");
for name in visited {
if let Some(res) = config.resources.get(name) {
for dep in &res.depends_on {
if visited.contains(dep) {
println!(" \"{name}\" -> \"{dep}\";");
}
}
}
}
println!("}}");
}
fn print_subgraph_mermaid(
visited: &std::collections::HashSet<String>,
config: &types::ForjarConfig,
) {
println!("graph LR");
for name in visited {
if let Some(res) = config.resources.get(name) {
for dep in &res.depends_on {
if visited.contains(dep) {
println!(" {name} --> {dep}");
}
}
if res.depends_on.is_empty() {
println!(" {name}");
}
}
}
}
pub(crate) fn cmd_graph_subgraph(file: &Path, format: &str, resource: &str) -> Result<(), String> {
let config = parse_and_validate(file)?;
if !config.resources.contains_key(resource) {
return Err(format!("Resource '{resource}' not found"));
}
let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut stack = vec![resource.to_string()];
while let Some(name) = stack.pop() {
if !visited.insert(name.clone()) {
continue;
}
if let Some(res) = config.resources.get(&name) {
for dep in &res.depends_on {
stack.push(dep.clone());
}
}
}
if format == "dot" {
print_subgraph_dot(resource, &visited, &config);
} else {
print_subgraph_mermaid(&visited, &config);
}
Ok(())
}