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