Skip to main content

dol/
eval.rs

1//! Expression evaluation engine.
2//!
3//! Evaluates DOL [`Expression`] nodes against JSON-like field maps.
4//! Supports arithmetic, comparison, boolean logic, function calls
5//! (`upper`, `lower`, `date_format`, `coalesce`, etc.), and `CASE`/`IF`
6//! expressions.
7//!
8//! # Example
9//!
10//! ```ignore
11//! let fields = BTreeMap::from([("cpu", json!(87.5))]);
12//! let expr = Expression::Comparison { left: ..., operator: Gt, right: ... };
13//! assert!(eval::evaluate_expression(&fields, &expr)?);
14//! ```
15
16use 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    /// Return a fully ANSI-coloured error string suitable for terminal display.
30    #[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
115/// Evaluate any expression to a JSON value.
116pub 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
214/// Like `eval_expr` but returns None instead of erroring on missing fields.
215fn 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            // Also try label.xxx access
222            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
244/// Evaluate an expression as a boolean (for `where`, `if` contexts).
245pub 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
253/// Legacy wrapper — kept for backward compatibility.
254pub 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    // Try ISO 8601 format first, then common formats
686    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// Kept for backward compat with the few external callers
822#[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// Keep old signature for backward compat
852#[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
865/// Parse a human-readable byte size string (e.g. "93.6MB", "28.7GB", "155MB",
866/// "13.1MB", "1024", "1.5KiB") into bytes as f64.
867fn parse_byte_size(s: &str) -> Option<f64> {
868    let s = s.trim();
869    if s.is_empty() {
870        return None;
871    }
872
873    // Split number from unit suffix.
874    // The number can be an integer or decimal.
875    let (num_str, unit) = {
876        let trimmed = s.trim_end();
877        // Find where the numeric part ends
878        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            // No leading digits — try direct parse
885            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            // Try direct parse first (fast path for plain numbers)
920            if let Ok(n) = value.parse::<f64>() {
921                return Some(n);
922            }
923            // Fall back to byte-size parsing for human-readable sizes
924            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    // ── New Tier 1 tests ──
1133
1134    #[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        // cpu + mem > 250
1141        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        // cpu * 2 = 160
1154        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        // (cpu + 10) * 2
1261        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}