use std::fmt;
use std::io::Write;
use std::path::Path;
use synaptic_core::SynapticError;
use crate::compiled::CompiledGraph;
use crate::state::State;
use crate::{END, START};
impl<S: State> CompiledGraph<S> {
pub fn draw_mermaid(&self) -> String {
let mut lines = vec!["graph TD".to_string()];
let mut node_names: Vec<&str> = self.nodes.keys().map(|s| s.as_str()).collect();
node_names.sort();
lines.push(format!(" {START}([\"{START}\"])"));
for name in &node_names {
lines.push(format!(" {name}[\"{name}\"]"));
}
lines.push(format!(" {END}([\"{END}\"])"));
lines.push(format!(" {START} --> {}", self.entry_point));
let mut fixed: Vec<(&str, &str)> = self
.edges
.iter()
.map(|e| (e.source.as_str(), e.target.as_str()))
.collect();
fixed.sort();
for (source, target) in fixed {
lines.push(format!(" {source} --> {target}"));
}
let mut cond_sources: Vec<&str> = self
.conditional_edges
.iter()
.map(|ce| ce.source.as_str())
.collect();
cond_sources.sort();
for source in cond_sources {
let ce = self
.conditional_edges
.iter()
.find(|ce| ce.source == source)
.unwrap();
match &ce.path_map {
Some(path_map) => {
let mut entries: Vec<(&String, &String)> = path_map.iter().collect();
entries.sort_by_key(|(label, _)| label.to_string());
for (label, target) in entries {
lines.push(format!(" {source} -.-> |{label}| {target}"));
}
}
None => {
lines.push(format!(
" %% {source} has conditional edge (path_map not provided)"
));
}
}
}
lines.join("\n")
}
pub fn draw_ascii(&self) -> String {
let mut lines = vec!["Graph:".to_string()];
let mut node_names: Vec<&str> = self.nodes.keys().map(|s| s.as_str()).collect();
node_names.sort();
lines.push(format!(" Nodes: {}", node_names.join(", ")));
lines.push(format!(" Entry: {START} -> {}", self.entry_point));
lines.push(" Edges:".to_string());
let mut fixed: Vec<(&str, &str)> = self
.edges
.iter()
.map(|e| (e.source.as_str(), e.target.as_str()))
.collect();
fixed.sort();
for (source, target) in fixed {
lines.push(format!(" {source} -> {target}"));
}
let mut cond_sources: Vec<&str> = self
.conditional_edges
.iter()
.map(|ce| ce.source.as_str())
.collect();
cond_sources.sort();
for source in cond_sources {
let ce = self
.conditional_edges
.iter()
.find(|ce| ce.source == source)
.unwrap();
match &ce.path_map {
Some(path_map) => {
let mut targets: Vec<&String> = path_map.values().collect();
targets.sort();
targets.dedup();
let targets_str = targets.iter().map(|t| t.as_str()).collect::<Vec<_>>();
lines.push(format!(
" {source} -> {} [conditional]",
targets_str.join(" | ")
));
}
None => {
lines.push(format!(" {source} -> ??? [conditional]"));
}
}
}
lines.join("\n")
}
pub fn draw_dot(&self) -> String {
let mut lines = vec!["digraph G {".to_string()];
lines.push(" rankdir=TD;".to_string());
let mut node_names: Vec<&str> = self.nodes.keys().map(|s| s.as_str()).collect();
node_names.sort();
lines.push(format!(" \"{START}\" [shape=oval];"));
for name in &node_names {
lines.push(format!(" \"{name}\" [shape=box];"));
}
lines.push(format!(" \"{END}\" [shape=oval];"));
lines.push(format!(
" \"{START}\" -> \"{}\" [style=solid];",
self.entry_point
));
let mut fixed: Vec<(&str, &str)> = self
.edges
.iter()
.map(|e| (e.source.as_str(), e.target.as_str()))
.collect();
fixed.sort();
for (source, target) in fixed {
lines.push(format!(" \"{source}\" -> \"{target}\" [style=solid];"));
}
let mut cond_sources: Vec<&str> = self
.conditional_edges
.iter()
.map(|ce| ce.source.as_str())
.collect();
cond_sources.sort();
for source in cond_sources {
let ce = self
.conditional_edges
.iter()
.find(|ce| ce.source == source)
.unwrap();
if let Some(ref path_map) = ce.path_map {
let mut entries: Vec<(&String, &String)> = path_map.iter().collect();
entries.sort_by_key(|(label, _)| label.to_string());
for (label, target) in entries {
lines.push(format!(
" \"{source}\" -> \"{target}\" [style=dashed, label=\"{label}\"];",
));
}
}
}
lines.push("}".to_string());
lines.join("\n")
}
pub async fn draw_mermaid_png(&self, path: impl AsRef<Path>) -> Result<(), SynapticError> {
self.fetch_mermaid_ink("img", path).await
}
pub async fn draw_mermaid_svg(&self, path: impl AsRef<Path>) -> Result<(), SynapticError> {
self.fetch_mermaid_ink("svg", path).await
}
async fn fetch_mermaid_ink(
&self,
endpoint: &str,
path: impl AsRef<Path>,
) -> Result<(), SynapticError> {
use base64::Engine;
let mermaid = self.draw_mermaid();
let encoded = base64::engine::general_purpose::URL_SAFE.encode(mermaid.as_bytes());
let url = format!("https://mermaid.ink/{endpoint}/{encoded}");
let response = reqwest::get(&url)
.await
.map_err(|e| SynapticError::Graph(format!("mermaid.ink request failed: {e}")))?;
if !response.status().is_success() {
return Err(SynapticError::Graph(format!(
"mermaid.ink returned status {}",
response.status()
)));
}
let bytes = response.bytes().await.map_err(|e| {
SynapticError::Graph(format!("failed to read mermaid.ink response: {e}"))
})?;
std::fs::write(path, &bytes)
.map_err(|e| SynapticError::Graph(format!("failed to write image file: {e}")))?;
Ok(())
}
pub fn draw_png(&self, path: impl AsRef<Path>) -> Result<(), SynapticError> {
let dot = self.draw_dot();
let mut child = std::process::Command::new("dot")
.args(["-Tpng"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| {
SynapticError::Graph(format!(
"failed to run 'dot' command (is Graphviz installed?): {e}"
))
})?;
child
.stdin
.take()
.unwrap()
.write_all(dot.as_bytes())
.map_err(|e| SynapticError::Graph(format!("failed to write to dot stdin: {e}")))?;
let output = child
.wait_with_output()
.map_err(|e| SynapticError::Graph(format!("dot command failed: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(SynapticError::Graph(format!(
"dot command failed: {stderr}"
)));
}
std::fs::write(path, &output.stdout)
.map_err(|e| SynapticError::Graph(format!("failed to write PNG file: {e}")))?;
Ok(())
}
}
impl<S: State> fmt::Display for CompiledGraph<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.draw_ascii())
}
}