use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct WorkflowDefinition {
pub name: String,
pub description: String,
pub steps: Vec<WorkflowStep>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct WorkflowStep {
pub name: String,
#[serde(rename = "type")]
pub step_type: WorkflowStepType,
#[serde(skip_serializing_if = "Option::is_none")]
pub layer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parse_pattern: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub substeps: Vec<WorkflowStep>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_iterations: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub condition_pattern: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub on_match: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub on_no_match: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parallel_layers: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aggregator: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum WorkflowStepType {
Once, Loop, Foreach, Conditional, Parallel, }
impl WorkflowDefinition {
pub fn validate(&self) -> Result<(), String> {
if self.name.trim().is_empty() {
return Err("Workflow name cannot be empty".to_string());
}
if self.steps.is_empty() {
return Err(format!("Workflow '{}' has no steps", self.name));
}
for (i, step) in self.steps.iter().enumerate() {
step.validate(&format!("{}[{}]", self.name, i))?;
}
Ok(())
}
}
impl WorkflowStep {
pub fn validate(&self, path: &str) -> Result<(), String> {
match self.step_type {
WorkflowStepType::Once => {
if self.layer.is_none() {
return Err(format!("{}: Once requires 'layer'", path));
}
}
WorkflowStepType::Loop => {
if self.substeps.is_empty() {
return Err(format!("{}: Loop requires 'substeps'", path));
}
if self.exit_pattern.is_none() {
return Err(format!("{}: Loop requires 'exit_pattern'", path));
}
}
WorkflowStepType::Foreach => {
if self.parse_pattern.is_none() {
return Err(format!("{}: Foreach requires 'parse_pattern'", path));
}
if self.substeps.is_empty() {
return Err(format!("{}: Foreach requires 'substeps'", path));
}
}
WorkflowStepType::Conditional => {
if self.layer.is_none() {
return Err(format!("{}: Conditional requires 'layer'", path));
}
if self.condition_pattern.is_none() {
return Err(format!(
"{}: Conditional requires 'condition_pattern'",
path
));
}
}
WorkflowStepType::Parallel => {
if self.parallel_layers.is_empty() {
return Err(format!("{}: Parallel requires 'parallel_layers'", path));
}
}
}
for (i, substep) in self.substeps.iter().enumerate() {
substep.validate(&format!("{}[{}]", path, i))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_workflow_validation_empty_steps() {
let workflow = WorkflowDefinition {
name: "test".to_string(),
description: "Test".to_string(),
steps: vec![],
};
assert!(workflow.validate().is_err());
}
#[test]
fn test_workflow_validation_empty_name() {
let workflow = WorkflowDefinition {
name: "".to_string(),
description: "Test".to_string(),
steps: vec![WorkflowStep {
name: "test".to_string(),
step_type: WorkflowStepType::Once,
layer: Some("test_layer".to_string()),
parse_pattern: None,
substeps: Vec::new(),
max_iterations: None,
exit_pattern: None,
condition_pattern: None,
on_match: Vec::new(),
on_no_match: Vec::new(),
parallel_layers: Vec::new(),
aggregator: None,
}],
};
assert!(workflow.validate().is_err());
}
#[test]
fn test_once_step_validation() {
let step = WorkflowStep {
name: "test".to_string(),
step_type: WorkflowStepType::Once,
layer: Some("test_layer".to_string()),
parse_pattern: None,
substeps: Vec::new(),
max_iterations: None,
exit_pattern: None,
condition_pattern: None,
on_match: Vec::new(),
on_no_match: Vec::new(),
parallel_layers: Vec::new(),
aggregator: None,
};
assert!(step.validate("test").is_ok());
}
#[test]
fn test_loop_step_missing_exit_pattern() {
let step = WorkflowStep {
name: "test".to_string(),
step_type: WorkflowStepType::Loop,
layer: None,
parse_pattern: None,
substeps: vec![],
max_iterations: Some(5),
exit_pattern: None, condition_pattern: None,
on_match: Vec::new(),
on_no_match: Vec::new(),
parallel_layers: Vec::new(),
aggregator: None,
};
assert!(step.validate("test").is_err());
}
}