miyabi_workflow/
condition.rs

1//! Conditional branching for workflows
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// Conditions for workflow branching
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub enum Condition {
9    /// Always evaluates to true (default/fallback branch)
10    Always,
11
12    /// Check if a field equals a specific value
13    FieldEquals { field: String, value: Value },
14
15    /// Check if a numeric field is greater than a value
16    FieldGreaterThan { field: String, value: f64 },
17
18    /// Check if a numeric field is less than a value
19    FieldLessThan { field: String, value: f64 },
20
21    /// Check if a field exists
22    FieldExists { field: String },
23
24    /// Logical AND of multiple conditions
25    And(Vec<Condition>),
26
27    /// Logical OR of multiple conditions
28    Or(Vec<Condition>),
29
30    /// Logical NOT
31    Not(Box<Condition>),
32}
33
34impl Condition {
35    /// Evaluate condition against a JSON context
36    pub fn evaluate(&self, context: &Value) -> bool {
37        match self {
38            Condition::Always => true,
39
40            Condition::FieldEquals { field, value } => {
41                Self::get_field(context, field).map(|v| v == value).unwrap_or(false)
42            },
43
44            Condition::FieldGreaterThan { field, value } => Self::get_field(context, field)
45                .and_then(|v| v.as_f64())
46                .map(|v| v > *value)
47                .unwrap_or(false),
48
49            Condition::FieldLessThan { field, value } => Self::get_field(context, field)
50                .and_then(|v| v.as_f64())
51                .map(|v| v < *value)
52                .unwrap_or(false),
53
54            Condition::FieldExists { field } => Self::get_field(context, field).is_some(),
55
56            Condition::And(conditions) => conditions.iter().all(|c| c.evaluate(context)),
57
58            Condition::Or(conditions) => conditions.iter().any(|c| c.evaluate(context)),
59
60            Condition::Not(condition) => !condition.evaluate(context),
61        }
62    }
63
64    /// Get field from JSON context (supports nested fields with dot notation)
65    fn get_field<'a>(context: &'a Value, field: &str) -> Option<&'a Value> {
66        if field.contains('.') {
67            // Handle nested fields: "result.status"
68            let parts: Vec<&str> = field.split('.').collect();
69            let mut current = context;
70
71            for part in parts {
72                current = current.get(part)?;
73            }
74
75            Some(current)
76        } else {
77            context.get(field)
78        }
79    }
80
81    /// Create a condition that checks if a field equals true
82    pub fn success(field: &str) -> Self {
83        Condition::FieldEquals {
84            field: field.to_string(),
85            value: Value::Bool(true),
86        }
87    }
88
89    /// Create a condition that checks if a field equals false
90    pub fn failure(field: &str) -> Self {
91        Condition::FieldEquals {
92            field: field.to_string(),
93            value: Value::Bool(false),
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use serde_json::json;
102
103    #[test]
104    fn test_always_condition() {
105        let cond = Condition::Always;
106        let context = json!({});
107
108        assert!(cond.evaluate(&context));
109    }
110
111    #[test]
112    fn test_field_equals() {
113        let cond = Condition::FieldEquals {
114            field: "status".to_string(),
115            value: json!("passed"),
116        };
117
118        let context_pass = json!({ "status": "passed" });
119        let context_fail = json!({ "status": "failed" });
120
121        assert!(cond.evaluate(&context_pass));
122        assert!(!cond.evaluate(&context_fail));
123    }
124
125    #[test]
126    fn test_field_greater_than() {
127        let cond = Condition::FieldGreaterThan {
128            field: "score".to_string(),
129            value: 0.8,
130        };
131
132        let context_high = json!({ "score": 0.95 });
133        let context_low = json!({ "score": 0.5 });
134
135        assert!(cond.evaluate(&context_high));
136        assert!(!cond.evaluate(&context_low));
137    }
138
139    #[test]
140    fn test_field_less_than() {
141        let cond = Condition::FieldLessThan {
142            field: "errors".to_string(),
143            value: 5.0,
144        };
145
146        let context_few = json!({ "errors": 2.0 });
147        let context_many = json!({ "errors": 10.0 });
148
149        assert!(cond.evaluate(&context_few));
150        assert!(!cond.evaluate(&context_many));
151    }
152
153    #[test]
154    fn test_field_exists() {
155        let cond = Condition::FieldExists {
156            field: "result".to_string(),
157        };
158
159        let context_exists = json!({ "result": "data" });
160        let context_missing = json!({ "other": "data" });
161
162        assert!(cond.evaluate(&context_exists));
163        assert!(!cond.evaluate(&context_missing));
164    }
165
166    #[test]
167    fn test_nested_field() {
168        let cond = Condition::FieldEquals {
169            field: "result.status".to_string(),
170            value: json!("success"),
171        };
172
173        let context = json!({
174            "result": {
175                "status": "success",
176                "code": 200
177            }
178        });
179
180        assert!(cond.evaluate(&context));
181    }
182
183    #[test]
184    fn test_and_condition() {
185        let cond = Condition::And(vec![
186            Condition::FieldEquals {
187                field: "status".to_string(),
188                value: json!("passed"),
189            },
190            Condition::FieldGreaterThan {
191                field: "score".to_string(),
192                value: 0.8,
193            },
194        ]);
195
196        let context_both = json!({ "status": "passed", "score": 0.9 });
197        let context_one = json!({ "status": "passed", "score": 0.5 });
198
199        assert!(cond.evaluate(&context_both));
200        assert!(!cond.evaluate(&context_one));
201    }
202
203    #[test]
204    fn test_or_condition() {
205        let cond = Condition::Or(vec![
206            Condition::FieldEquals {
207                field: "status".to_string(),
208                value: json!("passed"),
209            },
210            Condition::FieldEquals {
211                field: "status".to_string(),
212                value: json!("warning"),
213            },
214        ]);
215
216        let context_pass = json!({ "status": "passed" });
217        let context_warn = json!({ "status": "warning" });
218        let context_fail = json!({ "status": "failed" });
219
220        assert!(cond.evaluate(&context_pass));
221        assert!(cond.evaluate(&context_warn));
222        assert!(!cond.evaluate(&context_fail));
223    }
224
225    #[test]
226    fn test_not_condition() {
227        let cond = Condition::Not(Box::new(Condition::FieldEquals {
228            field: "failed".to_string(),
229            value: json!(true),
230        }));
231
232        let context_ok = json!({ "failed": false });
233        let context_failed = json!({ "failed": true });
234
235        assert!(cond.evaluate(&context_ok));
236        assert!(!cond.evaluate(&context_failed));
237    }
238
239    #[test]
240    fn test_success_helper() {
241        let cond = Condition::success("passed");
242        let context = json!({ "passed": true });
243
244        assert!(cond.evaluate(&context));
245    }
246
247    #[test]
248    fn test_failure_helper() {
249        let cond = Condition::failure("passed");
250        let context = json!({ "passed": false });
251
252        assert!(cond.evaluate(&context));
253    }
254}