use crate::{CanvasError, Chain, Chord, Group};
pub enum DagFormat {
Dot,
Mermaid,
Json,
Svg,
Png,
}
fn render_dot_to_svg(dot: &str) -> Result<String, CanvasError> {
use std::io::Write;
use std::process::{Command, Stdio};
let mut child = Command::new("dot")
.arg("-Tsvg")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
CanvasError::Invalid(format!(
"Failed to execute 'dot' command. Is GraphViz installed? Error: {}",
e
))
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(dot.as_bytes())
.map_err(|e| CanvasError::Invalid(format!("Failed to write DOT to stdin: {}", e)))?;
}
let output = child
.wait_with_output()
.map_err(|e| CanvasError::Invalid(format!("Failed to wait for dot process: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(CanvasError::Invalid(format!(
"dot command failed: {}",
stderr
)));
}
String::from_utf8(output.stdout)
.map_err(|e| CanvasError::Invalid(format!("Invalid UTF-8 in SVG output: {}", e)))
}
fn render_dot_to_png(dot: &str) -> Result<Vec<u8>, CanvasError> {
use std::io::Write;
use std::process::{Command, Stdio};
let mut child = Command::new("dot")
.arg("-Tpng")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
CanvasError::Invalid(format!(
"Failed to execute 'dot' command. Is GraphViz installed? Error: {}",
e
))
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(dot.as_bytes())
.map_err(|e| CanvasError::Invalid(format!("Failed to write DOT to stdin: {}", e)))?;
}
let output = child
.wait_with_output()
.map_err(|e| CanvasError::Invalid(format!("Failed to wait for dot process: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(CanvasError::Invalid(format!(
"dot command failed: {}",
stderr
)));
}
Ok(output.stdout)
}
#[allow(dead_code)]
pub fn is_graphviz_available() -> bool {
use std::process::Command;
Command::new("dot")
.arg("-V")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
pub trait DagExport {
fn to_dot(&self) -> String;
fn to_mermaid(&self) -> String;
fn to_json(&self) -> Result<String, serde_json::Error>;
fn to_svg(&self) -> Result<String, CanvasError> {
let dot = self.to_dot();
render_dot_to_svg(&dot)
}
fn to_png(&self) -> Result<Vec<u8>, CanvasError> {
let dot = self.to_dot();
render_dot_to_png(&dot)
}
fn svg_render_command(&self) -> String {
"dot -Tsvg -o output.svg input.dot".to_string()
}
fn png_render_command(&self) -> String {
"dot -Tpng -o output.png input.dot".to_string()
}
}
impl DagExport for Chain {
fn to_dot(&self) -> String {
let mut dot = String::from("digraph Chain {\n");
dot.push_str(" rankdir=LR;\n");
dot.push_str(" node [shape=box];\n\n");
for (i, task) in self.tasks.iter().enumerate() {
dot.push_str(&format!(" n{} [label=\"{}\"];\n", i, task.task));
if i > 0 {
dot.push_str(&format!(" n{} -> n{};\n", i - 1, i));
}
}
dot.push_str("}\n");
dot
}
fn to_mermaid(&self) -> String {
let mut mmd = String::from("graph LR\n");
for (i, task) in self.tasks.iter().enumerate() {
let node_id = format!("n{}", i);
mmd.push_str(&format!(" {}[\"{}\"]\n", node_id, task.task));
if i > 0 {
mmd.push_str(&format!(" n{} --> n{}\n", i - 1, i));
}
}
mmd
}
fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
impl DagExport for Group {
fn to_dot(&self) -> String {
let mut dot = String::from("digraph Group {\n");
dot.push_str(" rankdir=TB;\n");
dot.push_str(" node [shape=box];\n\n");
dot.push_str(" start [shape=circle, label=\"start\"];\n");
for (i, task) in self.tasks.iter().enumerate() {
dot.push_str(&format!(" n{} [label=\"{}\"];\n", i, task.task));
dot.push_str(&format!(" start -> n{};\n", i));
}
dot.push_str("}\n");
dot
}
fn to_mermaid(&self) -> String {
let mut mmd = String::from("graph TB\n");
mmd.push_str(" start((start))\n");
for (i, task) in self.tasks.iter().enumerate() {
mmd.push_str(&format!(" n{}[\"{}\"]\n", i, task.task));
mmd.push_str(&format!(" start --> n{}\n", i));
}
mmd
}
fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
impl DagExport for Chord {
fn to_dot(&self) -> String {
let mut dot = String::from("digraph Chord {\n");
dot.push_str(" rankdir=TB;\n");
dot.push_str(" node [shape=box];\n\n");
dot.push_str(" start [shape=circle, label=\"start\"];\n");
dot.push_str(&format!(
" callback [label=\"{}\", style=filled, fillcolor=lightblue];\n",
self.body.task
));
for (i, task) in self.header.tasks.iter().enumerate() {
dot.push_str(&format!(" n{} [label=\"{}\"];\n", i, task.task));
dot.push_str(&format!(" start -> n{};\n", i));
dot.push_str(&format!(" n{} -> callback;\n", i));
}
dot.push_str("}\n");
dot
}
fn to_mermaid(&self) -> String {
let mut mmd = String::from("graph TB\n");
mmd.push_str(" start((start))\n");
mmd.push_str(&format!(" callback[\"{}\"]\n", self.body.task));
mmd.push_str(" style callback fill:#add8e6\n");
for (i, task) in self.header.tasks.iter().enumerate() {
mmd.push_str(&format!(" n{}[\"{}\"]\n", i, task.task));
mmd.push_str(&format!(" start --> n{}\n", i));
mmd.push_str(&format!(" n{} --> callback\n", i));
}
mmd
}
fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}