use std::path::Path;
use provable_contracts::graph::{dependency_graph, graph_nodes, DependencyGraph};
use crate::contract_walk::collect_contracts;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphFormat {
Text,
Dot,
Json,
Mermaid,
}
impl GraphFormat {
pub fn from_str(s: &str) -> Result<Self, String> {
match s {
"text" => Ok(Self::Text),
"dot" => Ok(Self::Dot),
"json" => Ok(Self::Json),
"mermaid" => Ok(Self::Mermaid),
other => Err(format!(
"unknown format '{other}', expected 'text', 'dot', 'json', or 'mermaid'"
)),
}
}
}
pub fn run(contract_dir: &Path, format: GraphFormat) -> Result<(), Box<dyn std::error::Error>> {
let mut contracts = Vec::new();
collect_contracts(contract_dir, &mut contracts);
contracts.sort_by(|a, b| a.0.cmp(&b.0));
let refs: Vec<(String, &provable_contracts::schema::Contract)> =
contracts.iter().map(|(s, c)| (s.clone(), c)).collect();
let graph = dependency_graph(&refs);
match format {
GraphFormat::Text => render_text(&graph),
GraphFormat::Dot => render_dot(&graph),
GraphFormat::Json => render_json(&graph),
GraphFormat::Mermaid => render_mermaid(&graph),
}
if !graph.cycles.is_empty() {
return Err("Dependency graph contains cycles".into());
}
Ok(())
}
fn render_text(graph: &DependencyGraph) {
let nodes = graph_nodes(graph);
println!("Contract Dependency Graph");
println!("=========================");
println!("Nodes: {}", graph.nodes.len());
println!();
for node in &nodes {
let deps = graph.edges.get(&node.stem).cloned().unwrap_or_default();
if deps.is_empty() {
println!(" {} (dependents: {}, deps: 0)", node.stem, node.dependents);
} else {
println!(
" {} → [{}] (dependents: {})",
node.stem,
deps.join(", "),
node.dependents,
);
}
}
if !graph.cycles.is_empty() {
println!();
println!("CYCLES DETECTED:");
for cycle in &graph.cycles {
println!(" {}", cycle.join(" → "));
}
}
if !graph.topo_order.is_empty() {
println!();
println!("Topological order:");
for (i, node) in graph.topo_order.iter().enumerate() {
println!(" {}: {node}", i + 1);
}
}
}
fn render_dot(graph: &DependencyGraph) {
println!("digraph contracts {{");
println!(" rankdir=LR;");
println!(" node [shape=box, style=rounded, fontname=\"Helvetica\"];");
println!(" edge [color=\"#666666\"];");
println!();
for node in &graph.nodes {
let deps = graph.edges.get(node).map_or(0, Vec::len);
if deps == 0 && !is_depended_on(graph, node) {
println!(" \"{node}\";");
}
}
for (node, deps) in &graph.edges {
for dep in deps {
println!(" \"{node}\" -> \"{dep}\";");
}
}
if !graph.cycles.is_empty() {
println!();
println!(" // Cycles detected");
for cycle in &graph.cycles {
for pair in cycle.windows(2) {
println!(
" \"{0}\" -> \"{1}\" [color=red, penwidth=2];",
pair[0], pair[1]
);
}
}
}
println!("}}");
}
fn render_json(graph: &DependencyGraph) {
println!("{{");
let nodes_json: Vec<String> = graph.nodes.iter().map(|n| format!(" \"{n}\"")).collect();
println!(" \"nodes\": [");
println!("{}", nodes_json.join(",\n"));
println!(" ],");
println!(" \"edges\": [");
let mut edge_lines = Vec::new();
for (node, deps) in &graph.edges {
for dep in deps {
edge_lines.push(format!(" {{\"from\": \"{node}\", \"to\": \"{dep}\"}}"));
}
}
println!("{}", edge_lines.join(",\n"));
println!(" ],");
let topo_json: Vec<String> = graph
.topo_order
.iter()
.map(|n| format!(" \"{n}\""))
.collect();
println!(" \"topo_order\": [");
println!("{}", topo_json.join(",\n"));
println!(" ],");
println!(" \"cycles\": [");
let cycle_lines: Vec<String> = graph
.cycles
.iter()
.map(|c| {
let items: Vec<String> = c.iter().map(|n| format!("\"{n}\"")).collect();
format!(" [{}]", items.join(", "))
})
.collect();
println!("{}", cycle_lines.join(",\n"));
println!(" ]");
println!("}}");
}
fn render_mermaid(graph: &DependencyGraph) {
println!("graph TD");
for (node, deps) in &graph.edges {
let from = mermaid_id(node);
if deps.is_empty() {
println!(" {from}[\"{node}\"]");
}
for dep in deps {
let to = mermaid_id(dep);
println!(" {from}[\"{node}\"] --> {to}[\"{dep}\"]");
}
}
if !graph.cycles.is_empty() {
println!();
println!(" %% Cycles detected");
for cycle in &graph.cycles {
let names: Vec<&str> = cycle.iter().map(String::as_str).collect();
println!(" %% {}", names.join(" -> "));
}
}
}
fn mermaid_id(stem: &str) -> String {
stem.replace('-', "_")
}
fn is_depended_on(graph: &DependencyGraph, node: &str) -> bool {
graph
.edges
.values()
.any(|deps| deps.iter().any(|d| d == node))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graph_format_from_str() {
assert_eq!(GraphFormat::from_str("text").unwrap(), GraphFormat::Text);
assert_eq!(GraphFormat::from_str("dot").unwrap(), GraphFormat::Dot);
assert_eq!(GraphFormat::from_str("json").unwrap(), GraphFormat::Json);
assert_eq!(
GraphFormat::from_str("mermaid").unwrap(),
GraphFormat::Mermaid
);
assert!(GraphFormat::from_str("xml").is_err());
}
#[test]
fn test_from_str_other_format_returns_descriptive_error() {
let other = "yaml";
let err = GraphFormat::from_str(other).unwrap_err();
assert!(
err.contains(other),
"error should include the unrecognized format"
);
assert!(err.contains("unknown format"));
}
#[test]
fn test_mermaid_id() {
assert_eq!(mermaid_id("softmax-kernel-v1"), "softmax_kernel_v1");
assert_eq!(mermaid_id("silu"), "silu");
}
}