1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8#[serde(rename_all = "snake_case")]
9#[non_exhaustive]
10pub enum Op {
11 Eq,
13 Ne,
15 Gt,
17 Gte,
19 Lt,
21 Lte,
23 Exists,
25 Contains,
27}
28
29impl Op {
30 pub const ALL: [Op; 8] = [
39 Op::Eq,
40 Op::Ne,
41 Op::Gt,
42 Op::Gte,
43 Op::Lt,
44 Op::Lte,
45 Op::Exists,
46 Op::Contains,
47 ];
48}
49
50#[allow(dead_code)]
57fn assert_all_ops_exhaustive(op: &Op) {
58 match op {
59 Op::Eq | Op::Ne | Op::Gt | Op::Gte | Op::Lt | Op::Lte | Op::Exists | Op::Contains => {}
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65#[serde(deny_unknown_fields)]
66pub struct Condition {
67 pub field: String,
69 pub op: Op,
71 #[serde(default)]
73 pub value: Value,
74}
75
76pub fn compile_condition(c: &Condition) -> impl Fn(&Value) -> bool + Send + Sync + 'static {
80 let cond = c.clone();
81 move |env: &Value| {
82 let lhs = env.get(&cond.field);
83 match cond.op {
84 Op::Exists => lhs.is_some_and(|v| !v.is_null()),
85 Op::Eq => lhs == Some(&cond.value),
86 Op::Ne => lhs != Some(&cond.value),
87 Op::Gt | Op::Gte | Op::Lt | Op::Lte => compare_numbers(lhs, &cond.value, &cond.op),
88 Op::Contains => contains(lhs, &cond.value),
89 }
90 }
91}
92
93fn compare_numbers(lhs: Option<&Value>, rhs: &Value, op: &Op) -> bool {
94 let (Some(a), Some(b)) = (lhs.and_then(Value::as_f64), rhs.as_f64()) else {
95 return false;
96 };
97 match op {
98 Op::Gt => a > b,
99 Op::Gte => a >= b,
100 Op::Lt => a < b,
101 Op::Lte => a <= b,
102 _ => false,
103 }
104}
105
106fn contains(lhs: Option<&Value>, needle: &Value) -> bool {
107 match lhs {
108 Some(Value::String(s)) => needle.as_str().is_some_and(|n| s.contains(n)),
109 Some(Value::Array(items)) => items.contains(needle),
110 _ => false,
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use serde_json::json;
118
119 fn check(field: &str, op: Op, value: Value, env: Value) -> bool {
120 compile_condition(&Condition {
121 field: field.into(),
122 op,
123 value,
124 })(&env)
125 }
126
127 #[test]
128 fn gte_true_and_the_rejected_negatives() {
129 assert!(check("risk", Op::Gte, json!(0.8), json!({"risk": 0.8})));
130 assert!(check("risk", Op::Gte, json!(0.8), json!({"risk": 0.9})));
131 assert!(!check("risk", Op::Gte, json!(0.8), json!({"risk": 0.7})));
132 assert!(!check("risk", Op::Gte, json!(0.8), json!({"risk": "high"})));
133 assert!(!check("risk", Op::Gte, json!(0.8), json!({})));
134 }
135
136 #[test]
137 fn exists_ignores_value_and_null() {
138 assert!(check("a", Op::Exists, Value::Null, json!({"a": 1})));
139 assert!(!check("a", Op::Exists, Value::Null, json!({"a": null})));
140 assert!(!check("a", Op::Exists, Value::Null, json!({})));
141 }
142
143 #[test]
144 fn gt_is_strict_above_boundary() {
145 assert!(check("n", Op::Gt, json!(1.0), json!({"n": 1.1})));
146 assert!(!check("n", Op::Gt, json!(1.0), json!({"n": 1.0})));
147 }
148
149 #[test]
150 fn lt_is_strict_below_boundary() {
151 assert!(check("n", Op::Lt, json!(1.0), json!({"n": 0.9})));
152 assert!(!check("n", Op::Lt, json!(1.0), json!({"n": 1.0})));
153 }
154
155 #[test]
156 fn lte_includes_boundary_excludes_above() {
157 assert!(check("n", Op::Lte, json!(1.0), json!({"n": 1.0})));
158 assert!(!check("n", Op::Lte, json!(1.0), json!({"n": 1.1})));
159 }
160
161 #[test]
162 fn contains_string_and_array() {
163 assert!(check(
164 "s",
165 Op::Contains,
166 json!("ell"),
167 json!({"s": "hello"})
168 ));
169 assert!(check(
170 "xs",
171 Op::Contains,
172 json!(2),
173 json!({"xs": [1, 2, 3]})
174 ));
175 assert!(!check(
176 "s",
177 Op::Contains,
178 json!("zzz"),
179 json!({"s": "hello"})
180 ));
181 }
182
183 #[test]
184 fn contains_rejects_absent_member_and_wrong_type() {
185 assert!(!check(
186 "xs",
187 Op::Contains,
188 json!(4),
189 json!({"xs": [1, 2, 3]})
190 ));
191 assert!(!check("n", Op::Contains, json!(2), json!({"n": 42})));
192 }
193
194 #[test]
195 fn eq_and_ne() {
196 assert!(check("k", Op::Eq, json!("v"), json!({"k": "v"})));
197 assert!(check("k", Op::Ne, json!("v"), json!({"k": "x"})));
198 }
199
200 #[test]
201 fn eq_false_when_unequal() {
202 assert!(!check("k", Op::Eq, json!("v"), json!({"k": "x"})));
203 }
204
205 #[test]
206 fn ne_false_when_equal() {
207 assert!(!check("k", Op::Ne, json!("v"), json!({"k": "v"})));
208 }
209}