use crate::{Chain, Chord, Group, WorkflowCheckpoint, WorkflowEvent, WorkflowState};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowSnapshot {
pub snapshot_id: Uuid,
pub workflow_id: Uuid,
pub timestamp: u64,
pub state: WorkflowState,
pub completed_tasks: Vec<Uuid>,
pub task_results: HashMap<Uuid, serde_json::Value>,
pub checkpoint: Option<WorkflowCheckpoint>,
}
impl WorkflowSnapshot {
pub fn new(workflow_id: Uuid, state: WorkflowState) -> Self {
Self {
snapshot_id: Uuid::new_v4(),
workflow_id,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
state,
completed_tasks: Vec::new(),
task_results: HashMap::new(),
checkpoint: None,
}
}
pub fn record_task(&mut self, task_id: Uuid, result: serde_json::Value) {
self.completed_tasks.push(task_id);
self.task_results.insert(task_id, result);
}
pub fn with_checkpoint(mut self, checkpoint: WorkflowCheckpoint) -> Self {
self.checkpoint = Some(checkpoint);
self
}
}
#[derive(Debug, Clone)]
pub struct TimeTravelDebugger {
pub workflow_id: Uuid,
pub snapshots: Vec<WorkflowSnapshot>,
pub current_index: usize,
pub step_mode: bool,
}
impl TimeTravelDebugger {
pub fn new(workflow_id: Uuid) -> Self {
Self {
workflow_id,
snapshots: Vec::new(),
current_index: 0,
step_mode: false,
}
}
pub fn record_snapshot(&mut self, snapshot: WorkflowSnapshot) {
self.snapshots.push(snapshot);
self.current_index = self.snapshots.len() - 1;
}
pub fn replay_from(&mut self, snapshot_index: usize) -> Option<&WorkflowSnapshot> {
if snapshot_index < self.snapshots.len() {
self.current_index = snapshot_index;
self.snapshots.get(snapshot_index)
} else {
None
}
}
pub fn step_forward(&mut self) -> Option<&WorkflowSnapshot> {
if self.current_index + 1 < self.snapshots.len() {
self.current_index += 1;
self.snapshots.get(self.current_index)
} else {
None
}
}
pub fn step_backward(&mut self) -> Option<&WorkflowSnapshot> {
if self.current_index > 0 {
self.current_index -= 1;
self.snapshots.get(self.current_index)
} else {
None
}
}
pub fn current_snapshot(&self) -> Option<&WorkflowSnapshot> {
self.snapshots.get(self.current_index)
}
pub fn enable_step_mode(&mut self) {
self.step_mode = true;
}
pub fn disable_step_mode(&mut self) {
self.step_mode = false;
}
pub fn snapshot_count(&self) -> usize {
self.snapshots.len()
}
pub fn clear(&mut self) {
self.snapshots.clear();
self.current_index = 0;
}
}
impl std::fmt::Display for TimeTravelDebugger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"TimeTravelDebugger[workflow={}, snapshots={}, current={}]",
self.workflow_id,
self.snapshots.len(),
self.current_index
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VisualTheme {
pub name: String,
pub colors: HashMap<String, String>,
pub shapes: HashMap<String, String>,
pub edge_styles: HashMap<String, String>,
pub font_family: String,
pub font_size: u8,
}
impl VisualTheme {
pub fn light() -> Self {
let mut colors = HashMap::new();
colors.insert("pending".to_string(), "#E0E0E0".to_string());
colors.insert("running".to_string(), "#2196F3".to_string());
colors.insert("completed".to_string(), "#4CAF50".to_string());
colors.insert("failed".to_string(), "#F44336".to_string());
colors.insert("cancelled".to_string(), "#FF9800".to_string());
let mut shapes = HashMap::new();
shapes.insert("task".to_string(), "box".to_string());
shapes.insert("group".to_string(), "ellipse".to_string());
shapes.insert("chord".to_string(), "diamond".to_string());
let mut edge_styles = HashMap::new();
edge_styles.insert("chain".to_string(), "solid".to_string());
edge_styles.insert("callback".to_string(), "dashed".to_string());
edge_styles.insert("error".to_string(), "dotted".to_string());
Self {
name: "light".to_string(),
colors,
shapes,
edge_styles,
font_family: "Arial".to_string(),
font_size: 12,
}
}
pub fn dark() -> Self {
let mut colors = HashMap::new();
colors.insert("pending".to_string(), "#424242".to_string());
colors.insert("running".to_string(), "#1976D2".to_string());
colors.insert("completed".to_string(), "#388E3C".to_string());
colors.insert("failed".to_string(), "#D32F2F".to_string());
colors.insert("cancelled".to_string(), "#F57C00".to_string());
let mut shapes = HashMap::new();
shapes.insert("task".to_string(), "box".to_string());
shapes.insert("group".to_string(), "ellipse".to_string());
shapes.insert("chord".to_string(), "diamond".to_string());
let mut edge_styles = HashMap::new();
edge_styles.insert("chain".to_string(), "solid".to_string());
edge_styles.insert("callback".to_string(), "dashed".to_string());
edge_styles.insert("error".to_string(), "dotted".to_string());
Self {
name: "dark".to_string(),
colors,
shapes,
edge_styles,
font_family: "Arial".to_string(),
font_size: 12,
}
}
pub fn color_for_state(&self, state: &str) -> Option<&str> {
self.colors.get(state).map(|s| s.as_str())
}
pub fn shape_for_type(&self, task_type: &str) -> Option<&str> {
self.shapes.get(task_type).map(|s| s.as_str())
}
}
impl Default for VisualTheme {
fn default() -> Self {
Self::light()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskVisualMetadata {
pub task_id: Uuid,
pub task_name: String,
pub state: String,
pub progress: f64,
pub position: Option<(f64, f64)>,
pub color: String,
pub shape: String,
pub css_classes: Vec<String>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl TaskVisualMetadata {
pub fn new(task_id: Uuid, task_name: String, state: String) -> Self {
Self {
task_id,
task_name,
state: state.clone(),
progress: 0.0,
position: None,
color: Self::default_color_for_state(&state),
shape: "box".to_string(),
css_classes: vec![format!("task-{}", state)],
metadata: HashMap::new(),
}
}
fn default_color_for_state(state: &str) -> String {
match state {
"pending" => "#E0E0E0",
"running" => "#2196F3",
"completed" => "#4CAF50",
"failed" => "#F44336",
"cancelled" => "#FF9800",
_ => "#9E9E9E",
}
.to_string()
}
pub fn with_progress(mut self, progress: f64) -> Self {
self.progress = progress.clamp(0.0, 100.0);
self
}
pub fn with_position(mut self, x: f64, y: f64) -> Self {
self.position = Some((x, y));
self
}
pub fn with_color(mut self, color: String) -> Self {
self.color = color;
self
}
pub fn add_css_class(&mut self, class: String) {
if !self.css_classes.contains(&class) {
self.css_classes.push(class);
}
}
pub fn add_metadata(&mut self, key: String, value: serde_json::Value) {
self.metadata.insert(key, value);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowVisualizationData {
pub workflow_id: Uuid,
pub workflow_name: String,
pub state: WorkflowState,
pub tasks: Vec<TaskVisualMetadata>,
pub edges: Vec<(Uuid, Uuid, String)>,
pub theme: VisualTheme,
pub layout_hint: String,
pub viewport: (f64, f64),
}
impl WorkflowVisualizationData {
pub fn new(workflow_id: Uuid, workflow_name: String, state: WorkflowState) -> Self {
Self {
workflow_id,
workflow_name,
state,
tasks: Vec::new(),
edges: Vec::new(),
theme: VisualTheme::default(),
layout_hint: "hierarchical".to_string(),
viewport: (1000.0, 600.0),
}
}
pub fn add_task(&mut self, task: TaskVisualMetadata) {
self.tasks.push(task);
}
pub fn add_edge(&mut self, from: Uuid, to: Uuid, edge_type: String) {
self.edges.push((from, to, edge_type));
}
pub fn with_theme(mut self, theme: VisualTheme) -> Self {
self.theme = theme;
self
}
pub fn with_layout(mut self, layout_hint: String) -> Self {
self.layout_hint = layout_hint;
self
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn to_visjs_format(&self) -> serde_json::Value {
let nodes: Vec<serde_json::Value> = self
.tasks
.iter()
.map(|task| {
serde_json::json!({
"id": task.task_id.to_string(),
"label": task.task_name,
"color": task.color,
"shape": task.shape,
"title": format!("{} ({})", task.task_name, task.state),
"value": task.progress,
})
})
.collect();
let edges: Vec<serde_json::Value> = self
.edges
.iter()
.map(|(from, to, edge_type)| {
serde_json::json!({
"from": from.to_string(),
"to": to.to_string(),
"arrows": "to",
"dashes": edge_type == "callback",
})
})
.collect();
serde_json::json!({
"nodes": nodes,
"edges": edges,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineEntry {
pub task_id: Uuid,
pub task_name: String,
pub start_time: u64,
pub end_time: Option<u64>,
pub duration: Option<u64>,
pub state: String,
pub worker_id: Option<String>,
pub parent_id: Option<Uuid>,
pub color: String,
}
impl TimelineEntry {
pub fn new(task_id: Uuid, task_name: String, start_time: u64) -> Self {
Self {
task_id,
task_name,
start_time,
end_time: None,
duration: None,
state: "running".to_string(),
worker_id: None,
parent_id: None,
color: "#2196F3".to_string(),
}
}
pub fn complete(&mut self, end_time: u64) {
self.end_time = Some(end_time);
self.duration = Some(end_time.saturating_sub(self.start_time));
self.state = "completed".to_string();
self.color = "#4CAF50".to_string();
}
pub fn fail(&mut self, end_time: u64) {
self.end_time = Some(end_time);
self.duration = Some(end_time.saturating_sub(self.start_time));
self.state = "failed".to_string();
self.color = "#F44336".to_string();
}
pub fn with_worker(mut self, worker_id: String) -> Self {
self.worker_id = Some(worker_id);
self
}
pub fn with_parent(mut self, parent_id: Uuid) -> Self {
self.parent_id = Some(parent_id);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionTimeline {
pub workflow_id: Uuid,
pub entries: Vec<TimelineEntry>,
pub workflow_start: u64,
pub workflow_end: Option<u64>,
}
impl ExecutionTimeline {
pub fn new(workflow_id: Uuid) -> Self {
Self {
workflow_id,
entries: Vec::new(),
workflow_start: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64,
workflow_end: None,
}
}
pub fn add_entry(&mut self, entry: TimelineEntry) {
self.entries.push(entry);
}
pub fn start_task(&mut self, task_id: Uuid, task_name: String) -> usize {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let entry = TimelineEntry::new(task_id, task_name, now);
self.entries.push(entry);
self.entries.len() - 1
}
pub fn complete_task(&mut self, index: usize) {
if let Some(entry) = self.entries.get_mut(index) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
entry.complete(now);
}
}
pub fn fail_task(&mut self, index: usize) {
if let Some(entry) = self.entries.get_mut(index) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
entry.fail(now);
}
}
pub fn complete_workflow(&mut self) {
self.workflow_end = Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64,
);
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn to_google_charts_format(&self) -> serde_json::Value {
let rows: Vec<serde_json::Value> = self
.entries
.iter()
.map(|entry| {
serde_json::json!([
entry.task_name,
entry.task_name,
entry.start_time,
entry.end_time.unwrap_or(entry.start_time),
])
})
.collect();
serde_json::json!({
"cols": [
{"id": "", "label": "Task ID", "type": "string"},
{"id": "", "label": "Task Name", "type": "string"},
{"id": "", "label": "Start", "type": "number"},
{"id": "", "label": "End", "type": "number"}
],
"rows": rows.iter().map(|row| serde_json::json!({"c": row})).collect::<Vec<_>>()
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimationFrame {
pub frame_number: usize,
pub timestamp: u64,
pub workflow_state: WorkflowState,
pub task_states: HashMap<Uuid, String>,
pub active_tasks: Vec<Uuid>,
pub completed_tasks: Vec<Uuid>,
pub events: Vec<WorkflowEvent>,
}
impl AnimationFrame {
pub fn new(frame_number: usize, workflow_state: WorkflowState) -> Self {
Self {
frame_number,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64,
workflow_state,
task_states: HashMap::new(),
active_tasks: Vec::new(),
completed_tasks: Vec::new(),
events: Vec::new(),
}
}
pub fn set_task_state(&mut self, task_id: Uuid, state: String) {
self.task_states.insert(task_id, state);
}
pub fn add_active_task(&mut self, task_id: Uuid) {
if !self.active_tasks.contains(&task_id) {
self.active_tasks.push(task_id);
}
}
pub fn add_completed_task(&mut self, task_id: Uuid) {
if !self.completed_tasks.contains(&task_id) {
self.completed_tasks.push(task_id);
}
self.active_tasks.retain(|id| id != &task_id);
}
pub fn add_event(&mut self, event: WorkflowEvent) {
self.events.push(event);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowAnimation {
pub workflow_id: Uuid,
pub frames: Vec<AnimationFrame>,
pub frame_duration: u64,
pub total_duration: u64,
}
impl WorkflowAnimation {
pub fn new(workflow_id: Uuid, frame_duration: u64) -> Self {
Self {
workflow_id,
frames: Vec::new(),
frame_duration,
total_duration: 0,
}
}
pub fn add_frame(&mut self, frame: AnimationFrame) {
self.frames.push(frame);
self.total_duration = self.frames.len() as u64 * self.frame_duration;
}
pub fn get_frame(&self, index: usize) -> Option<&AnimationFrame> {
self.frames.get(index)
}
pub fn frame_count(&self) -> usize {
self.frames.len()
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
pub trait DagExportWithState {
fn to_dot_with_state(
&self,
state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> String;
fn to_mermaid_with_state(
&self,
state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> String;
fn to_json_with_state(
&self,
state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> Result<String, serde_json::Error>;
}
impl DagExportWithState for Chain {
fn to_dot_with_state(
&self,
_state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> String {
let mut dot = String::from("digraph Chain {\n");
dot.push_str(" rankdir=LR;\n");
for (i, sig) in self.tasks.iter().enumerate() {
let task_id = sig.options.task_id.unwrap_or_else(Uuid::new_v4);
let state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
let color = match state {
"completed" => "#4CAF50",
"running" => "#2196F3",
"failed" => "#F44336",
_ => "#E0E0E0",
};
dot.push_str(&format!(
" task{} [label=\"{}\" style=filled fillcolor=\"{}\"];\n",
i, sig.task, color
));
if i > 0 {
dot.push_str(&format!(" task{} -> task{};\n", i - 1, i));
}
}
dot.push('}');
dot
}
fn to_mermaid_with_state(
&self,
_state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> String {
let mut mmd = String::from("graph LR\n");
for (i, sig) in self.tasks.iter().enumerate() {
let task_id = sig.options.task_id.unwrap_or_else(Uuid::new_v4);
let state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
let style_class = match state {
"completed" => "completed",
"running" => "running",
"failed" => "failed",
_ => "pending",
};
mmd.push_str(&format!(
" task{}[\"{}\"]:::{}\n",
i, sig.task, style_class
));
if i > 0 {
mmd.push_str(&format!(" task{} --> task{}\n", i - 1, i));
}
}
mmd.push_str("\n classDef completed fill:#4CAF50,stroke:#333,stroke-width:2px\n");
mmd.push_str(" classDef running fill:#2196F3,stroke:#333,stroke-width:2px\n");
mmd.push_str(" classDef failed fill:#F44336,stroke:#333,stroke-width:2px\n");
mmd.push_str(" classDef pending fill:#E0E0E0,stroke:#333,stroke-width:2px\n");
mmd
}
fn to_json_with_state(
&self,
state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> Result<String, serde_json::Error> {
let mut nodes = Vec::new();
let mut edges = Vec::new();
for (i, sig) in self.tasks.iter().enumerate() {
let task_id = sig.options.task_id.unwrap_or_else(Uuid::new_v4);
let task_state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
nodes.push(serde_json::json!({
"id": format!("task{}", i),
"label": sig.task,
"state": task_state,
"task_id": task_id,
}));
if i > 0 {
edges.push(serde_json::json!({
"from": format!("task{}", i - 1),
"to": format!("task{}", i),
}));
}
}
let result = serde_json::json!({
"type": "chain",
"workflow_state": state,
"nodes": nodes,
"edges": edges,
});
serde_json::to_string_pretty(&result)
}
}
impl DagExportWithState for Group {
fn to_dot_with_state(
&self,
_state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> String {
let mut dot = String::from("digraph Group {\n");
for (i, sig) in self.tasks.iter().enumerate() {
let task_id = sig.options.task_id.unwrap_or_else(Uuid::new_v4);
let state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
let color = match state {
"completed" => "#4CAF50",
"running" => "#2196F3",
"failed" => "#F44336",
_ => "#E0E0E0",
};
dot.push_str(&format!(
" task{} [label=\"{}\" style=filled fillcolor=\"{}\"];\n",
i, sig.task, color
));
}
dot.push('}');
dot
}
fn to_mermaid_with_state(
&self,
_state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> String {
let mut mmd = String::from("graph TB\n");
for (i, sig) in self.tasks.iter().enumerate() {
let task_id = sig.options.task_id.unwrap_or_else(Uuid::new_v4);
let state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
let style_class = match state {
"completed" => "completed",
"running" => "running",
"failed" => "failed",
_ => "pending",
};
mmd.push_str(&format!(
" task{}[\"{}\"]:::{}\n",
i, sig.task, style_class
));
}
mmd.push_str("\n classDef completed fill:#4CAF50,stroke:#333,stroke-width:2px\n");
mmd.push_str(" classDef running fill:#2196F3,stroke:#333,stroke-width:2px\n");
mmd.push_str(" classDef failed fill:#F44336,stroke:#333,stroke-width:2px\n");
mmd.push_str(" classDef pending fill:#E0E0E0,stroke:#333,stroke-width:2px\n");
mmd
}
fn to_json_with_state(
&self,
state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> Result<String, serde_json::Error> {
let mut nodes = Vec::new();
for (i, sig) in self.tasks.iter().enumerate() {
let task_id = sig.options.task_id.unwrap_or_else(Uuid::new_v4);
let task_state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
nodes.push(serde_json::json!({
"id": format!("task{}", i),
"label": sig.task,
"state": task_state,
"task_id": task_id,
}));
}
let result = serde_json::json!({
"type": "group",
"workflow_state": state,
"nodes": nodes,
"edges": [],
});
serde_json::to_string_pretty(&result)
}
}
impl DagExportWithState for Chord {
fn to_dot_with_state(
&self,
_state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> String {
let mut dot = String::from("digraph Chord {\n");
dot.push_str(" rankdir=LR;\n");
for (i, sig) in self.header.tasks.iter().enumerate() {
let task_id = sig.options.task_id.unwrap_or_else(Uuid::new_v4);
let state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
let color = match state {
"completed" => "#4CAF50",
"running" => "#2196F3",
"failed" => "#F44336",
_ => "#E0E0E0",
};
dot.push_str(&format!(
" task{} [label=\"{}\" style=filled fillcolor=\"{}\"];\n",
i, sig.task, color
));
}
let task_id = self.body.options.task_id.unwrap_or_else(Uuid::new_v4);
let state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
let color = match state {
"completed" => "#4CAF50",
"running" => "#2196F3",
"failed" => "#F44336",
_ => "#E0E0E0",
};
dot.push_str(&format!(
" callback [label=\"{}\" shape=diamond style=filled fillcolor=\"{}\"];\n",
self.body.task, color
));
for i in 0..self.header.tasks.len() {
dot.push_str(&format!(" task{} -> callback;\n", i));
}
dot.push('}');
dot
}
fn to_mermaid_with_state(
&self,
_state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> String {
let mut mmd = String::from("graph TB\n");
for (i, sig) in self.header.tasks.iter().enumerate() {
let task_id = sig.options.task_id.unwrap_or_else(Uuid::new_v4);
let state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
let style_class = match state {
"completed" => "completed",
"running" => "running",
"failed" => "failed",
_ => "pending",
};
mmd.push_str(&format!(
" task{}[\"{}\"]:::{}\n",
i, sig.task, style_class
));
}
let task_id = self.body.options.task_id.unwrap_or_else(Uuid::new_v4);
let state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
let style_class = match state {
"completed" => "completed",
"running" => "running",
"failed" => "failed",
_ => "pending",
};
mmd.push_str(&format!(
" callback{{\"{}\"}}:::{}\n",
self.body.task, style_class
));
for i in 0..self.header.tasks.len() {
mmd.push_str(&format!(" task{} --> callback\n", i));
}
mmd.push_str("\n classDef completed fill:#4CAF50,stroke:#333,stroke-width:2px\n");
mmd.push_str(" classDef running fill:#2196F3,stroke:#333,stroke-width:2px\n");
mmd.push_str(" classDef failed fill:#F44336,stroke:#333,stroke-width:2px\n");
mmd.push_str(" classDef pending fill:#E0E0E0,stroke:#333,stroke-width:2px\n");
mmd
}
fn to_json_with_state(
&self,
state: &WorkflowState,
task_states: &HashMap<Uuid, String>,
) -> Result<String, serde_json::Error> {
let mut nodes = Vec::new();
let mut edges = Vec::new();
for (i, sig) in self.header.tasks.iter().enumerate() {
let task_id = sig.options.task_id.unwrap_or_else(Uuid::new_v4);
let task_state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
nodes.push(serde_json::json!({
"id": format!("task{}", i),
"label": sig.task,
"state": task_state,
"task_id": task_id,
}));
}
let task_id = self.body.options.task_id.unwrap_or_else(Uuid::new_v4);
let task_state = task_states
.get(&task_id)
.map(|s| s.as_str())
.unwrap_or("pending");
nodes.push(serde_json::json!({
"id": "callback",
"label": self.body.task,
"state": task_state,
"task_id": task_id,
"shape": "diamond",
}));
for i in 0..self.header.tasks.len() {
edges.push(serde_json::json!({
"from": format!("task{}", i),
"to": "callback",
}));
}
let result = serde_json::json!({
"type": "chord",
"workflow_state": state,
"nodes": nodes,
"edges": edges,
});
serde_json::to_string_pretty(&result)
}
}