Skip to main content

oxigdal_workflow/conditional/
branching.rs

1//! Conditional branching logic for workflows.
2
3use crate::conditional::expressions::{Expression, ExpressionContext};
4use crate::error::{Result, WorkflowError};
5use serde::{Deserialize, Serialize};
6
7/// Conditional branch definition.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ConditionalBranch {
10    /// Branch condition.
11    pub condition: Expression,
12    /// Tasks to execute if condition is true.
13    pub then_tasks: Vec<String>,
14    /// Tasks to execute if condition is false (optional).
15    pub else_tasks: Option<Vec<String>>,
16}
17
18/// Switch-case conditional structure.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SwitchCase {
21    /// Variable to switch on.
22    pub variable: String,
23    /// Cases.
24    pub cases: Vec<Case>,
25    /// Default case (executed if no case matches).
26    pub default: Option<Vec<String>>,
27}
28
29/// A single case in a switch statement.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Case {
32    /// Value to match.
33    pub value: serde_json::Value,
34    /// Tasks to execute if value matches.
35    pub tasks: Vec<String>,
36}
37
38/// Loop conditional structure.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct LoopCondition {
41    /// Loop condition.
42    pub condition: Expression,
43    /// Tasks to execute in loop body.
44    pub body_tasks: Vec<String>,
45    /// Maximum iterations (safety limit).
46    pub max_iterations: usize,
47}
48
49impl ConditionalBranch {
50    /// Create a new conditional branch.
51    pub fn new(condition: Expression, then_tasks: Vec<String>) -> Self {
52        Self {
53            condition,
54            then_tasks,
55            else_tasks: None,
56        }
57    }
58
59    /// Set the else branch.
60    pub fn with_else(mut self, else_tasks: Vec<String>) -> Self {
61        self.else_tasks = Some(else_tasks);
62        self
63    }
64
65    /// Evaluate the branch and return the tasks to execute.
66    pub fn evaluate(&self, context: &ExpressionContext) -> Result<Vec<String>> {
67        let condition_result = self.condition.evaluate(context)?;
68
69        let is_true = condition_result
70            .as_bool()
71            .ok_or_else(|| WorkflowError::conditional("Condition must evaluate to boolean"))?;
72
73        if is_true {
74            Ok(self.then_tasks.clone())
75        } else if let Some(ref else_tasks) = self.else_tasks {
76            Ok(else_tasks.clone())
77        } else {
78            Ok(Vec::new())
79        }
80    }
81}
82
83impl SwitchCase {
84    /// Create a new switch-case structure.
85    pub fn new(variable: String, cases: Vec<Case>) -> Self {
86        Self {
87            variable,
88            cases,
89            default: None,
90        }
91    }
92
93    /// Set the default case.
94    pub fn with_default(mut self, default_tasks: Vec<String>) -> Self {
95        self.default = Some(default_tasks);
96        self
97    }
98
99    /// Evaluate the switch and return the tasks to execute.
100    pub fn evaluate(&self, context: &ExpressionContext) -> Result<Vec<String>> {
101        let value = context.get(&self.variable).ok_or_else(|| {
102            WorkflowError::conditional(format!("Variable '{}' not found", self.variable))
103        })?;
104
105        for case in &self.cases {
106            if &case.value == value {
107                return Ok(case.tasks.clone());
108            }
109        }
110
111        // No case matched, use default
112        Ok(self.default.clone().unwrap_or_default())
113    }
114}
115
116impl LoopCondition {
117    /// Create a new loop condition.
118    pub fn new(condition: Expression, body_tasks: Vec<String>, max_iterations: usize) -> Self {
119        Self {
120            condition,
121            body_tasks,
122            max_iterations,
123        }
124    }
125
126    /// Evaluate the loop condition.
127    pub fn should_continue(&self, context: &ExpressionContext) -> Result<bool> {
128        let condition_result = self.condition.evaluate(context)?;
129
130        condition_result
131            .as_bool()
132            .ok_or_else(|| WorkflowError::conditional("Loop condition must evaluate to boolean"))
133    }
134
135    /// Get the tasks to execute in this iteration.
136    pub fn get_body_tasks(&self) -> Vec<String> {
137        self.body_tasks.clone()
138    }
139}
140
141/// Conditional execution decision.
142#[derive(Debug, Clone)]
143pub enum ExecutionDecision {
144    /// Execute these tasks.
145    Execute(Vec<String>),
146    /// Skip execution.
147    Skip,
148    /// Repeat execution with these tasks.
149    Repeat(Vec<String>),
150}
151
152/// Conditional execution evaluator.
153pub struct ConditionalEvaluator {
154    /// Registered branches.
155    branches: Vec<ConditionalBranch>,
156    /// Registered switches.
157    switches: Vec<SwitchCase>,
158    /// Registered loops.
159    loops: Vec<LoopCondition>,
160}
161
162impl ConditionalEvaluator {
163    /// Create a new conditional evaluator.
164    pub fn new() -> Self {
165        Self {
166            branches: Vec::new(),
167            switches: Vec::new(),
168            loops: Vec::new(),
169        }
170    }
171
172    /// Add a conditional branch.
173    pub fn add_branch(&mut self, branch: ConditionalBranch) {
174        self.branches.push(branch);
175    }
176
177    /// Add a switch-case.
178    pub fn add_switch(&mut self, switch: SwitchCase) {
179        self.switches.push(switch);
180    }
181
182    /// Add a loop.
183    pub fn add_loop(&mut self, loop_cond: LoopCondition) {
184        self.loops.push(loop_cond);
185    }
186
187    /// Evaluate all conditionals and determine which tasks to execute.
188    pub fn evaluate(&self, context: &ExpressionContext) -> Result<Vec<String>> {
189        let mut tasks_to_execute = Vec::new();
190
191        // Evaluate branches
192        for branch in &self.branches {
193            tasks_to_execute.extend(branch.evaluate(context)?);
194        }
195
196        // Evaluate switches
197        for switch in &self.switches {
198            tasks_to_execute.extend(switch.evaluate(context)?);
199        }
200
201        Ok(tasks_to_execute)
202    }
203}
204
205impl Default for ConditionalEvaluator {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::conditional::expressions::Expression;
215    use serde_json::Value;
216    use std::collections::HashMap;
217
218    #[test]
219    fn test_conditional_branch() {
220        let branch = ConditionalBranch::new(
221            Expression::eq(
222                Expression::variable("status"),
223                Expression::literal(Value::String("success".to_string())),
224            ),
225            vec!["task1".to_string(), "task2".to_string()],
226        )
227        .with_else(vec!["task3".to_string()]);
228
229        let mut ctx = HashMap::new();
230        ctx.insert("status".to_string(), Value::String("success".to_string()));
231
232        let tasks = branch.evaluate(&ctx).expect("Failed to evaluate");
233        assert_eq!(tasks, vec!["task1".to_string(), "task2".to_string()]);
234    }
235
236    #[test]
237    fn test_switch_case() {
238        let switch = SwitchCase::new(
239            "env".to_string(),
240            vec![
241                Case {
242                    value: Value::String("dev".to_string()),
243                    tasks: vec!["dev_task".to_string()],
244                },
245                Case {
246                    value: Value::String("prod".to_string()),
247                    tasks: vec!["prod_task".to_string()],
248                },
249            ],
250        )
251        .with_default(vec!["default_task".to_string()]);
252
253        let mut ctx = HashMap::new();
254        ctx.insert("env".to_string(), Value::String("prod".to_string()));
255
256        let tasks = switch.evaluate(&ctx).expect("Failed to evaluate");
257        assert_eq!(tasks, vec!["prod_task".to_string()]);
258    }
259
260    #[test]
261    fn test_loop_condition() {
262        let loop_cond = LoopCondition::new(
263            Expression::binary(
264                Expression::variable("count"),
265                crate::conditional::expressions::BinaryOperator::Lt,
266                Expression::literal(Value::Number(5.into())),
267            ),
268            vec!["increment".to_string()],
269            10,
270        );
271
272        let mut ctx = HashMap::new();
273        ctx.insert("count".to_string(), Value::Number(3.into()));
274
275        let should_continue = loop_cond.should_continue(&ctx).expect("Failed to evaluate");
276        assert!(should_continue);
277    }
278}