Skip to main content

wfe_core/models/
condition.rs

1use serde::{Deserialize, Serialize};
2
3/// A condition that determines whether a workflow step should execute.
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5pub enum StepCondition {
6    /// All sub-conditions must be true (AND).
7    All(Vec<StepCondition>),
8    /// At least one sub-condition must be true (OR).
9    Any(Vec<StepCondition>),
10    /// No sub-conditions may be true (NOR).
11    None(Vec<StepCondition>),
12    /// Exactly one sub-condition must be true (XOR).
13    OneOf(Vec<StepCondition>),
14    /// Negation of a single condition (NOT).
15    Not(Box<StepCondition>),
16    /// A leaf comparison against a field in workflow data.
17    Comparison(FieldComparison),
18}
19
20/// A comparison of a workflow data field against an expected value.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct FieldComparison {
23    /// Dot-separated field path, e.g. ".outputs.docker_started".
24    pub field: String,
25    /// The comparison operator.
26    pub operator: ComparisonOp,
27    /// The value to compare against. Required for all operators except IsNull/IsNotNull.
28    pub value: Option<serde_json::Value>,
29}
30
31/// Comparison operators for field conditions.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub enum ComparisonOp {
34    /// Equals.
35    Equals,
36    /// Notequals.
37    NotEquals,
38    /// Gt.
39    Gt,
40    /// Gte.
41    Gte,
42    /// Lt.
43    Lt,
44    /// Lte.
45    Lte,
46    /// Contains.
47    Contains,
48    /// Isnull.
49    IsNull,
50    /// Isnotnull.
51    IsNotNull,
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use pretty_assertions::assert_eq;
58    use serde_json::json;
59
60    #[test]
61    fn comparison_op_serde_round_trip() {
62        for op in [
63            ComparisonOp::Equals,
64            ComparisonOp::NotEquals,
65            ComparisonOp::Gt,
66            ComparisonOp::Gte,
67            ComparisonOp::Lt,
68            ComparisonOp::Lte,
69            ComparisonOp::Contains,
70            ComparisonOp::IsNull,
71            ComparisonOp::IsNotNull,
72        ] {
73            let json_str = serde_json::to_string(&op).unwrap();
74            let deserialized: ComparisonOp = serde_json::from_str(&json_str).unwrap();
75            assert_eq!(op, deserialized);
76        }
77    }
78
79    #[test]
80    fn field_comparison_serde_round_trip() {
81        let comp = FieldComparison {
82            field: ".outputs.status".to_string(),
83            operator: ComparisonOp::Equals,
84            value: Some(json!("success")),
85        };
86        let json_str = serde_json::to_string(&comp).unwrap();
87        let deserialized: FieldComparison = serde_json::from_str(&json_str).unwrap();
88        assert_eq!(comp, deserialized);
89    }
90
91    #[test]
92    fn field_comparison_without_value_serde_round_trip() {
93        let comp = FieldComparison {
94            field: ".outputs.result".to_string(),
95            operator: ComparisonOp::IsNull,
96            value: None,
97        };
98        let json_str = serde_json::to_string(&comp).unwrap();
99        let deserialized: FieldComparison = serde_json::from_str(&json_str).unwrap();
100        assert_eq!(comp, deserialized);
101    }
102
103    #[test]
104    fn step_condition_comparison_serde_round_trip() {
105        let condition = StepCondition::Comparison(FieldComparison {
106            field: ".count".to_string(),
107            operator: ComparisonOp::Gt,
108            value: Some(json!(5)),
109        });
110        let json_str = serde_json::to_string(&condition).unwrap();
111        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
112        assert_eq!(condition, deserialized);
113    }
114
115    #[test]
116    fn step_condition_not_serde_round_trip() {
117        let condition = StepCondition::Not(Box::new(StepCondition::Comparison(FieldComparison {
118            field: ".active".to_string(),
119            operator: ComparisonOp::Equals,
120            value: Some(json!(false)),
121        })));
122        let json_str = serde_json::to_string(&condition).unwrap();
123        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
124        assert_eq!(condition, deserialized);
125    }
126
127    #[test]
128    fn step_condition_all_serde_round_trip() {
129        let condition = StepCondition::All(vec![
130            StepCondition::Comparison(FieldComparison {
131                field: ".a".to_string(),
132                operator: ComparisonOp::Equals,
133                value: Some(json!(1)),
134            }),
135            StepCondition::Comparison(FieldComparison {
136                field: ".b".to_string(),
137                operator: ComparisonOp::Equals,
138                value: Some(json!(2)),
139            }),
140        ]);
141        let json_str = serde_json::to_string(&condition).unwrap();
142        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
143        assert_eq!(condition, deserialized);
144    }
145
146    #[test]
147    fn step_condition_any_serde_round_trip() {
148        let condition = StepCondition::Any(vec![StepCondition::Comparison(FieldComparison {
149            field: ".x".to_string(),
150            operator: ComparisonOp::IsNull,
151            value: None,
152        })]);
153        let json_str = serde_json::to_string(&condition).unwrap();
154        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
155        assert_eq!(condition, deserialized);
156    }
157
158    #[test]
159    fn step_condition_none_serde_round_trip() {
160        let condition = StepCondition::None(vec![StepCondition::Comparison(FieldComparison {
161            field: ".err".to_string(),
162            operator: ComparisonOp::IsNotNull,
163            value: None,
164        })]);
165        let json_str = serde_json::to_string(&condition).unwrap();
166        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
167        assert_eq!(condition, deserialized);
168    }
169
170    #[test]
171    fn step_condition_one_of_serde_round_trip() {
172        let condition = StepCondition::OneOf(vec![
173            StepCondition::Comparison(FieldComparison {
174                field: ".mode".to_string(),
175                operator: ComparisonOp::Equals,
176                value: Some(json!("fast")),
177            }),
178            StepCondition::Comparison(FieldComparison {
179                field: ".mode".to_string(),
180                operator: ComparisonOp::Equals,
181                value: Some(json!("slow")),
182            }),
183        ]);
184        let json_str = serde_json::to_string(&condition).unwrap();
185        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
186        assert_eq!(condition, deserialized);
187    }
188
189    #[test]
190    fn nested_combinator_serde_round_trip() {
191        let condition = StepCondition::All(vec![
192            StepCondition::Any(vec![
193                StepCondition::Comparison(FieldComparison {
194                    field: ".a".to_string(),
195                    operator: ComparisonOp::Equals,
196                    value: Some(json!(1)),
197                }),
198                StepCondition::Comparison(FieldComparison {
199                    field: ".b".to_string(),
200                    operator: ComparisonOp::Equals,
201                    value: Some(json!(2)),
202                }),
203            ]),
204            StepCondition::Not(Box::new(StepCondition::Comparison(FieldComparison {
205                field: ".c".to_string(),
206                operator: ComparisonOp::IsNull,
207                value: None,
208            }))),
209        ]);
210        let json_str = serde_json::to_string(&condition).unwrap();
211        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
212        assert_eq!(condition, deserialized);
213    }
214}