1use serde_json::{Map, Value};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct CheckParseError(pub String);
30
31pub fn eval_check(expr: &str, row: &Map<String, Value>) -> Result<bool, CheckParseError> {
36 let tokens = tokenize(expr)?;
37 let mut parser = Parser { tokens, pos: 0 };
38 let result = parser.parse_or(row)?;
39 if parser.pos != parser.tokens.len() {
40 return Err(CheckParseError(format!(
41 "unexpected trailing tokens in check expression: {expr}"
42 )));
43 }
44 Ok(result != Some(false))
46}
47
48#[derive(Debug, Clone, PartialEq)]
49enum Token {
50 Ident(String),
51 Number(f64),
52 Str(String),
53 Bool(bool),
54 Null,
55 Op(String),
56 And,
57 Or,
58 Not,
59 LParen,
60 RParen,
61}
62
63fn tokenize(expr: &str) -> Result<Vec<Token>, CheckParseError> {
64 let chars: Vec<char> = expr.chars().collect();
65 let mut tokens = Vec::new();
66 let mut i = 0;
67 while i < chars.len() {
68 let c = chars[i];
69 if c.is_whitespace() {
70 i += 1;
71 continue;
72 }
73 match c {
74 '(' => {
75 tokens.push(Token::LParen);
76 i += 1;
77 }
78 ')' => {
79 tokens.push(Token::RParen);
80 i += 1;
81 }
82 '\'' | '"' => {
83 let quote = c;
84 i += 1;
85 let mut s = String::new();
86 while i < chars.len() && chars[i] != quote {
87 if chars[i] == '\\' && i + 1 < chars.len() {
88 s.push(chars[i + 1]);
89 i += 2;
90 } else {
91 s.push(chars[i]);
92 i += 1;
93 }
94 }
95 if i >= chars.len() {
96 return Err(CheckParseError("unterminated string literal".into()));
97 }
98 i += 1; tokens.push(Token::Str(s));
100 }
101 '!' if !(i + 1 < chars.len() && chars[i + 1] == '=') => {
103 tokens.push(Token::Not);
104 i += 1;
105 }
106 '=' | '!' | '<' | '>' => {
107 let mut op = String::new();
108 op.push(c);
109 i += 1;
110 if i < chars.len() && (chars[i] == '=' || (c == '<' && chars[i] == '>')) {
111 op.push(chars[i]);
112 i += 1;
113 }
114 tokens.push(Token::Op(op));
115 }
116 '&' if i + 1 < chars.len() && chars[i + 1] == '&' => {
117 tokens.push(Token::And);
118 i += 2;
119 }
120 '|' if i + 1 < chars.len() && chars[i + 1] == '|' => {
121 tokens.push(Token::Or);
122 i += 2;
123 }
124 _ if c.is_ascii_digit()
125 || (c == '-' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit()) =>
126 {
127 let mut num = String::new();
128 num.push(c);
129 i += 1;
130 while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
131 num.push(chars[i]);
132 i += 1;
133 }
134 let value = num
135 .parse::<f64>()
136 .map_err(|_| CheckParseError(format!("invalid number: {num}")))?;
137 tokens.push(Token::Number(value));
138 }
139 _ if c.is_alphabetic() || c == '_' => {
140 let mut ident = String::new();
141 while i < chars.len()
142 && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == '.')
143 {
144 ident.push(chars[i]);
145 i += 1;
146 }
147 match ident.to_ascii_lowercase().as_str() {
148 "and" => tokens.push(Token::And),
149 "or" => tokens.push(Token::Or),
150 "not" => tokens.push(Token::Not),
151 "true" => tokens.push(Token::Bool(true)),
152 "false" => tokens.push(Token::Bool(false)),
153 "null" => tokens.push(Token::Null),
154 _ => tokens.push(Token::Ident(ident)),
155 }
156 }
157 _ => return Err(CheckParseError(format!("unexpected character: {c}"))),
158 }
159 }
160 Ok(tokens)
161}
162
163struct Parser {
164 tokens: Vec<Token>,
165 pos: usize,
166}
167
168#[derive(Debug, Clone, PartialEq)]
169enum Operand {
170 Null,
171 Num(f64),
172 Str(String),
173 Bool(bool),
174}
175
176impl Parser {
177 fn peek(&self) -> Option<&Token> {
178 self.tokens.get(self.pos)
179 }
180
181 fn parse_or(&mut self, row: &Map<String, Value>) -> Result<Option<bool>, CheckParseError> {
182 let mut acc = self.parse_and(row)?;
183 while matches!(self.peek(), Some(Token::Or)) {
184 self.pos += 1;
185 let rhs = self.parse_and(row)?;
186 acc = or3(acc, rhs);
187 }
188 Ok(acc)
189 }
190
191 fn parse_and(&mut self, row: &Map<String, Value>) -> Result<Option<bool>, CheckParseError> {
192 let mut acc = self.parse_not(row)?;
193 while matches!(self.peek(), Some(Token::And)) {
194 self.pos += 1;
195 let rhs = self.parse_not(row)?;
196 acc = and3(acc, rhs);
197 }
198 Ok(acc)
199 }
200
201 fn parse_not(&mut self, row: &Map<String, Value>) -> Result<Option<bool>, CheckParseError> {
202 if matches!(self.peek(), Some(Token::Not)) {
203 self.pos += 1;
204 let inner = self.parse_not(row)?;
205 return Ok(not3(inner));
206 }
207 self.parse_atom(row)
208 }
209
210 fn parse_atom(&mut self, row: &Map<String, Value>) -> Result<Option<bool>, CheckParseError> {
211 if matches!(self.peek(), Some(Token::LParen)) {
212 self.pos += 1;
213 let inner = self.parse_or(row)?;
214 match self.peek() {
215 Some(Token::RParen) => {
216 self.pos += 1;
217 Ok(inner)
218 }
219 _ => Err(CheckParseError("expected closing parenthesis".into())),
220 }
221 } else {
222 self.parse_comparison(row)
223 }
224 }
225
226 fn parse_comparison(
227 &mut self,
228 row: &Map<String, Value>,
229 ) -> Result<Option<bool>, CheckParseError> {
230 let lhs = self.parse_operand(row)?;
231 let op = match self.peek() {
232 Some(Token::Op(op)) => op.clone(),
233 other => {
234 return Err(CheckParseError(format!(
235 "expected comparison operator, found {other:?}"
236 )))
237 }
238 };
239 self.pos += 1;
240 let rhs = self.parse_operand(row)?;
241 Ok(compare(&lhs, &op, &rhs))
242 }
243
244 fn parse_operand(&mut self, row: &Map<String, Value>) -> Result<Operand, CheckParseError> {
245 let tok = self
246 .peek()
247 .cloned()
248 .ok_or_else(|| CheckParseError("unexpected end of expression".into()))?;
249 self.pos += 1;
250 Ok(match tok {
251 Token::Number(n) => Operand::Num(n),
252 Token::Str(s) => Operand::Str(s),
253 Token::Bool(b) => Operand::Bool(b),
254 Token::Null => Operand::Null,
255 Token::Ident(name) => value_to_operand(row.get(&name)),
256 other => {
257 return Err(CheckParseError(format!(
258 "expected an operand, found {other:?}"
259 )))
260 }
261 })
262 }
263}
264
265fn value_to_operand(value: Option<&Value>) -> Operand {
266 match value {
267 None | Some(Value::Null) => Operand::Null,
268 Some(Value::Bool(b)) => Operand::Bool(*b),
269 Some(Value::Number(n)) => Operand::Num(n.as_f64().unwrap_or(f64::NAN)),
270 Some(Value::String(s)) => Operand::Str(s.clone()),
271 Some(other) => Operand::Str(other.to_string()),
272 }
273}
274
275fn compare(lhs: &Operand, op: &str, rhs: &Operand) -> Option<bool> {
277 if matches!(lhs, Operand::Null) || matches!(rhs, Operand::Null) {
278 return None;
279 }
280 let ord = match (lhs, rhs) {
281 (Operand::Num(a), Operand::Num(b)) => a.partial_cmp(b),
282 (Operand::Str(a), Operand::Str(b)) => Some(a.cmp(b)),
283 (Operand::Bool(a), Operand::Bool(b)) => Some(a.cmp(b)),
284 _ => None,
286 };
287 match op {
288 "=" | "==" => Some(lhs == rhs),
289 "!=" | "<>" => Some(lhs != rhs),
290 "<" => ord.map(|o| o.is_lt()),
291 "<=" => ord.map(|o| o.is_le()),
292 ">" => ord.map(|o| o.is_gt()),
293 ">=" => ord.map(|o| o.is_ge()),
294 _ => None,
295 }
296}
297
298fn not3(v: Option<bool>) -> Option<bool> {
299 v.map(|b| !b)
300}
301
302fn and3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
303 match (a, b) {
304 (Some(false), _) | (_, Some(false)) => Some(false),
305 (Some(true), Some(true)) => Some(true),
306 _ => None,
307 }
308}
309
310fn or3(a: Option<bool>, b: Option<bool>) -> Option<bool> {
311 match (a, b) {
312 (Some(true), _) | (_, Some(true)) => Some(true),
313 (Some(false), Some(false)) => Some(false),
314 _ => None,
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use serde_json::json;
322
323 fn row(v: Value) -> Map<String, Value> {
324 v.as_object().unwrap().clone()
325 }
326
327 #[test]
328 fn passes_simple_comparison() {
329 let r = row(json!({ "age": 30 }));
330 assert!(eval_check("age >= 0", &r).unwrap());
331 }
332
333 #[test]
334 fn fails_simple_comparison() {
335 let r = row(json!({ "age": -1 }));
336 assert!(!eval_check("age >= 0", &r).unwrap());
337 }
338
339 #[test]
340 fn null_comparison_passes() {
341 let r = row(json!({ "age": null }));
342 assert!(eval_check("age >= 0", &r).unwrap());
343 }
344
345 #[test]
346 fn and_or_combination() {
347 let r = row(json!({ "a": 5, "b": 10 }));
348 assert!(eval_check("a > 0 AND b > 0", &r).unwrap());
349 assert!(!eval_check("a > 0 AND b < 0", &r).unwrap());
350 assert!(eval_check("a < 0 OR b > 0", &r).unwrap());
351 assert!(!eval_check("a < 0 OR b < 0", &r).unwrap());
352 }
353
354 #[test]
355 fn parentheses_and_not() {
356 let r = row(json!({ "a": 5, "b": 10 }));
357 assert!(eval_check("NOT (a > 100)", &r).unwrap());
358 assert!(eval_check("(a = 5 OR a = 6) AND b = 10", &r).unwrap());
359 }
360
361 #[test]
362 fn bang_is_logical_not() {
363 let r = row(json!({ "a": 5 }));
364 assert!(eval_check("!(a > 100)", &r).unwrap());
366 assert!(!eval_check("!(a > 0)", &r).unwrap());
367 assert!(eval_check("a != 6", &r).unwrap());
368 assert!(!eval_check("a != 5", &r).unwrap());
369 }
370
371 #[test]
372 fn string_and_cross_column() {
373 let r = row(json!({ "lo": 1, "hi": 9, "kind": "x" }));
374 assert!(eval_check("lo < hi", &r).unwrap());
375 assert!(eval_check("kind = 'x'", &r).unwrap());
376 assert!(!eval_check("kind != 'x'", &r).unwrap());
377 }
378
379 #[test]
380 fn rejects_garbage() {
381 let r = row(json!({}));
382 assert!(eval_check("@@@", &r).is_err());
383 }
384}