1use std::borrow::Cow;
17use std::cmp::Ordering;
18use std::collections::BTreeMap;
19
20use serde_json::{Number, Value as JsonValue};
21use thiserror::Error;
22
23use chrono::{Datelike, NaiveDateTime, Timelike};
24
25use crate::ast::{BinOp, Expression, Operator, SetValue, Value};
26use crate::{ANSI_BOLD, ANSI_FG_CYAN, ANSI_FG_RED, ANSI_FG_YELLOW, ANSI_RESET};
27
28impl EvalError {
29 #[must_use]
31 pub fn format_color(&self) -> String {
32 use std::fmt::Write;
33 let mut out = String::new();
34
35 write!(
36 out,
37 "{bold}{red}error{reset}: {msg}",
38 bold = ANSI_BOLD,
39 red = ANSI_FG_RED,
40 reset = ANSI_RESET,
41 msg = self,
42 )
43 .expect("write! to String is infallible");
44
45 match self {
46 EvalError::UnsupportedField { field } => {
47 write!(
48 out,
49 "\n {yellow}help{reset}: field `{field}` does not exist on this target",
50 yellow = ANSI_FG_YELLOW,
51 reset = ANSI_RESET,
52 field = field,
53 )
54 .expect("write! to String is infallible");
55 write!(
56 out,
57 "\n {cyan}note{reset}: use `fields <target>` to list available fields",
58 cyan = ANSI_FG_CYAN,
59 reset = ANSI_RESET,
60 )
61 .expect("write! to String is infallible");
62 }
63 EvalError::InvalidComparison { field, operator } => {
64 write!(
65 out,
66 "\n {yellow}help{reset}: field `{field}` cannot be used with operator `{operator}`",
67 yellow = ANSI_FG_YELLOW,
68 reset = ANSI_RESET,
69 field = field,
70 operator = operator,
71 )
72 .expect("write! to String is infallible");
73 }
74 EvalError::UnknownFunction { name } => {
75 write!(
76 out,
77 "\n {yellow}help{reset}: `{name}` is not a known function",
78 yellow = ANSI_FG_YELLOW,
79 reset = ANSI_RESET,
80 name = name,
81 )
82 .expect("write! to String is infallible");
83 }
84 EvalError::InvalidArguments { name } => {
85 write!(
86 out,
87 "\n {yellow}help{reset}: check the arguments for `{name}()`",
88 yellow = ANSI_FG_YELLOW,
89 reset = ANSI_RESET,
90 name = name,
91 )
92 .expect("write! to String is infallible");
93 }
94 _ => {}
95 }
96
97 out
98 }
99}
100
101#[derive(Debug, Error)]
102pub enum EvalError {
103 #[error("unsupported field `{field}`")]
104 UnsupportedField { field: String },
105 #[error("field `{field}` cannot be compared with operator `{operator}`")]
106 InvalidComparison { field: String, operator: String },
107 #[error("arithmetic error: {0}")]
108 Arithmetic(String),
109 #[error("unknown function `{name}`")]
110 UnknownFunction { name: String },
111 #[error("invalid arguments for `{name}`")]
112 InvalidArguments { name: String },
113}
114
115pub fn eval_expr(
117 fields: &BTreeMap<String, JsonValue>,
118 expression: &Expression,
119) -> Result<JsonValue, EvalError> {
120 match expression {
121 Expression::Field(field) => resolve_field(fields, field).map(std::borrow::Cow::into_owned),
122
123 Expression::Literal(value) => Ok(value_to_json(value)),
124
125 Expression::Arithmetic { left, op, right } => {
126 let l = eval_expr(fields, left)?;
127 let r = eval_expr(fields, right)?;
128 apply_arithmetic(&l, *op, &r)
129 }
130
131 Expression::Comparison {
132 left,
133 operator,
134 right,
135 } => {
136 let result = eval_bool_expr(fields, left, *operator, right)?;
137 Ok(JsonValue::Bool(result))
138 }
139
140 Expression::In { expr, values } => {
141 let actual = eval_expr(fields, expr)?;
142 for v in values {
143 if json_value_eq(&actual, v) {
144 return Ok(JsonValue::Bool(true));
145 }
146 }
147 Ok(JsonValue::Bool(false))
148 }
149
150 Expression::Between { expr, low, high } => {
151 let val = eval_expr(fields, expr)?;
152 let l = eval_expr(fields, low)?;
153 let h = eval_expr(fields, high)?;
154 let vf = json_as_f64(&val);
155 let lf = json_as_f64(&l);
156 let hf = json_as_f64(&h);
157 match (vf, lf, hf) {
158 (Some(v), Some(l), Some(h)) => Ok(JsonValue::Bool(v >= l && v <= h)),
159 _ => Ok(JsonValue::Bool(false)),
160 }
161 }
162
163 Expression::IsNull(expr) => {
164 let val = eval_expr_opt(fields, expr);
165 let is_null = match val {
166 None | Some(JsonValue::Null) => true,
167 Some(val) if val.is_string() && val.as_str().unwrap_or("").is_empty() => true,
168 _ => false,
169 };
170 Ok(JsonValue::Bool(is_null))
171 }
172
173 Expression::IsNotNull(expr) => {
174 let val = eval_expr_opt(fields, expr);
175 let is_not_null = match val {
176 None | Some(JsonValue::Null) => false,
177 Some(val) if val.is_string() && val.as_str().unwrap_or("").is_empty() => false,
178 _ => true,
179 };
180 Ok(JsonValue::Bool(is_not_null))
181 }
182
183 Expression::FnCall { name, args } => {
184 let evaluated: Result<Vec<JsonValue>, _> =
185 args.iter().map(|a| eval_expr(fields, a)).collect();
186 apply_function(name, &evaluated?)
187 }
188
189 Expression::And(left, right) => {
190 let l = eval_bool(fields, left)?;
191 if !l {
192 return Ok(JsonValue::Bool(false));
193 }
194 let r = eval_bool(fields, right)?;
195 Ok(JsonValue::Bool(r))
196 }
197
198 Expression::Or(left, right) => {
199 let l = eval_bool(fields, left)?;
200 if l {
201 return Ok(JsonValue::Bool(true));
202 }
203 let r = eval_bool(fields, right)?;
204 Ok(JsonValue::Bool(r))
205 }
206
207 Expression::Not(expr) => {
208 let val = eval_bool(fields, expr)?;
209 Ok(JsonValue::Bool(!val))
210 }
211 }
212}
213
214fn eval_expr_opt(
216 fields: &BTreeMap<String, JsonValue>,
217 expression: &Expression,
218) -> Option<JsonValue> {
219 match expression {
220 Expression::Field(field) => fields.get(field).cloned().or_else(|| {
221 if let Some(label_key) = field.strip_prefix("label.")
223 && let Some(JsonValue::Array(items)) = fields.get("labels")
224 {
225 for item in items {
226 if let JsonValue::String(entry) = item
227 && let Some(eq_pos) = entry.find('=')
228 {
229 let key = &entry[..eq_pos];
230 let val = &entry[eq_pos + 1..];
231 if key == label_key {
232 return Some(JsonValue::String(val.to_owned()));
233 }
234 }
235 }
236 }
237 None
238 }),
239 Expression::Literal(value) => Some(value_to_json(value)),
240 _ => eval_expr(fields, expression).ok(),
241 }
242}
243
244pub fn eval_bool(
246 fields: &BTreeMap<String, JsonValue>,
247 expression: &Expression,
248) -> Result<bool, EvalError> {
249 let val = eval_expr(fields, expression)?;
250 Ok(json_is_truthy(&val))
251}
252
253pub fn evaluate_expression(
255 fields: &BTreeMap<String, JsonValue>,
256 expression: &Expression,
257) -> Result<bool, EvalError> {
258 eval_bool(fields, expression)
259}
260
261fn json_is_truthy(val: &JsonValue) -> bool {
262 match val {
263 JsonValue::Null => false,
264 JsonValue::Bool(b) => *b,
265 JsonValue::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
266 JsonValue::String(s) => !s.is_empty() && s != "0" && s.to_lowercase() != "false",
267 JsonValue::Array(a) => !a.is_empty(),
268 JsonValue::Object(_) => true,
269 }
270}
271
272fn eval_bool_expr(
273 fields: &BTreeMap<String, JsonValue>,
274 left: &Expression,
275 operator: Operator,
276 right: &Expression,
277) -> Result<bool, EvalError> {
278 let l = eval_expr(fields, left)?;
279 let r = eval_expr(fields, right)?;
280
281 match operator {
282 Operator::Eq => Ok(json_eq(&l, &r)),
283 Operator::NotEq => Ok(!json_eq(&l, &r)),
284 Operator::Contains => {
285 json_contains_val(&l, &r).ok_or_else(|| EvalError::InvalidComparison {
286 field: format!("{l:?}"),
287 operator: "Contains".to_owned(),
288 })
289 }
290 Operator::Matches => {
291 let pattern = match &r {
292 JsonValue::String(s) => s.clone(),
293 _ => {
294 return Err(EvalError::InvalidComparison {
295 field: format!("{l:?}"),
296 operator: "Matches".to_owned(),
297 });
298 }
299 };
300 let re = regex::Regex::new(&pattern).map_err(|_| EvalError::InvalidComparison {
301 field: format!("{l:?}"),
302 operator: "Matches (invalid regex)".to_owned(),
303 })?;
304 match &l {
305 JsonValue::String(s) => Ok(re.is_match(s)),
306 JsonValue::Array(values) => Ok(values.iter().any(|v| {
307 let s = render_json_cell(v);
308 re.is_match(&s)
309 })),
310 _ => Err(EvalError::InvalidComparison {
311 field: format!("{l:?}"),
312 operator: "Matches".to_owned(),
313 }),
314 }
315 }
316 Operator::StartsWith => {
317 let l_str = match &l {
318 JsonValue::String(s) => s.clone(),
319 _ => {
320 return Err(EvalError::InvalidComparison {
321 field: format!("{l:?}"),
322 operator: "StartsWith".to_owned(),
323 });
324 }
325 };
326 let r_str = match &r {
327 JsonValue::String(s) => s.clone(),
328 _ => {
329 return Err(EvalError::InvalidComparison {
330 field: format!("{r:?}"),
331 operator: "StartsWith".to_owned(),
332 });
333 }
334 };
335 Ok(l_str.starts_with(&r_str))
336 }
337 Operator::EndsWith => {
338 let l_str = match &l {
339 JsonValue::String(s) => s.clone(),
340 _ => {
341 return Err(EvalError::InvalidComparison {
342 field: format!("{l:?}"),
343 operator: "EndsWith".to_owned(),
344 });
345 }
346 };
347 let r_str = match &r {
348 JsonValue::String(s) => s.clone(),
349 _ => {
350 return Err(EvalError::InvalidComparison {
351 field: format!("{r:?}"),
352 operator: "EndsWith".to_owned(),
353 });
354 }
355 };
356 Ok(l_str.ends_with(&r_str))
357 }
358 Operator::Gt | Operator::Lt | Operator::Gte | Operator::Lte => {
359 let ln = json_as_f64(&l).ok_or_else(|| EvalError::InvalidComparison {
360 field: format!("{l:?}"),
361 operator: format!("{operator:?}"),
362 })?;
363 let rn = json_as_f64(&r).ok_or_else(|| EvalError::InvalidComparison {
364 field: format!("{r:?}"),
365 operator: format!("{operator:?}"),
366 })?;
367 Ok(match operator {
368 Operator::Gt => ln > rn,
369 Operator::Lt => ln < rn,
370 Operator::Gte => ln >= rn,
371 Operator::Lte => ln <= rn,
372 _ => unreachable!(),
373 })
374 }
375 }
376}
377
378fn apply_arithmetic(
379 left: &JsonValue,
380 op: BinOp,
381 right: &JsonValue,
382) -> Result<JsonValue, EvalError> {
383 let l = json_as_f64(left)
384 .ok_or_else(|| EvalError::Arithmetic(format!("left operand is not numeric: {left:?}")))?;
385 let r = json_as_f64(right)
386 .ok_or_else(|| EvalError::Arithmetic(format!("right operand is not numeric: {right:?}")))?;
387 let result = match op {
388 BinOp::Add => l + r,
389 BinOp::Sub => l - r,
390 BinOp::Mul => l * r,
391 BinOp::Div => {
392 if r == 0.0 {
393 return Err(EvalError::Arithmetic("division by zero".to_owned()));
394 }
395 l / r
396 }
397 BinOp::Mod => {
398 if r == 0.0 {
399 return Err(EvalError::Arithmetic("modulo by zero".to_owned()));
400 }
401 l % r
402 }
403 };
404 Number::from_f64(result)
405 .map(JsonValue::Number)
406 .ok_or_else(|| EvalError::Arithmetic("result is NaN or infinity".to_owned()))
407}
408
409fn apply_function(name: &str, args: &[JsonValue]) -> Result<JsonValue, EvalError> {
410 match name {
411 "upper" => {
412 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
413 EvalError::InvalidArguments {
414 name: name.to_owned(),
415 }
416 })?;
417 Ok(JsonValue::String(s.to_uppercase()))
418 }
419 "lower" => {
420 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
421 EvalError::InvalidArguments {
422 name: name.to_owned(),
423 }
424 })?;
425 Ok(JsonValue::String(s.to_lowercase()))
426 }
427 "length" => {
428 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
429 EvalError::InvalidArguments {
430 name: name.to_owned(),
431 }
432 })?;
433 Ok(JsonValue::Number(Number::from(s.len() as u64)))
434 }
435 "trim" => {
436 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
437 EvalError::InvalidArguments {
438 name: name.to_owned(),
439 }
440 })?;
441 Ok(JsonValue::String(s.trim().to_owned()))
442 }
443 "concat" => {
444 let result: String = args.iter().map(render_json_cell).collect();
445 Ok(JsonValue::String(result))
446 }
447 "substring" => {
448 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
449 EvalError::InvalidArguments {
450 name: name.to_owned(),
451 }
452 })?;
453 let start = args.get(1).and_then(json_as_f64).map_or(0, |f| f as usize);
454 let len = args
455 .get(2)
456 .and_then(json_as_f64)
457 .map_or(s.len(), |f| f as usize);
458 let end = (start + len).min(s.len());
459 let sub = if start < s.len() { &s[start..end] } else { "" };
460 Ok(JsonValue::String(sub.to_owned()))
461 }
462 "coalesce" => {
463 for arg in args {
464 if !(matches!(arg, JsonValue::Null)
465 || arg.is_string() && arg.as_str().unwrap_or("").is_empty())
466 {
467 return Ok(arg.clone());
468 }
469 }
470 Ok(JsonValue::Null)
471 }
472 "starts_with" => {
473 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
474 EvalError::InvalidArguments {
475 name: name.to_owned(),
476 }
477 })?;
478 let prefix = args.get(1).and_then(|v| v.as_str()).ok_or_else(|| {
479 EvalError::InvalidArguments {
480 name: name.to_owned(),
481 }
482 })?;
483 Ok(JsonValue::Bool(s.starts_with(prefix)))
484 }
485 "ends_with" => {
486 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
487 EvalError::InvalidArguments {
488 name: name.to_owned(),
489 }
490 })?;
491 let suffix = args.get(1).and_then(|v| v.as_str()).ok_or_else(|| {
492 EvalError::InvalidArguments {
493 name: name.to_owned(),
494 }
495 })?;
496 Ok(JsonValue::Bool(s.ends_with(suffix)))
497 }
498 "replace" => {
499 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
500 EvalError::InvalidArguments {
501 name: name.to_owned(),
502 }
503 })?;
504 let from = args.get(1).and_then(|v| v.as_str()).ok_or_else(|| {
505 EvalError::InvalidArguments {
506 name: name.to_owned(),
507 }
508 })?;
509 let to = args.get(2).and_then(|v| v.as_str()).ok_or_else(|| {
510 EvalError::InvalidArguments {
511 name: name.to_owned(),
512 }
513 })?;
514 Ok(JsonValue::String(s.replace(from, to)))
515 }
516 "reverse" => {
517 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
518 EvalError::InvalidArguments {
519 name: name.to_owned(),
520 }
521 })?;
522 Ok(JsonValue::String(s.chars().rev().collect()))
523 }
524 "repeat" => {
525 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
526 EvalError::InvalidArguments {
527 name: name.to_owned(),
528 }
529 })?;
530 let n = args.get(1).and_then(json_as_f64).map_or(0, |f| f as usize);
531 Ok(JsonValue::String(s.repeat(n)))
532 }
533 "position" => {
534 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
535 EvalError::InvalidArguments {
536 name: name.to_owned(),
537 }
538 })?;
539 let substr = args.get(1).and_then(|v| v.as_str()).ok_or_else(|| {
540 EvalError::InvalidArguments {
541 name: name.to_owned(),
542 }
543 })?;
544 let pos = s.find(substr).map_or(0, |i| i as u64);
545 Ok(JsonValue::Number(Number::from(pos)))
546 }
547 "now" => Ok(JsonValue::String(chrono::Utc::now().to_rfc3339())),
548 "date_format" => {
549 let ts = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
550 EvalError::InvalidArguments {
551 name: name.to_owned(),
552 }
553 })?;
554 let fmt = args.get(1).and_then(|v| v.as_str()).ok_or_else(|| {
555 EvalError::InvalidArguments {
556 name: name.to_owned(),
557 }
558 })?;
559 let dt = parse_timestamp(ts)?;
560 let result = dt.format(fmt).to_string();
561 Ok(JsonValue::String(result))
562 }
563 "date_diff" => {
564 let a = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
565 EvalError::InvalidArguments {
566 name: name.to_owned(),
567 }
568 })?;
569 let b = args.get(1).and_then(|v| v.as_str()).ok_or_else(|| {
570 EvalError::InvalidArguments {
571 name: name.to_owned(),
572 }
573 })?;
574 let unit = args.get(2).and_then(|v| v.as_str()).ok_or_else(|| {
575 EvalError::InvalidArguments {
576 name: name.to_owned(),
577 }
578 })?;
579 let dt_a = parse_timestamp(a)?;
580 let dt_b = parse_timestamp(b)?;
581 let diff = dt_b.signed_duration_since(dt_a);
582 let result = match unit {
583 "seconds" | "s" => diff.num_seconds(),
584 "minutes" | "m" => diff.num_minutes(),
585 "hours" | "h" => diff.num_hours(),
586 "days" | "d" => diff.num_days(),
587 _ => {
588 return Err(EvalError::InvalidArguments {
589 name: name.to_owned(),
590 });
591 }
592 };
593 Ok(JsonValue::Number(Number::from(result)))
594 }
595 "extract" => {
596 let ts = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
597 EvalError::InvalidArguments {
598 name: name.to_owned(),
599 }
600 })?;
601 let part = args.get(1).and_then(|v| v.as_str()).ok_or_else(|| {
602 EvalError::InvalidArguments {
603 name: name.to_owned(),
604 }
605 })?;
606 let dt = parse_timestamp(ts)?;
607 let result = match part {
608 "year" => i64::from(dt.year()),
609 "month" => i64::from(dt.month()),
610 "day" => i64::from(dt.day()),
611 "hour" => i64::from(dt.hour()),
612 "minute" => i64::from(dt.minute()),
613 "second" => i64::from(dt.second()),
614 _ => {
615 return Err(EvalError::InvalidArguments {
616 name: name.to_owned(),
617 });
618 }
619 };
620 Ok(JsonValue::Number(Number::from(result)))
621 }
622 "split_part" => {
623 let s = args.first().and_then(|v| v.as_str()).ok_or_else(|| {
624 EvalError::InvalidArguments {
625 name: name.to_owned(),
626 }
627 })?;
628 let delim = args.get(1).and_then(|v| v.as_str()).ok_or_else(|| {
629 EvalError::InvalidArguments {
630 name: name.to_owned(),
631 }
632 })?;
633 let n = args.get(2).and_then(json_as_f64).map_or(1, |f| f as usize);
634 let part = s
635 .split(delim)
636 .nth(n.saturating_sub(1))
637 .unwrap_or("")
638 .to_owned();
639 Ok(JsonValue::String(part))
640 }
641 "to_int" => {
642 let val = args.first().ok_or_else(|| EvalError::InvalidArguments {
643 name: name.to_owned(),
644 })?;
645 let result = match val {
646 JsonValue::Number(n) => n.as_f64().map(|f| f as i64).unwrap_or(0),
647 JsonValue::String(s) => {
648 if let Ok(n) = s.parse::<i64>() {
649 n
650 } else if let Ok(f) = s.parse::<f64>() {
651 f as i64
652 } else {
653 0
654 }
655 }
656 JsonValue::Bool(b) => i64::from(*b),
657 _ => 0,
658 };
659 Ok(JsonValue::Number(Number::from(result)))
660 }
661 "to_float" => {
662 let val = args.first().ok_or_else(|| EvalError::InvalidArguments {
663 name: name.to_owned(),
664 })?;
665 let result = json_as_f64(val).unwrap_or(0.0);
666 Number::from_f64(result)
667 .map(JsonValue::Number)
668 .ok_or_else(|| EvalError::InvalidArguments {
669 name: name.to_owned(),
670 })
671 }
672 "to_string" => {
673 let val = args.first().ok_or_else(|| EvalError::InvalidArguments {
674 name: name.to_owned(),
675 })?;
676 Ok(JsonValue::String(render_json_cell(val)))
677 }
678 _ => Err(EvalError::UnknownFunction {
679 name: name.to_owned(),
680 }),
681 }
682}
683
684fn parse_timestamp(s: &str) -> Result<NaiveDateTime, EvalError> {
685 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
687 return Ok(dt.naive_utc());
688 }
689 if let Ok(dt) = chrono::DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
690 return Ok(dt.naive_utc());
691 }
692 if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
693 return Ok(dt);
694 }
695 if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") {
696 return Ok(dt);
697 }
698 if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.fZ") {
699 return Ok(dt);
700 }
701 Err(EvalError::InvalidArguments {
702 name: "timestamp".to_owned(),
703 })
704}
705
706fn resolve_field<'a>(
707 fields: &'a BTreeMap<String, JsonValue>,
708 field: &str,
709) -> Result<Cow<'a, JsonValue>, EvalError> {
710 if let Some(value) = fields.get(field) {
711 return Ok(Cow::Borrowed(value));
712 }
713
714 if let Some(label_key) = field.strip_prefix("label.") {
715 let labels = fields
716 .get("labels")
717 .ok_or_else(|| EvalError::UnsupportedField {
718 field: field.to_owned(),
719 })?;
720
721 match labels {
722 JsonValue::Array(items) => {
723 for item in items {
724 if let JsonValue::String(entry) = item
725 && let Some(eq_pos) = entry.find('=')
726 {
727 let key = &entry[..eq_pos];
728 let val = &entry[eq_pos + 1..];
729 if key == label_key {
730 return Ok(Cow::Owned(JsonValue::String(val.to_owned())));
731 }
732 }
733 }
734 }
735 JsonValue::String(s) => {
736 for entry in s.split(',') {
737 let entry = entry.trim();
738 if let Some(eq_pos) = entry.find('=') {
739 let key = &entry[..eq_pos];
740 let val = &entry[eq_pos + 1..];
741 if key == label_key {
742 return Ok(Cow::Owned(JsonValue::String(val.to_owned())));
743 }
744 }
745 }
746 }
747 _ => {}
748 }
749
750 return Err(EvalError::UnsupportedField {
751 field: field.to_owned(),
752 });
753 }
754
755 fields
756 .get(field)
757 .ok_or_else(|| EvalError::UnsupportedField {
758 field: field.to_owned(),
759 })
760 .map(Cow::Borrowed)
761}
762
763pub fn evaluate_set_value(
764 fields: &BTreeMap<String, JsonValue>,
765 set_value: &SetValue,
766) -> Result<JsonValue, EvalError> {
767 match set_value {
768 SetValue::Literal(value) => Ok(value_to_json(value)),
769 SetValue::Expr(expr) => eval_expr(fields, expr),
770 SetValue::Case {
771 when_clauses,
772 else_value,
773 } => {
774 for (condition, result) in when_clauses {
775 if eval_bool(fields, condition)? {
776 return eval_expr(fields, result);
777 }
778 }
779 Ok(else_value
780 .as_ref()
781 .map_or(Ok(JsonValue::Null), |expr| eval_expr(fields, expr))?)
782 }
783 SetValue::IfElse {
784 condition,
785 then_value,
786 else_value,
787 } => {
788 if eval_bool(fields, condition)? {
789 eval_expr(fields, then_value)
790 } else {
791 Ok(else_value
792 .as_ref()
793 .map_or(Ok(JsonValue::Null), |expr| eval_expr(fields, expr))?)
794 }
795 }
796 }
797}
798
799pub fn value_to_json(value: &Value) -> JsonValue {
800 match value {
801 Value::String(s) | Value::Identifier(s) => JsonValue::String(s.clone()),
802 Value::Integer(n) => JsonValue::Number(Number::from(*n)),
803 Value::Float(f) | Value::Percentage(f) => {
804 Number::from_f64(*f).map_or(JsonValue::Null, JsonValue::Number)
805 }
806 Value::Boolean(b) => JsonValue::Bool(*b),
807 }
808}
809
810#[must_use]
811pub fn json_eq(left: &JsonValue, right: &JsonValue) -> bool {
812 match (left, right) {
813 (JsonValue::String(a), JsonValue::String(b)) => a == b,
814 (JsonValue::Bool(a), JsonValue::Bool(b)) => a == b,
815 (JsonValue::Number(a), _) => a.as_f64() == json_as_f64(right),
816 (_, JsonValue::Number(b)) => json_as_f64(left) == b.as_f64(),
817 _ => left == right,
818 }
819}
820
821#[must_use]
823pub fn json_value_eq(actual: &JsonValue, expected: &Value) -> bool {
824 match (actual, expected) {
825 (JsonValue::String(actual), Value::String(expected) | Value::Identifier(expected)) => {
826 actual == expected
827 }
828 (JsonValue::Bool(actual), Value::Boolean(expected)) => actual == expected,
829 (JsonValue::Number(actual), expected) => actual.as_f64() == value_as_f64(expected),
830 _ => false,
831 }
832}
833
834#[must_use]
835pub fn json_contains_val(actual: &JsonValue, expected: &JsonValue) -> Option<bool> {
836 let expected_str = match expected {
837 JsonValue::String(s) => s.clone(),
838 other => other.to_string(),
839 };
840
841 match actual {
842 JsonValue::String(actual) => Some(actual.contains(&expected_str)),
843 JsonValue::Array(values) => Some(values.iter().any(|value| match value {
844 JsonValue::String(actual) => actual.contains(&expected_str),
845 other => other.to_string().contains(&expected_str),
846 })),
847 _ => None,
848 }
849}
850
851#[must_use]
853pub fn json_contains(actual: &JsonValue, expected: &Value) -> Option<bool> {
854 json_contains_val(actual, &value_to_json(expected))
855}
856
857#[must_use]
858pub fn compare_json_values(left: &JsonValue, right: &JsonValue) -> Ordering {
859 match (json_as_f64(left), json_as_f64(right)) {
860 (Some(left), Some(right)) => left.partial_cmp(&right).unwrap_or(Ordering::Equal),
861 _ => render_json_cell(left).cmp(&render_json_cell(right)),
862 }
863}
864
865fn parse_byte_size(s: &str) -> Option<f64> {
868 let s = s.trim();
869 if s.is_empty() {
870 return None;
871 }
872
873 let (num_str, unit) = {
876 let trimmed = s.trim_end();
877 let num_end = trimmed
879 .char_indices()
880 .take_while(|(_, c)| c.is_ascii_digit() || *c == '.')
881 .last()
882 .map_or(0, |(i, _)| i + 1);
883 if num_end == 0 {
884 return s.parse::<f64>().ok();
886 }
887 let num_part = &trimmed[..num_end];
888 let unit_part = trimmed[num_end..].trim();
889 (num_part, unit_part)
890 };
891
892 let number: f64 = num_str.parse().ok()?;
893
894 if unit.is_empty() {
895 return Some(number);
896 }
897
898 let multiplier = match unit.to_lowercase().as_str() {
899 "b" => 1.0,
900 "kb" | "k" => 1_000.0,
901 "mb" | "m" => 1_000_000.0,
902 "gb" | "g" => 1_000_000_000.0,
903 "tb" | "t" => 1_000_000_000_000.0,
904 "kib" | "ki" => 1024.0,
905 "mib" | "mi" => 1024.0 * 1024.0,
906 "gib" | "gi" => 1024.0 * 1024.0 * 1024.0,
907 "tib" | "ti" => 1024.0 * 1024.0 * 1024.0 * 1024.0,
908 _ => return None,
909 };
910
911 Some(number * multiplier)
912}
913
914#[must_use]
915pub fn json_as_f64(value: &JsonValue) -> Option<f64> {
916 match value {
917 JsonValue::Number(value) => value.as_f64(),
918 JsonValue::String(value) => {
919 if let Ok(n) = value.parse::<f64>() {
921 return Some(n);
922 }
923 parse_byte_size(value)
925 }
926 _ => None,
927 }
928}
929
930#[must_use]
931pub fn value_as_f64(value: &Value) -> Option<f64> {
932 match value {
933 Value::Integer(value) => Some(*value as f64),
934 Value::Float(value) | Value::Percentage(value) => Some(*value),
935 Value::String(value) | Value::Identifier(value) => value.parse().ok(),
936 Value::Boolean(_) => None,
937 }
938}
939
940#[must_use]
941pub fn value_as_text(value: &Value) -> Option<String> {
942 match value {
943 Value::String(value) | Value::Identifier(value) => Some(value.clone()),
944 Value::Integer(value) => Some(value.to_string()),
945 Value::Float(value) | Value::Percentage(value) => Some(value.to_string()),
946 Value::Boolean(value) => Some(value.to_string()),
947 }
948}
949
950pub fn render_json_cell(value: &JsonValue) -> String {
951 match value {
952 JsonValue::Null => String::new(),
953 JsonValue::String(value) => value.clone(),
954 JsonValue::Array(values) => values
955 .iter()
956 .map(render_json_cell)
957 .collect::<Vec<_>>()
958 .join(","),
959 other => other.to_string(),
960 }
961}
962
963#[cfg(test)]
964mod tests {
965 fn cmp_expr(field: &str, op: Operator, val: Value) -> Expression {
966 Expression::Comparison {
967 left: Box::new(Expression::Field(field.to_owned())),
968 operator: op,
969 right: Box::new(Expression::Literal(val)),
970 }
971 }
972
973 fn in_expr(field: &str, values: Vec<Value>) -> Expression {
974 Expression::In {
975 expr: Box::new(Expression::Field(field.to_owned())),
976 values,
977 }
978 }
979 use super::*;
980
981 fn fields(pairs: &[(&str, &str)]) -> BTreeMap<String, JsonValue> {
982 pairs
983 .iter()
984 .map(|(k, v)| (k.to_string(), JsonValue::String(v.to_string())))
985 .collect()
986 }
987
988 #[test]
989 fn evaluates_string_equality() {
990 let f = fields(&[("status", "running")]);
991 let expr = cmp_expr("status", Operator::Eq, Value::String("running".into()));
992 assert!(evaluate_expression(&f, &expr).unwrap());
993 }
994
995 #[test]
996 fn evaluates_numeric_comparison() {
997 let mut f = BTreeMap::new();
998 f.insert("cpu".into(), serde_json::json!(87.5));
999 let expr = cmp_expr("cpu", Operator::Gt, Value::Percentage(80.0));
1000 assert!(evaluate_expression(&f, &expr).unwrap());
1001 }
1002
1003 #[test]
1004 fn evaluates_and_or_not() {
1005 let f = fields(&[("a", "1"), ("b", "2")]);
1006 let left = cmp_expr("a", Operator::Eq, Value::String("1".into()));
1007 let right = cmp_expr("b", Operator::Eq, Value::String("9".into()));
1008 assert!(
1009 evaluate_expression(
1010 &f,
1011 &Expression::Or(Box::new(left.clone()), Box::new(right.clone()))
1012 )
1013 .unwrap()
1014 );
1015 assert!(
1016 !evaluate_expression(&f, &Expression::And(Box::new(left), Box::new(right))).unwrap()
1017 );
1018 }
1019
1020 #[test]
1021 fn evaluates_set_case() {
1022 let mut f = BTreeMap::new();
1023 f.insert("cpu".into(), serde_json::json!(87.5));
1024 let sv = SetValue::Case {
1025 when_clauses: vec![
1026 (
1027 cmp_expr("cpu", Operator::Gt, Value::Percentage(80.0)),
1028 Expression::Literal(Value::String("critical".into())),
1029 ),
1030 (
1031 cmp_expr("cpu", Operator::Gt, Value::Percentage(50.0)),
1032 Expression::Literal(Value::String("warning".into())),
1033 ),
1034 ],
1035 else_value: Some(Expression::Literal(Value::String("ok".into()))),
1036 };
1037 let result = evaluate_set_value(&f, &sv).unwrap();
1038 assert_eq!(result, JsonValue::String("critical".to_string()));
1039 }
1040
1041 #[test]
1042 fn evaluates_set_case_else() {
1043 let mut f = BTreeMap::new();
1044 f.insert("cpu".into(), serde_json::json!(30.0));
1045 let sv = SetValue::Case {
1046 when_clauses: vec![(
1047 cmp_expr("cpu", Operator::Gt, Value::Percentage(80.0)),
1048 Expression::Literal(Value::String("critical".into())),
1049 )],
1050 else_value: Some(Expression::Literal(Value::String("ok".into()))),
1051 };
1052 let result = evaluate_set_value(&f, &sv).unwrap();
1053 assert_eq!(result, JsonValue::String("ok".to_string()));
1054 }
1055
1056 #[test]
1057 fn evaluates_set_if_else() {
1058 let f = fields(&[("state", "running")]);
1059 let sv = SetValue::IfElse {
1060 condition: cmp_expr("state", Operator::Eq, Value::Identifier("running".into())),
1061 then_value: Expression::Literal(Value::String("up".into())),
1062 else_value: Some(Expression::Literal(Value::String("down".into()))),
1063 };
1064 let result = evaluate_set_value(&f, &sv).unwrap();
1065 assert_eq!(result, JsonValue::String("up".to_string()));
1066 }
1067
1068 #[test]
1069 fn evaluates_matches_regex() {
1070 let f = fields(&[("name", "api-service-v2")]);
1071 let expr = cmp_expr(
1072 "name",
1073 Operator::Matches,
1074 Value::String("^api-.*\\d+$".into()),
1075 );
1076 assert!(evaluate_expression(&f, &expr).unwrap());
1077 }
1078
1079 #[test]
1080 fn evaluates_matches_regex_no_match() {
1081 let f = fields(&[("name", "api-service")]);
1082 let expr = cmp_expr(
1083 "name",
1084 Operator::Matches,
1085 Value::String("^api-.*\\d+$".into()),
1086 );
1087 assert!(!evaluate_expression(&f, &expr).unwrap());
1088 }
1089
1090 #[test]
1091 fn evaluates_in_operator() {
1092 let f = fields(&[("state", "running")]);
1093 let expr = in_expr(
1094 "state",
1095 vec![
1096 Value::String("running".into()),
1097 Value::String("restarting".into()),
1098 ],
1099 );
1100 assert!(evaluate_expression(&f, &expr).unwrap());
1101 }
1102
1103 #[test]
1104 fn evaluates_in_operator_no_match() {
1105 let f = fields(&[("state", "exited")]);
1106 let expr = in_expr(
1107 "state",
1108 vec![
1109 Value::String("running".into()),
1110 Value::String("restarting".into()),
1111 ],
1112 );
1113 assert!(!evaluate_expression(&f, &expr).unwrap());
1114 }
1115
1116 #[test]
1117 fn evaluates_label_field_access() {
1118 let mut f = BTreeMap::new();
1119 f.insert("labels".into(), serde_json::json!(["env=prod", "tier=api"]));
1120 let expr = cmp_expr("label.env", Operator::Eq, Value::String("prod".into()));
1121 assert!(evaluate_expression(&f, &expr).unwrap());
1122 }
1123
1124 #[test]
1125 fn evaluates_label_field_no_match() {
1126 let mut f = BTreeMap::new();
1127 f.insert("labels".into(), serde_json::json!(["env=staging"]));
1128 let expr = cmp_expr("label.env", Operator::Eq, Value::String("prod".into()));
1129 assert!(!evaluate_expression(&f, &expr).unwrap());
1130 }
1131
1132 #[test]
1135 fn evaluates_arithmetic() {
1136 let mut f = BTreeMap::new();
1137 f.insert("cpu".into(), serde_json::json!(80.0));
1138 f.insert("mem".into(), serde_json::json!(200.0));
1139
1140 let sum = Expression::Arithmetic {
1142 left: Box::new(Expression::Field("cpu".to_owned())),
1143 op: BinOp::Add,
1144 right: Box::new(Expression::Field("mem".to_owned())),
1145 };
1146 let cmp = Expression::Comparison {
1147 left: Box::new(sum),
1148 operator: Operator::Gt,
1149 right: Box::new(Expression::Literal(Value::Float(250.0))),
1150 };
1151 assert!(eval_bool(&f, &cmp).unwrap());
1152
1153 let mul = Expression::Arithmetic {
1155 left: Box::new(Expression::Field("cpu".to_owned())),
1156 op: BinOp::Mul,
1157 right: Box::new(Expression::Literal(Value::Float(2.0))),
1158 };
1159 let eq = Expression::Comparison {
1160 left: Box::new(mul),
1161 operator: Operator::Eq,
1162 right: Box::new(Expression::Literal(Value::Float(160.0))),
1163 };
1164 assert!(eval_bool(&f, &eq).unwrap());
1165 }
1166
1167 #[test]
1168 fn evaluates_between() {
1169 let mut f = BTreeMap::new();
1170 f.insert("cpu".into(), serde_json::json!(65.0));
1171 let expr = Expression::Between {
1172 expr: Box::new(Expression::Field("cpu".to_owned())),
1173 low: Box::new(Expression::Literal(Value::Float(50.0))),
1174 high: Box::new(Expression::Literal(Value::Float(80.0))),
1175 };
1176 assert!(eval_bool(&f, &expr).unwrap());
1177
1178 f.insert("cpu".into(), serde_json::json!(95.0));
1179 assert!(!eval_bool(&f, &expr).unwrap());
1180 }
1181
1182 #[test]
1183 fn evaluates_is_null() {
1184 let mut f = BTreeMap::new();
1185 f.insert("name".into(), JsonValue::String("web".into()));
1186 let expr = Expression::IsNull(Box::new(Expression::Field("name".to_owned())));
1187 assert!(!eval_bool(&f, &expr).unwrap());
1188 assert!(eval_bool(&BTreeMap::new(), &expr).unwrap());
1189 }
1190
1191 #[test]
1192 fn evaluates_is_not_null() {
1193 let mut f = BTreeMap::new();
1194 f.insert("name".into(), JsonValue::String("web".into()));
1195 let expr = Expression::IsNotNull(Box::new(Expression::Field("name".to_owned())));
1196 assert!(eval_bool(&f, &expr).unwrap());
1197 assert!(!eval_bool(&BTreeMap::new(), &expr).unwrap());
1198 }
1199
1200 #[test]
1201 fn evaluates_function_calls() {
1202 let f = fields(&[("name", "Api-Service")]);
1203 let upper = Expression::FnCall {
1204 name: "upper".to_owned(),
1205 args: vec![Expression::Field("name".to_owned())],
1206 };
1207 assert_eq!(
1208 eval_expr(&f, &upper).unwrap(),
1209 JsonValue::String("API-SERVICE".to_owned())
1210 );
1211
1212 let lower = Expression::FnCall {
1213 name: "lower".to_owned(),
1214 args: vec![Expression::Field("name".to_owned())],
1215 };
1216 assert_eq!(
1217 eval_expr(&f, &lower).unwrap(),
1218 JsonValue::String("api-service".to_owned())
1219 );
1220
1221 let len = Expression::FnCall {
1222 name: "length".to_owned(),
1223 args: vec![Expression::Field("name".to_owned())],
1224 };
1225 assert_eq!(
1226 eval_expr(&f, &len).unwrap(),
1227 JsonValue::Number(Number::from(11))
1228 );
1229
1230 let concat = Expression::FnCall {
1231 name: "concat".to_owned(),
1232 args: vec![
1233 Expression::Field("name".to_owned()),
1234 Expression::Literal(Value::String(":v1".to_owned())),
1235 ],
1236 };
1237 assert_eq!(
1238 eval_expr(&f, &concat).unwrap(),
1239 JsonValue::String("Api-Service:v1".to_owned())
1240 );
1241 }
1242
1243 #[test]
1244 fn evaluates_set_expr() {
1245 let mut f = BTreeMap::new();
1246 f.insert("memory".into(), serde_json::json!(1073741824u64));
1247 let sv = SetValue::Expr(Expression::Arithmetic {
1248 left: Box::new(Expression::Field("memory".to_owned())),
1249 op: BinOp::Div,
1250 right: Box::new(Expression::Literal(Value::Float(1073741824.0))),
1251 });
1252 let result = evaluate_set_value(&f, &sv).unwrap();
1253 assert_eq!(result, JsonValue::Number(Number::from_f64(1.0).unwrap()));
1254 }
1255
1256 #[test]
1257 fn multiplies_arithmetic() {
1258 let mut f = BTreeMap::new();
1259 f.insert("cpu".into(), serde_json::json!(50.0));
1260 let add = Expression::Arithmetic {
1262 left: Box::new(Expression::Field("cpu".to_owned())),
1263 op: BinOp::Add,
1264 right: Box::new(Expression::Literal(Value::Float(10.0))),
1265 };
1266 let mul = Expression::Arithmetic {
1267 left: Box::new(add),
1268 op: BinOp::Mul,
1269 right: Box::new(Expression::Literal(Value::Float(2.0))),
1270 };
1271 let result = eval_expr(&f, &mul).unwrap();
1272 assert_eq!(result.as_f64(), Some(120.0));
1273 }
1274
1275 #[test]
1276 fn evaluates_starts_with() {
1277 let f = fields(&[("name", "api-service")]);
1278 let expr = Expression::FnCall {
1279 name: "starts_with".to_owned(),
1280 args: vec![
1281 Expression::Field("name".to_owned()),
1282 Expression::Literal(Value::String("api".to_owned())),
1283 ],
1284 };
1285 assert_eq!(eval_expr(&f, &expr).unwrap(), JsonValue::Bool(true));
1286
1287 let expr_no = Expression::FnCall {
1288 name: "starts_with".to_owned(),
1289 args: vec![
1290 Expression::Field("name".to_owned()),
1291 Expression::Literal(Value::String("web".to_owned())),
1292 ],
1293 };
1294 assert_eq!(eval_expr(&f, &expr_no).unwrap(), JsonValue::Bool(false));
1295 }
1296
1297 #[test]
1298 fn evaluates_ends_with() {
1299 let f = fields(&[("name", "api-service")]);
1300 let expr = Expression::FnCall {
1301 name: "ends_with".to_owned(),
1302 args: vec![
1303 Expression::Field("name".to_owned()),
1304 Expression::Literal(Value::String("service".to_owned())),
1305 ],
1306 };
1307 assert_eq!(eval_expr(&f, &expr).unwrap(), JsonValue::Bool(true));
1308 }
1309
1310 #[test]
1311 fn evaluates_replace() {
1312 let f = fields(&[("name", "api-service-v2")]);
1313 let expr = Expression::FnCall {
1314 name: "replace".to_owned(),
1315 args: vec![
1316 Expression::Field("name".to_owned()),
1317 Expression::Literal(Value::String("service".to_owned())),
1318 Expression::Literal(Value::String("app".to_owned())),
1319 ],
1320 };
1321 assert_eq!(
1322 eval_expr(&f, &expr).unwrap(),
1323 JsonValue::String("api-app-v2".to_owned())
1324 );
1325 }
1326
1327 #[test]
1328 fn evaluates_reverse() {
1329 let f = fields(&[("name", "abc")]);
1330 let expr = Expression::FnCall {
1331 name: "reverse".to_owned(),
1332 args: vec![Expression::Field("name".to_owned())],
1333 };
1334 assert_eq!(
1335 eval_expr(&f, &expr).unwrap(),
1336 JsonValue::String("cba".to_owned())
1337 );
1338 }
1339
1340 #[test]
1341 fn evaluates_repeat() {
1342 let f = fields(&[("name", "ab")]);
1343 let expr = Expression::FnCall {
1344 name: "repeat".to_owned(),
1345 args: vec![
1346 Expression::Field("name".to_owned()),
1347 Expression::Literal(Value::Integer(3)),
1348 ],
1349 };
1350 assert_eq!(
1351 eval_expr(&f, &expr).unwrap(),
1352 JsonValue::String("ababab".to_owned())
1353 );
1354 }
1355
1356 #[test]
1357 fn evaluates_position() {
1358 let f = fields(&[("name", "hello world")]);
1359 let expr = Expression::FnCall {
1360 name: "position".to_owned(),
1361 args: vec![
1362 Expression::Field("name".to_owned()),
1363 Expression::Literal(Value::String("world".to_owned())),
1364 ],
1365 };
1366 assert_eq!(
1367 eval_expr(&f, &expr).unwrap(),
1368 JsonValue::Number(Number::from(6))
1369 );
1370 }
1371
1372 #[test]
1373 fn evaluates_position_not_found() {
1374 let f = fields(&[("name", "hello")]);
1375 let expr = Expression::FnCall {
1376 name: "position".to_owned(),
1377 args: vec![
1378 Expression::Field("name".to_owned()),
1379 Expression::Literal(Value::String("xyz".to_owned())),
1380 ],
1381 };
1382 assert_eq!(
1383 eval_expr(&f, &expr).unwrap(),
1384 JsonValue::Number(Number::from(0))
1385 );
1386 }
1387
1388 #[test]
1389 fn evaluates_split_part() {
1390 let f = fields(&[("name", "a,b,c,d")]);
1391 let expr = Expression::FnCall {
1392 name: "split_part".to_owned(),
1393 args: vec![
1394 Expression::Field("name".to_owned()),
1395 Expression::Literal(Value::String(",".to_owned())),
1396 Expression::Literal(Value::Integer(2)),
1397 ],
1398 };
1399 assert_eq!(
1400 eval_expr(&f, &expr).unwrap(),
1401 JsonValue::String("b".to_owned())
1402 );
1403 }
1404
1405 #[test]
1406 fn evaluates_now() {
1407 let expr = Expression::FnCall {
1408 name: "now".to_owned(),
1409 args: vec![],
1410 };
1411 let result = eval_expr(&BTreeMap::new(), &expr).unwrap();
1412 assert!(result.is_string());
1413 let s = result.as_str().unwrap();
1414 assert!(s.len() > 10, "now() should return an ISO timestamp");
1415 }
1416
1417 #[test]
1418 fn evaluates_date_format() {
1419 let f = fields(&[("ts", "2026-06-01T12:30:00Z")]);
1420 let expr = Expression::FnCall {
1421 name: "date_format".to_owned(),
1422 args: vec![
1423 Expression::Field("ts".to_owned()),
1424 Expression::Literal(Value::String("%Y-%m-%d".to_owned())),
1425 ],
1426 };
1427 assert_eq!(
1428 eval_expr(&f, &expr).unwrap(),
1429 JsonValue::String("2026-06-01".to_owned())
1430 );
1431 }
1432
1433 #[test]
1434 fn evaluates_date_diff_days() {
1435 let f = fields(&[
1436 ("start", "2026-01-01T00:00:00Z"),
1437 ("end", "2026-01-11T00:00:00Z"),
1438 ]);
1439 let expr = Expression::FnCall {
1440 name: "date_diff".to_owned(),
1441 args: vec![
1442 Expression::Field("start".to_owned()),
1443 Expression::Field("end".to_owned()),
1444 Expression::Literal(Value::String("days".to_owned())),
1445 ],
1446 };
1447 assert_eq!(
1448 eval_expr(&f, &expr).unwrap(),
1449 JsonValue::Number(Number::from(10))
1450 );
1451 }
1452
1453 #[test]
1454 fn evaluates_extract_year() {
1455 let f = fields(&[("ts", "2026-06-15T08:30:00Z")]);
1456 let expr = Expression::FnCall {
1457 name: "extract".to_owned(),
1458 args: vec![
1459 Expression::Field("ts".to_owned()),
1460 Expression::Literal(Value::String("year".to_owned())),
1461 ],
1462 };
1463 assert_eq!(
1464 eval_expr(&f, &expr).unwrap(),
1465 JsonValue::Number(Number::from(2026))
1466 );
1467 }
1468
1469 #[test]
1470 fn handles_division_by_zero() {
1471 let mut f = BTreeMap::new();
1472 f.insert("x".into(), serde_json::json!(10.0));
1473 let div = Expression::Arithmetic {
1474 left: Box::new(Expression::Field("x".to_owned())),
1475 op: BinOp::Div,
1476 right: Box::new(Expression::Literal(Value::Float(0.0))),
1477 };
1478 assert!(eval_expr(&f, &div).is_err());
1479 }
1480 #[test]
1481 fn evaluates_to_int_from_string() {
1482 let f = fields(&[("val", "42")]);
1483 let expr = Expression::FnCall {
1484 name: "to_int".to_owned(),
1485 args: vec![Expression::Field("val".to_owned())],
1486 };
1487 assert_eq!(
1488 eval_expr(&f, &expr).unwrap(),
1489 JsonValue::Number(Number::from(42))
1490 );
1491 }
1492
1493 #[test]
1494 fn evaluates_to_int_from_float_string() {
1495 let f = fields(&[("val", "42.7")]);
1496 let expr = Expression::FnCall {
1497 name: "to_int".to_owned(),
1498 args: vec![Expression::Field("val".to_owned())],
1499 };
1500 assert_eq!(
1501 eval_expr(&f, &expr).unwrap(),
1502 JsonValue::Number(Number::from(42))
1503 );
1504 }
1505
1506 #[test]
1507 fn evaluates_to_int_from_bool_true() {
1508 let mut f = std::collections::BTreeMap::new();
1509 f.insert("val".into(), JsonValue::Bool(true));
1510 let expr = Expression::FnCall {
1511 name: "to_int".to_owned(),
1512 args: vec![Expression::Field("val".to_owned())],
1513 };
1514 assert_eq!(
1515 eval_expr(&f, &expr).unwrap(),
1516 JsonValue::Number(Number::from(1))
1517 );
1518 }
1519
1520 #[test]
1521 fn evaluates_to_float_from_integer() {
1522 let mut f = std::collections::BTreeMap::new();
1523 f.insert("val".into(), JsonValue::Number(Number::from(42)));
1524 let expr = Expression::FnCall {
1525 name: "to_float".to_owned(),
1526 args: vec![Expression::Field("val".to_owned())],
1527 };
1528 let result = eval_expr(&f, &expr).unwrap();
1529 assert_eq!(result.as_f64(), Some(42.0));
1530 }
1531
1532 #[test]
1533 #[allow(clippy::approx_constant)]
1534 fn evaluates_to_float_from_string() {
1535 let f = fields(&[("val", "3.14")]);
1536 let expr = Expression::FnCall {
1537 name: "to_float".to_owned(),
1538 args: vec![Expression::Field("val".to_owned())],
1539 };
1540 let result = eval_expr(&f, &expr).unwrap();
1541 assert_eq!(result.as_f64(), Some(3.14));
1542 }
1543
1544 #[test]
1545 fn evaluates_to_string_from_number() {
1546 let mut f = std::collections::BTreeMap::new();
1547 f.insert("val".into(), JsonValue::Number(Number::from(42)));
1548 let expr = Expression::FnCall {
1549 name: "to_string".to_owned(),
1550 args: vec![Expression::Field("val".to_owned())],
1551 };
1552 assert_eq!(
1553 eval_expr(&f, &expr).unwrap(),
1554 JsonValue::String("42".to_owned())
1555 );
1556 }
1557
1558 #[test]
1559 fn evaluates_to_string_from_bool() {
1560 let mut f = std::collections::BTreeMap::new();
1561 f.insert("val".into(), JsonValue::Bool(true));
1562 let expr = Expression::FnCall {
1563 name: "to_string".to_owned(),
1564 args: vec![Expression::Field("val".to_owned())],
1565 };
1566 assert_eq!(
1567 eval_expr(&f, &expr).unwrap(),
1568 JsonValue::String("true".to_owned())
1569 );
1570 }
1571}