use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowDefinition {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default = "default_version")]
pub version: String,
#[serde(default)]
pub trigger: TriggerConfig,
#[serde(default)]
pub variables: HashMap<String, String>,
pub steps: Vec<WorkflowStep>,
}
fn default_version() -> String {
"1.0".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TriggerConfig {
#[serde(rename = "type", default = "default_trigger_type")]
pub trigger_type: String,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub event: Option<String>,
#[serde(default)]
pub cron: Option<String>,
}
fn default_trigger_type() -> String {
"manual".to_string()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum OnFailure {
#[default]
Stop,
Continue,
Retry,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowStep {
pub name: String,
pub specialist: String,
#[serde(default = "default_adapter")]
pub adapter: String,
#[serde(default)]
pub config: StepConfig,
#[serde(default)]
pub input: Option<String>,
#[serde(default)]
pub actions: Vec<StepAction>,
#[serde(default)]
pub output_key: Option<String>,
#[serde(default, rename = "if")]
pub condition: Option<String>,
#[serde(default)]
pub parallel_group: Option<String>,
#[serde(default)]
pub on_failure: OnFailure,
#[serde(default = "default_max_retries")]
pub max_retries: u32,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
fn default_max_retries() -> u32 {
2
}
fn default_timeout() -> u64 {
300
}
fn default_adapter() -> String {
"claude-code-sdk".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StepConfig {
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub max_turns: Option<u32>,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub api_key: Option<String>,
#[serde(default)]
pub temperature: Option<f64>,
#[serde(default)]
pub system_prompt: Option<String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub env: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StepAction {
Simple(String),
Detailed {
name: String,
#[serde(default)]
params: HashMap<String, serde_json::Value>,
},
}
impl std::fmt::Display for StepAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StepAction::Simple(s) => write!(f, "{}", s),
StepAction::Detailed { name, .. } => write!(f, "{}", name),
}
}
}
impl WorkflowDefinition {
pub fn from_yaml(yaml: &str) -> Result<Self, String> {
serde_yaml::from_str(yaml).map_err(|e| format!("Failed to parse workflow YAML: {}", e))
}
pub fn from_file(path: &str) -> Result<Self, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read workflow file '{}': {}", path, e))?;
Self::from_yaml(&content)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_workflow() {
let yaml = r#"
name: "Test Flow"
steps:
- name: "Step 1"
specialist: "developer"
input: "Hello, world!"
"#;
let wf = WorkflowDefinition::from_yaml(yaml).unwrap();
assert_eq!(wf.name, "Test Flow");
assert_eq!(wf.steps.len(), 1);
assert_eq!(wf.steps[0].specialist, "developer");
assert_eq!(wf.steps[0].adapter, "claude-code-sdk");
}
#[test]
fn test_parse_full_workflow() {
let yaml = r#"
name: "SDLC Flow"
description: "End-to-end development"
version: "2.0"
trigger:
type: webhook
source: github
event: issues.opened
variables:
model: "GLM-4.7"
base_url: "https://open.bigmodel.cn/api/anthropic"
steps:
- name: "Refine"
specialist: "issue-refiner"
adapter: "claude-code-sdk"
config:
model: "${model}"
input: "${trigger.payload}"
actions:
- analyze_requirements
- generate_acceptance_criteria
output_key: "refined"
- name: "Implement"
specialist: "crafter"
config:
model: "GLM-4.7"
input: "${steps.Refine.output}"
output_key: "implementation"
if: "${steps.Refine.output} != ''"
"#;
let wf = WorkflowDefinition::from_yaml(yaml).unwrap();
assert_eq!(wf.name, "SDLC Flow");
assert_eq!(wf.version, "2.0");
assert_eq!(wf.trigger.trigger_type, "webhook");
assert_eq!(wf.trigger.source, Some("github".to_string()));
assert_eq!(wf.variables.get("model").unwrap(), "GLM-4.7");
assert_eq!(wf.steps.len(), 2);
assert_eq!(wf.steps[0].actions.len(), 2);
assert!(wf.steps[1].condition.is_some());
}
}