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 {
91 let fields: serde_json::Map<String, serde_json::Value> = fired
92 .iter()
93 .map(|m| (m.clone(), serde_json::Value::Bool(true)))
94 .collect();
95 let projected = CalGrainResult {
96 hash: String::new(),
97 grain_type: "gate".into(),
98 score: 0.0,
99 fields: serde_json::Value::Object(fields),
100 score_breakdown: None,
101 explanation: None,
102 relative_time: None,
103 is_deterministic: false,
104 contested_by: None,
105 };
106 grain_matches_condition_tree(&projected, condition)
107}
108
109pub fn referenced_members(condition: &Condition) -> Vec<String> {
115 fn walk(c: &Condition, out: &mut Vec<String>) {
116 match c {
117 Condition::Comparison { field, .. }
118 | Condition::In { field, .. }
119 | Condition::NotIn { field, .. }
120 | Condition::IsNull { field, .. }
121 | Condition::IsNotNull { field, .. }
122 | Condition::Contains { field, .. }
123 | Condition::StartsWith { field, .. }
124 | Condition::IsCategory { field, .. } => out.push(field.clone()),
125 Condition::And { left, right, .. } | Condition::Or { left, right, .. } => {
126 walk(left, out);
127 walk(right, out);
128 }
129 Condition::Not { inner, .. } => walk(inner, out),
130 }
131 }
132 let mut out = Vec::new();
133 walk(condition, &mut out);
134 out.sort();
135 out.dedup();
136 out
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn gate(src: &str) -> Condition {
144 parse_predicate(src).unwrap()
145 }
146
147 #[test]
148 fn boolean_composition_round_trips_through_storage() {
149 let c = gate("(a = true AND b = true) OR c = true");
153 let back = from_value(&to_value(&c).unwrap()).unwrap();
154
155 for fired in [
156 vec![],
157 vec!["a".to_string()],
158 vec!["c".to_string()],
159 vec!["a".to_string(), "b".to_string()],
160 vec!["a".to_string(), "b".to_string(), "c".to_string()],
161 ] {
162 assert_eq!(
163 gate_satisfied(&c, &fired),
164 gate_satisfied(&back, &fired),
165 "storage changed the meaning for {fired:?}"
166 );
167 }
168 assert_eq!(referenced_members(&c), referenced_members(&back));
169 }
170
171 #[test]
172 fn and_requires_both_members() {
173 let c = gate("a = true AND b = true");
174 assert!(!gate_satisfied(&c, &["a".into()]));
175 assert!(!gate_satisfied(&c, &["b".into()]));
176 assert!(gate_satisfied(&c, &["a".into(), "b".into()]));
177 }
178
179 #[test]
180 fn or_requires_either() {
181 let c = gate("a = true OR b = true");
182 assert!(gate_satisfied(&c, &["a".into()]));
183 assert!(gate_satisfied(&c, &["b".into()]));
184 assert!(!gate_satisfied(&c, &[]));
185 }
186
187 #[test]
188 fn parenthesised_precedence_matches_the_written_expression() {
189 let c = gate("(a = true AND b = true) OR c = true");
191 assert!(gate_satisfied(&c, &["c".into()]), "c alone satisfies the right branch");
192 assert!(gate_satisfied(&c, &["a".into(), "b".into()]));
193 assert!(!gate_satisfied(&c, &["a".into()]), "a alone satisfies neither branch");
194 }
195
196 #[test]
197 fn precedence_is_or_below_and_not_left_to_right() {
198 let c = gate("a = true AND b = true OR c = true");
202 assert!(gate_satisfied(&c, &["c".into()]), "c alone must satisfy it");
203 }
204
205 #[test]
206 fn negation_works() {
207 let c = gate("NOT (a = true)");
208 assert!(gate_satisfied(&c, &[]), "a absent means the negation holds");
209 assert!(!gate_satisfied(&c, &["a".into()]));
210 }
211
212 #[test]
213 fn referenced_members_finds_every_name() {
214 let c = gate("(a = true AND b = true) OR NOT (c = true)");
215 assert_eq!(referenced_members(&c), vec!["a", "b", "c"]);
216 }
217
218 #[test]
219 fn a_predicate_that_does_not_parse_is_refused_with_its_code() {
220 let e = parse_predicate("this is not a predicate").unwrap_err();
221 assert_eq!(e.code(), "TRG-E001");
222 }
223
224 #[test]
225 fn an_empty_predicate_is_refused() {
226 assert!(parse_predicate("").is_err());
228 }
229}