use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WorkflowConfig {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default = "default_version")]
pub version: String,
pub steps: Vec<StepConfig>,
#[serde(default)]
pub variables: HashMap<String, String>,
#[serde(default)]
pub timeout_secs: Option<u64>,
#[serde(default)]
pub default_retry_policy: RetryPolicy,
#[serde(default)]
pub continue_on_failure: bool,
}
fn default_version() -> String {
"1.0".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StepConfig {
pub id: String,
pub description: String,
#[serde(default)]
pub depends_on: Vec<String>,
#[serde(default)]
pub tool: Option<String>,
#[serde(default)]
pub tool_args: Option<serde_json::Value>,
#[serde(default)]
pub prompt: Option<String>,
#[serde(default)]
pub requires_approval: bool,
#[serde(default)]
pub timeout_secs: Option<u64>,
#[serde(default)]
pub retry_policy: Option<RetryPolicy>,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RetryPolicy {
#[serde(default = "default_max_retries")]
pub max_retries: usize,
#[serde(default = "default_retry_delay_secs")]
pub retry_delay_secs: u64,
#[serde(default)]
pub retry_on_all_errors: bool,
}
fn default_max_retries() -> usize {
3
}
fn default_retry_delay_secs() -> u64 {
5
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: default_max_retries(),
retry_delay_secs: default_retry_delay_secs(),
retry_on_all_errors: false,
}
}
}
impl WorkflowConfig {
pub fn new(name: impl Into<String>, steps: Vec<StepConfig>) -> Self {
Self {
name: name.into(),
description: String::new(),
version: default_version(),
steps,
variables: HashMap::new(),
timeout_secs: None,
default_retry_policy: RetryPolicy::default(),
continue_on_failure: false,
}
}
pub fn from_json(json: &str) -> serde_json::Result<Self> {
serde_json::from_str(json)
}
pub fn to_json(&self) -> serde_json::Result<String> {
serde_json::to_string_pretty(self)
}
pub fn step_ids(&self) -> Vec<String> {
self.steps.iter().map(|s| s.id.clone()).collect()
}
pub fn get_step(&self, id: &str) -> Option<&StepConfig> {
self.steps.iter().find(|s| s.id == id)
}
pub fn effective_retry_policy(&self, step_id: &str) -> RetryPolicy {
self.get_step(step_id)
.and_then(|s| s.retry_policy.clone())
.unwrap_or_else(|| self.default_retry_policy.clone())
}
pub fn effective_timeout(&self, step_id: &str) -> Option<u64> {
self.get_step(step_id)
.and_then(|s| s.timeout_secs)
.or(self.timeout_secs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_workflow_config_creation() {
let config = WorkflowConfig::new(
"test_workflow",
vec![
StepConfig {
id: "step1".to_string(),
description: "First step".to_string(),
depends_on: vec![],
tool: None,
tool_args: None,
prompt: Some("Do something".to_string()),
requires_approval: false,
timeout_secs: None,
retry_policy: None,
metadata: HashMap::new(),
},
StepConfig {
id: "step2".to_string(),
description: "Second step".to_string(),
depends_on: vec!["step1".to_string()],
tool: Some("calculator".to_string()),
tool_args: Some(serde_json::json!({"expression": "2+2"})),
prompt: None,
requires_approval: false,
timeout_secs: None,
retry_policy: None,
metadata: HashMap::new(),
},
],
);
assert_eq!(config.name, "test_workflow");
assert_eq!(config.steps.len(), 2);
assert_eq!(config.step_ids(), vec!["step1", "step2"]);
}
#[test]
fn test_workflow_config_serialization() {
let config = WorkflowConfig::new(
"test_workflow",
vec![StepConfig {
id: "step1".to_string(),
description: "First step".to_string(),
depends_on: vec![],
tool: None,
tool_args: None,
prompt: Some("Calculate something".to_string()),
requires_approval: false,
timeout_secs: Some(30),
retry_policy: None,
metadata: HashMap::new(),
}],
);
let json = config.to_json().unwrap();
let parsed: WorkflowConfig = WorkflowConfig::from_json(&json).unwrap();
assert_eq!(parsed, config);
}
#[test]
fn test_effective_retry_policy() {
let config = WorkflowConfig::new(
"test",
vec![
StepConfig {
id: "step1".to_string(),
description: "Step 1".to_string(),
depends_on: vec![],
tool: None,
tool_args: None,
prompt: None,
requires_approval: false,
timeout_secs: None,
retry_policy: Some(RetryPolicy {
max_retries: 5,
retry_delay_secs: 10,
retry_on_all_errors: true,
}),
metadata: HashMap::new(),
},
StepConfig {
id: "step2".to_string(),
description: "Step 2".to_string(),
depends_on: vec!["step1".to_string()],
tool: None,
tool_args: None,
prompt: None,
requires_approval: false,
timeout_secs: None,
retry_policy: None,
metadata: HashMap::new(),
},
],
);
let policy1 = config.effective_retry_policy("step1");
assert_eq!(policy1.max_retries, 5);
let policy2 = config.effective_retry_policy("step2");
assert_eq!(policy2.max_retries, 3);
}
#[test]
fn test_effective_timeout() {
let mut config = WorkflowConfig::new(
"test",
vec![StepConfig {
id: "step1".to_string(),
description: "Step 1".to_string(),
depends_on: vec![],
tool: None,
tool_args: None,
prompt: None,
requires_approval: false,
timeout_secs: Some(60),
retry_policy: None,
metadata: HashMap::new(),
}],
);
config.timeout_secs = Some(120);
assert_eq!(config.effective_timeout("step1"), Some(60));
assert_eq!(config.effective_timeout("nonexistent"), Some(120));
}
}