areev_trigger/
predicate.rs1use areev_cal::ast::Condition;
26use areev_cal::executor::{grain_matches_condition_tree, CalGrainResult};
27use areev_core::format::deserialize::DeserializedGrain;
28
29use crate::error::{Result, TriggerError};
30
31pub fn parse_predicate(where_src: &str) -> Result<Condition> {
38 let probe = format!("RECALL grains WHERE {where_src}");
39 let query = areev_cal::parse(&probe).map_err(|e| TriggerError::Malformed {
40 what: format!("predicate {where_src:?} does not parse: {e}"),
41 })?;
42 match query.statement {
43 areev_cal::ast::CalStatement::Recall(r) => r
44 .where_clause
45 .map(|w| w.condition)
46 .ok_or_else(|| TriggerError::Malformed { what: "predicate is empty".into() }),
47 _ => Err(TriggerError::Malformed {
48 what: format!("predicate {where_src:?} is not a filter expression"),
49 }),
50 }
51}
52
53pub fn to_value(c: &Condition) -> Result<serde_json::Value> {
55 serde_json::to_value(c)
56 .map_err(|e| TriggerError::Malformed { what: format!("predicate is not serializable: {e}") })
57}
58
59pub fn from_value(v: &serde_json::Value) -> Result<Condition> {
61 serde_json::from_value(v.clone())
62 .map_err(|e| TriggerError::Malformed { what: format!("stored predicate is unreadable: {e}") })
63}
64
65pub fn grain_matches(grain: &DeserializedGrain, condition: &Condition) -> bool {
71 let projected = CalGrainResult {
72 hash: grain.hash.to_hex(),
73 grain_type: grain.grain_type.as_str().to_string(),
74 score: 0.0,
75 fields: serde_json::to_value(&grain.fields).unwrap_or(serde_json::Value::Null),
76 score_breakdown: None,
77 explanation: None,
78 relative_time: None,
79 is_deterministic: false,
80 contested_by: None,
81 };
82 grain_matches_condition_tree(&projected, condition)
83}
84
85pub fn gate_satisfied(condition: &Condition, fired: &[String]) -> bool {
100 let fields: serde_json::Map<String, serde_json::Value> = referenced_members(condition)
101 .into_iter()
102 .map(|m| {
103 let has_fired = fired.contains(&m);
104 (m, serde_json::Value::Bool(has_fired))
105 })
106 .collect();
107 let projected = CalGrainResult {
108 hash: String::new(),
109 grain_type: "gate".into(),
110 score: 0.0,
111 fields: serde_json::Value::Object(fields),
112 score_breakdown: None,
113 explanation: None,
114 relative_time: None,
115 is_deterministic: false,
116 contested_by: None,
117 };
118 grain_matches_condition_tree(&projected, condition)
119}
120
121pub fn referenced_members(condition: &Condition) -> Vec<String> {
127 fn walk(c: &Condition, out: &mut Vec<String>) {
128 match c {
129 Condition::Comparison { field, .. }
130 | Condition::In { field, .. }
131 | Condition::NotIn { field, .. }
132 | Condition::IsNull { field, .. }
133 | Condition::IsNotNull { field, .. }
134 | Condition::Contains { field, .. }
135 | Condition::StartsWith { field, .. }
136 | Condition::IsCategory { field, .. } => out.push(field.clone()),
137 Condition::And { left, right, .. } | Condition::Or { left, right, .. } => {
138 walk(left, out);
139 walk(right, out);
140 }
141 Condition::Not { inner, .. } => walk(inner, out),
142 }
143 }
144 let mut out = Vec::new();
145 walk(condition, &mut out);
146 out.sort();
147 out.dedup();
148 out
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 fn gate(src: &str) -> Condition {
156 parse_predicate(src).unwrap()
157 }
158
159 #[test]
160 fn boolean_composition_round_trips_through_storage() {
161 let c = gate("(a = true AND b = true) OR c = true");
165 let back = from_value(&to_value(&c).unwrap()).unwrap();
166
167 for fired in [
168 vec![],
169 vec!["a".to_string()],
170 vec!["c".to_string()],
171 vec!["a".to_string(), "b".to_string()],
172 vec!["a".to_string(), "b".to_string(), "c".to_string()],
173 ] {
174 assert_eq!(
175 gate_satisfied(&c, &fired),
176 gate_satisfied(&back, &fired),
177 "storage changed the meaning for {fired:?}"
178 );
179 }
180 assert_eq!(referenced_members(&c), referenced_members(&back));
181 }
182
183 #[test]
184 fn and_requires_both_members() {
185 let c = gate("a = true AND b = true");
186 assert!(!gate_satisfied(&c, &["a".into()]));
187 assert!(!gate_satisfied(&c, &["b".into()]));
188 assert!(gate_satisfied(&c, &["a".into(), "b".into()]));
189 }
190
191 #[test]
192 fn or_requires_either() {
193 let c = gate("a = true OR b = true");
194 assert!(gate_satisfied(&c, &["a".into()]));
195 assert!(gate_satisfied(&c, &["b".into()]));
196 assert!(!gate_satisfied(&c, &[]));
197 }
198
199 #[test]
200 fn parenthesised_precedence_matches_the_written_expression() {
201 let c = gate("(a = true AND b = true) OR c = true");
203 assert!(gate_satisfied(&c, &["c".into()]), "c alone satisfies the right branch");
204 assert!(gate_satisfied(&c, &["a".into(), "b".into()]));
205 assert!(!gate_satisfied(&c, &["a".into()]), "a alone satisfies neither branch");
206 }
207
208 #[test]
209 fn precedence_is_or_below_and_not_left_to_right() {
210 let c = gate("a = true AND b = true OR c = true");
214 assert!(gate_satisfied(&c, &["c".into()]), "c alone must satisfy it");
215 }
216
217 #[test]
218 fn negation_works() {
219 let c = gate("NOT (a = true)");
220 assert!(gate_satisfied(&c, &[]), "a absent means the negation holds");
221 assert!(!gate_satisfied(&c, &["a".into()]));
222 }
223
224 #[test]
225 fn referenced_members_finds_every_name() {
226 let c = gate("(a = true AND b = true) OR NOT (c = true)");
227 assert_eq!(referenced_members(&c), vec!["a", "b", "c"]);
228 }
229
230 #[test]
231 fn a_predicate_that_does_not_parse_is_refused_with_its_code() {
232 let e = parse_predicate("this is not a predicate").unwrap_err();
233 assert_eq!(e.code(), "TRG-E001");
234 }
235
236 #[test]
237 fn an_empty_predicate_is_refused() {
238 assert!(parse_predicate("").is_err());
240 }
241}