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    NotEquals,
36    Gt,
37    Gte,
38    Lt,
39    Lte,
40    Contains,
41    IsNull,
42    IsNotNull,
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use pretty_assertions::assert_eq;
49    use serde_json::json;
50
51    #[test]
52    fn comparison_op_serde_round_trip() {
53        for op in [
54            ComparisonOp::Equals,
55            ComparisonOp::NotEquals,
56            ComparisonOp::Gt,
57            ComparisonOp::Gte,
58            ComparisonOp::Lt,
59            ComparisonOp::Lte,
60            ComparisonOp::Contains,
61            ComparisonOp::IsNull,
62            ComparisonOp::IsNotNull,
63        ] {
64            let json_str = serde_json::to_string(&op).unwrap();
65            let deserialized: ComparisonOp = serde_json::from_str(&json_str).unwrap();
66            assert_eq!(op, deserialized);
67        }
68    }
69
70    #[test]
71    fn field_comparison_serde_round_trip() {
72        let comp = FieldComparison {
73            field: ".outputs.status".to_string(),
74            operator: ComparisonOp::Equals,
75            value: Some(json!("success")),
76        };
77        let json_str = serde_json::to_string(&comp).unwrap();
78        let deserialized: FieldComparison = serde_json::from_str(&json_str).unwrap();
79        assert_eq!(comp, deserialized);
80    }
81
82    #[test]
83    fn field_comparison_without_value_serde_round_trip() {
84        let comp = FieldComparison {
85            field: ".outputs.result".to_string(),
86            operator: ComparisonOp::IsNull,
87            value: None,
88        };
89        let json_str = serde_json::to_string(&comp).unwrap();
90        let deserialized: FieldComparison = serde_json::from_str(&json_str).unwrap();
91        assert_eq!(comp, deserialized);
92    }
93
94    #[test]
95    fn step_condition_comparison_serde_round_trip() {
96        let condition = StepCondition::Comparison(FieldComparison {
97            field: ".count".to_string(),
98            operator: ComparisonOp::Gt,
99            value: Some(json!(5)),
100        });
101        let json_str = serde_json::to_string(&condition).unwrap();
102        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
103        assert_eq!(condition, deserialized);
104    }
105
106    #[test]
107    fn step_condition_not_serde_round_trip() {
108        let condition = StepCondition::Not(Box::new(StepCondition::Comparison(FieldComparison {
109            field: ".active".to_string(),
110            operator: ComparisonOp::Equals,
111            value: Some(json!(false)),
112        })));
113        let json_str = serde_json::to_string(&condition).unwrap();
114        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
115        assert_eq!(condition, deserialized);
116    }
117
118    #[test]
119    fn step_condition_all_serde_round_trip() {
120        let condition = StepCondition::All(vec![
121            StepCondition::Comparison(FieldComparison {
122                field: ".a".to_string(),
123                operator: ComparisonOp::Equals,
124                value: Some(json!(1)),
125            }),
126            StepCondition::Comparison(FieldComparison {
127                field: ".b".to_string(),
128                operator: ComparisonOp::Equals,
129                value: Some(json!(2)),
130            }),
131        ]);
132        let json_str = serde_json::to_string(&condition).unwrap();
133        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
134        assert_eq!(condition, deserialized);
135    }
136
137    #[test]
138    fn step_condition_any_serde_round_trip() {
139        let condition = StepCondition::Any(vec![StepCondition::Comparison(FieldComparison {
140            field: ".x".to_string(),
141            operator: ComparisonOp::IsNull,
142            value: None,
143        })]);
144        let json_str = serde_json::to_string(&condition).unwrap();
145        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
146        assert_eq!(condition, deserialized);
147    }
148
149    #[test]
150    fn step_condition_none_serde_round_trip() {
151        let condition = StepCondition::None(vec![StepCondition::Comparison(FieldComparison {
152            field: ".err".to_string(),
153            operator: ComparisonOp::IsNotNull,
154            value: None,
155        })]);
156        let json_str = serde_json::to_string(&condition).unwrap();
157        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
158        assert_eq!(condition, deserialized);
159    }
160
161    #[test]
162    fn step_condition_one_of_serde_round_trip() {
163        let condition = StepCondition::OneOf(vec![
164            StepCondition::Comparison(FieldComparison {
165                field: ".mode".to_string(),
166                operator: ComparisonOp::Equals,
167                value: Some(json!("fast")),
168            }),
169            StepCondition::Comparison(FieldComparison {
170                field: ".mode".to_string(),
171                operator: ComparisonOp::Equals,
172                value: Some(json!("slow")),
173            }),
174        ]);
175        let json_str = serde_json::to_string(&condition).unwrap();
176        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
177        assert_eq!(condition, deserialized);
178    }
179
180    #[test]
181    fn nested_combinator_serde_round_trip() {
182        let condition = StepCondition::All(vec![
183            StepCondition::Any(vec![
184                StepCondition::Comparison(FieldComparison {
185                    field: ".a".to_string(),
186                    operator: ComparisonOp::Equals,
187                    value: Some(json!(1)),
188                }),
189                StepCondition::Comparison(FieldComparison {
190                    field: ".b".to_string(),
191                    operator: ComparisonOp::Equals,
192                    value: Some(json!(2)),
193                }),
194            ]),
195            StepCondition::Not(Box::new(StepCondition::Comparison(FieldComparison {
196                field: ".c".to_string(),
197                operator: ComparisonOp::IsNull,
198                value: None,
199            }))),
200        ]);
201        let json_str = serde_json::to_string(&condition).unwrap();
202        let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
203        assert_eq!(condition, deserialized);
204    }
205}