use areev_cal::ast::Condition;
use areev_cal::executor::{grain_matches_condition_tree, CalGrainResult};
use areev_core::format::deserialize::DeserializedGrain;
use crate::error::{Result, TriggerError};
pub fn parse_predicate(where_src: &str) -> Result<Condition> {
let probe = format!("RECALL grains WHERE {where_src}");
let query = areev_cal::parse(&probe).map_err(|e| TriggerError::Malformed {
what: format!("predicate {where_src:?} does not parse: {e}"),
})?;
match query.statement {
areev_cal::ast::CalStatement::Recall(r) => r
.where_clause
.map(|w| w.condition)
.ok_or_else(|| TriggerError::Malformed { what: "predicate is empty".into() }),
_ => Err(TriggerError::Malformed {
what: format!("predicate {where_src:?} is not a filter expression"),
}),
}
}
pub fn to_value(c: &Condition) -> Result<serde_json::Value> {
serde_json::to_value(c)
.map_err(|e| TriggerError::Malformed { what: format!("predicate is not serializable: {e}") })
}
pub fn from_value(v: &serde_json::Value) -> Result<Condition> {
serde_json::from_value(v.clone())
.map_err(|e| TriggerError::Malformed { what: format!("stored predicate is unreadable: {e}") })
}
pub fn grain_matches(grain: &DeserializedGrain, condition: &Condition) -> bool {
let projected = CalGrainResult {
hash: grain.hash.to_hex(),
grain_type: grain.grain_type.as_str().to_string(),
score: 0.0,
fields: serde_json::to_value(&grain.fields).unwrap_or(serde_json::Value::Null),
score_breakdown: None,
explanation: None,
relative_time: None,
is_deterministic: false,
contested_by: None,
};
grain_matches_condition_tree(&projected, condition)
}
pub fn gate_satisfied(condition: &Condition, fired: &[String]) -> bool {
let fields: serde_json::Map<String, serde_json::Value> = referenced_members(condition)
.into_iter()
.map(|m| {
let has_fired = fired.contains(&m);
(m, serde_json::Value::Bool(has_fired))
})
.collect();
let projected = CalGrainResult {
hash: String::new(),
grain_type: "gate".into(),
score: 0.0,
fields: serde_json::Value::Object(fields),
score_breakdown: None,
explanation: None,
relative_time: None,
is_deterministic: false,
contested_by: None,
};
grain_matches_condition_tree(&projected, condition)
}
pub fn referenced_members(condition: &Condition) -> Vec<String> {
fn walk(c: &Condition, out: &mut Vec<String>) {
match c {
Condition::Comparison { field, .. }
| Condition::In { field, .. }
| Condition::NotIn { field, .. }
| Condition::IsNull { field, .. }
| Condition::IsNotNull { field, .. }
| Condition::Contains { field, .. }
| Condition::StartsWith { field, .. }
| Condition::IsCategory { field, .. } => out.push(field.clone()),
Condition::And { left, right, .. } | Condition::Or { left, right, .. } => {
walk(left, out);
walk(right, out);
}
Condition::Not { inner, .. } => walk(inner, out),
}
}
let mut out = Vec::new();
walk(condition, &mut out);
out.sort();
out.dedup();
out
}
#[cfg(test)]
mod tests {
use super::*;
fn gate(src: &str) -> Condition {
parse_predicate(src).unwrap()
}
#[test]
fn boolean_composition_round_trips_through_storage() {
let c = gate("(a = true AND b = true) OR c = true");
let back = from_value(&to_value(&c).unwrap()).unwrap();
for fired in [
vec![],
vec!["a".to_string()],
vec!["c".to_string()],
vec!["a".to_string(), "b".to_string()],
vec!["a".to_string(), "b".to_string(), "c".to_string()],
] {
assert_eq!(
gate_satisfied(&c, &fired),
gate_satisfied(&back, &fired),
"storage changed the meaning for {fired:?}"
);
}
assert_eq!(referenced_members(&c), referenced_members(&back));
}
#[test]
fn and_requires_both_members() {
let c = gate("a = true AND b = true");
assert!(!gate_satisfied(&c, &["a".into()]));
assert!(!gate_satisfied(&c, &["b".into()]));
assert!(gate_satisfied(&c, &["a".into(), "b".into()]));
}
#[test]
fn or_requires_either() {
let c = gate("a = true OR b = true");
assert!(gate_satisfied(&c, &["a".into()]));
assert!(gate_satisfied(&c, &["b".into()]));
assert!(!gate_satisfied(&c, &[]));
}
#[test]
fn parenthesised_precedence_matches_the_written_expression() {
let c = gate("(a = true AND b = true) OR c = true");
assert!(gate_satisfied(&c, &["c".into()]), "c alone satisfies the right branch");
assert!(gate_satisfied(&c, &["a".into(), "b".into()]));
assert!(!gate_satisfied(&c, &["a".into()]), "a alone satisfies neither branch");
}
#[test]
fn precedence_is_or_below_and_not_left_to_right() {
let c = gate("a = true AND b = true OR c = true");
assert!(gate_satisfied(&c, &["c".into()]), "c alone must satisfy it");
}
#[test]
fn negation_works() {
let c = gate("NOT (a = true)");
assert!(gate_satisfied(&c, &[]), "a absent means the negation holds");
assert!(!gate_satisfied(&c, &["a".into()]));
}
#[test]
fn referenced_members_finds_every_name() {
let c = gate("(a = true AND b = true) OR NOT (c = true)");
assert_eq!(referenced_members(&c), vec!["a", "b", "c"]);
}
#[test]
fn a_predicate_that_does_not_parse_is_refused_with_its_code() {
let e = parse_predicate("this is not a predicate").unwrap_err();
assert_eq!(e.code(), "TRG-E001");
}
#[test]
fn an_empty_predicate_is_refused() {
assert!(parse_predicate("").is_err());
}
}