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
100 .preferred_label
101 .clone()
102 .unwrap_or_default(),
103 _ => {
104 let ctx_key = if key.starts_with("context.") {
106 &key[8..]
107 } else {
108 key
109 };
110 context
111 .get(ctx_key)
112 .map(|v| match v {
113 serde_json::Value::String(s) => s.clone(),
114 serde_json::Value::Bool(b) => b.to_string(),
115 serde_json::Value::Number(n) => n.to_string(),
116 other => other.to_string(),
117 })
118 .unwrap_or_default()
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::attractor::outcome::Outcome;
127
128 #[test]
129 fn test_parse_empty() {
130 assert_eq!(parse_condition(""), Condition::Always);
131 }
132
133 #[test]
134 fn test_parse_eq() {
135 assert_eq!(
136 parse_condition("outcome=success"),
137 Condition::Eq("outcome".into(), "success".into())
138 );
139 }
140
141 #[test]
142 fn test_parse_neq() {
143 assert_eq!(
144 parse_condition("outcome!=failure"),
145 Condition::Neq("outcome".into(), "failure".into())
146 );
147 }
148
149 #[test]
150 fn test_parse_and() {
151 let cond = parse_condition("outcome=success && context.approved=true");
152 match cond {
153 Condition::And(parts) => {
154 assert_eq!(parts.len(), 2);
155 assert_eq!(
156 parts[0],
157 Condition::Eq("outcome".into(), "success".into())
158 );
159 assert_eq!(
160 parts[1],
161 Condition::Eq("context.approved".into(), "true".into())
162 );
163 }
164 _ => panic!("Expected And condition"),
165 }
166 }
167
168 #[test]
169 fn test_evaluate_outcome_eq() {
170 let outcome = Outcome::success();
171 let ctx = HashMap::new();
172 let cond = parse_condition("outcome=success");
173 assert!(evaluate_condition(&cond, &outcome, &ctx));
174 }
175
176 #[test]
177 fn test_evaluate_outcome_neq() {
178 let outcome = Outcome::success();
179 let ctx = HashMap::new();
180 let cond = parse_condition("outcome!=failure");
181 assert!(evaluate_condition(&cond, &outcome, &ctx));
182 }
183
184 #[test]
185 fn test_evaluate_preferred_label() {
186 let outcome = Outcome::success_with_label("approve");
187 let ctx = HashMap::new();
188 let cond = parse_condition("preferred_label=approve");
189 assert!(evaluate_condition(&cond, &outcome, &ctx));
190 }
191
192 #[test]
193 fn test_evaluate_context_value() {
194 let outcome = Outcome::success();
195 let mut ctx = HashMap::new();
196 ctx.insert("test_passed".into(), serde_json::json!("true"));
197 let cond = parse_condition("test_passed=true");
198 assert!(evaluate_condition(&cond, &outcome, &ctx));
199 }
200
201 #[test]
202 fn test_evaluate_context_prefix() {
203 let outcome = Outcome::success();
204 let mut ctx = HashMap::new();
205 ctx.insert("flag".into(), serde_json::json!("yes"));
206 let cond = parse_condition("context.flag=yes");
207 assert!(evaluate_condition(&cond, &outcome, &ctx));
208 }
209
210 #[test]
211 fn test_evaluate_and_all_true() {
212 let outcome = Outcome::success();
213 let mut ctx = HashMap::new();
214 ctx.insert("ready".into(), serde_json::json!("true"));
215 let cond = parse_condition("outcome=success && ready=true");
216 assert!(evaluate_condition(&cond, &outcome, &ctx));
217 }
218
219 #[test]
220 fn test_evaluate_and_one_false() {
221 let outcome = Outcome::success();
222 let ctx = HashMap::new();
223 let cond = parse_condition("outcome=success && missing=true");
224 assert!(!evaluate_condition(&cond, &outcome, &ctx));
225 }
226}