scud/attractor/
conditions.rs1use std::collections::HashMap;
15
16use super::outcome::Outcome;
17
18#[derive(Debug, Clone, PartialEq)]
20pub enum Condition {
21 Always,
23 Eq(String, String),
25 Neq(String, String),
27 And(Vec<Condition>),
29}
30
31pub fn parse_condition(input: &str) -> Condition {
33 let input = input.trim();
34 if input.is_empty() {
35 return Condition::Always;
36 }
37
38 let parts: Vec<&str> = input.split("&&").collect();
40 if parts.len() > 1 {
41 let conditions: Vec<Condition> = parts
42 .into_iter()
43 .map(|p| parse_single_condition(p.trim()))
44 .collect();
45 return Condition::And(conditions);
46 }
47
48 parse_single_condition(input)
49}
50
51fn parse_single_condition(input: &str) -> Condition {
52 let input = input.trim();
53 if input.is_empty() {
54 return Condition::Always;
55 }
56
57 if let Some(pos) = input.find("!=") {
59 let key = input[..pos].trim().to_string();
60 let value = input[pos + 2..].trim().to_string();
61 return Condition::Neq(key, value);
62 }
63
64 if let Some(pos) = input.find('=') {
66 let key = input[..pos].trim().to_string();
67 let value = input[pos + 1..].trim().to_string();
68 return Condition::Eq(key, value);
69 }
70
71 Condition::Eq(input.to_string(), "true".to_string())
73}
74
75pub fn evaluate_condition(
77 condition: &Condition,
78 outcome: &Outcome,
79 context: &HashMap<String, serde_json::Value>,
80) -> bool {
81 match condition {
82 Condition::Always => true,
83 Condition::Eq(key, value) => resolve_value(key, outcome, context) == *value,
84 Condition::Neq(key, value) => resolve_value(key, outcome, context) != *value,
85 Condition::And(conditions) => conditions
86 .iter()
87 .all(|c| evaluate_condition(c, outcome, context)),
88 }
89}
90
91fn resolve_value(
93 key: &str,
94 outcome: &Outcome,
95 context: &HashMap<String, serde_json::Value>,
96) -> String {
97 match key {
98 "outcome" => outcome.status.as_str().to_string(),
99 "preferred_label" => outcome.preferred_label.clone().unwrap_or_default(),
100 _ => {
101 let ctx_key = if key.starts_with("context.") {
103 &key[8..]
104 } else {
105 key
106 };
107 context
108 .get(ctx_key)
109 .map(|v| match v {
110 serde_json::Value::String(s) => s.clone(),
111 serde_json::Value::Bool(b) => b.to_string(),
112 serde_json::Value::Number(n) => n.to_string(),
113 other => other.to_string(),
114 })
115 .unwrap_or_default()
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::attractor::outcome::Outcome;
124
125 #[test]
126 fn test_parse_empty() {
127 assert_eq!(parse_condition(""), Condition::Always);
128 }
129
130 #[test]
131 fn test_parse_eq() {
132 assert_eq!(
133 parse_condition("outcome=success"),
134 Condition::Eq("outcome".into(), "success".into())
135 );
136 }
137
138 #[test]
139 fn test_parse_neq() {
140 assert_eq!(
141 parse_condition("outcome!=failure"),
142 Condition::Neq("outcome".into(), "failure".into())
143 );
144 }
145
146 #[test]
147 fn test_parse_and() {
148 let cond = parse_condition("outcome=success && context.approved=true");
149 match cond {
150 Condition::And(parts) => {
151 assert_eq!(parts.len(), 2);
152 assert_eq!(parts[0], Condition::Eq("outcome".into(), "success".into()));
153 assert_eq!(
154 parts[1],
155 Condition::Eq("context.approved".into(), "true".into())
156 );
157 }
158 _ => panic!("Expected And condition"),
159 }
160 }
161
162 #[test]
163 fn test_evaluate_outcome_eq() {
164 let outcome = Outcome::success();
165 let ctx = HashMap::new();
166 let cond = parse_condition("outcome=success");
167 assert!(evaluate_condition(&cond, &outcome, &ctx));
168 }
169
170 #[test]
171 fn test_evaluate_outcome_neq() {
172 let outcome = Outcome::success();
173 let ctx = HashMap::new();
174 let cond = parse_condition("outcome!=failure");
175 assert!(evaluate_condition(&cond, &outcome, &ctx));
176 }
177
178 #[test]
179 fn test_evaluate_preferred_label() {
180 let outcome = Outcome::success_with_label("approve");
181 let ctx = HashMap::new();
182 let cond = parse_condition("preferred_label=approve");
183 assert!(evaluate_condition(&cond, &outcome, &ctx));
184 }
185
186 #[test]
187 fn test_evaluate_context_value() {
188 let outcome = Outcome::success();
189 let mut ctx = HashMap::new();
190 ctx.insert("test_passed".into(), serde_json::json!("true"));
191 let cond = parse_condition("test_passed=true");
192 assert!(evaluate_condition(&cond, &outcome, &ctx));
193 }
194
195 #[test]
196 fn test_evaluate_context_prefix() {
197 let outcome = Outcome::success();
198 let mut ctx = HashMap::new();
199 ctx.insert("flag".into(), serde_json::json!("yes"));
200 let cond = parse_condition("context.flag=yes");
201 assert!(evaluate_condition(&cond, &outcome, &ctx));
202 }
203
204 #[test]
205 fn test_evaluate_and_all_true() {
206 let outcome = Outcome::success();
207 let mut ctx = HashMap::new();
208 ctx.insert("ready".into(), serde_json::json!("true"));
209 let cond = parse_condition("outcome=success && ready=true");
210 assert!(evaluate_condition(&cond, &outcome, &ctx));
211 }
212
213 #[test]
214 fn test_evaluate_and_one_false() {
215 let outcome = Outcome::success();
216 let ctx = HashMap::new();
217 let cond = parse_condition("outcome=success && missing=true");
218 assert!(!evaluate_condition(&cond, &outcome, &ctx));
219 }
220}