Skip to main content

webui_expressions/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! WebUI expression evaluation module.
5//!
6//! This module handles the evaluation of condition expressions in WebUI templates.
7
8use std::borrow::Cow;
9
10use serde_json::Value;
11use thiserror::Error;
12use webui_protocol::{
13    condition_expr, ComparisonOperator, CompoundCondition, ConditionExpr, LogicalOperator,
14    Predicate,
15};
16use webui_state::find_value_by_dotted_path_ref;
17
18/// Error types for expression evaluation.
19#[derive(Debug, Error)]
20pub enum ExpressionError {
21    #[error("Evaluation error: {0}")]
22    Evaluation(String),
23
24    #[error("Missing value: {0}")]
25    MissingValue(String),
26
27    #[error("Type error: {0}")]
28    TypeError(String),
29
30    #[error("Mixed logical operators: Cannot mix AND and OR operators")]
31    MixedOperators,
32
33    #[error("Too many logical operators: Maximum of 5 allowed, found {0}")]
34    TooManyOperators(usize),
35
36    #[error("Value comparison error: {0}")]
37    Comparison(String),
38}
39
40pub type Result<T> = std::result::Result<T, ExpressionError>;
41
42/// Evaluate a condition expression with the given state.
43///
44/// Missing identifier paths are falsy operands. Missing values used by
45/// comparison predicates remain evaluation errors.
46pub fn evaluate(condition: &ConditionExpr, state: &Value) -> Result<bool> {
47    evaluate_with_resolver(condition, |path| find_value_by_dotted_path_ref(path, state))
48}
49
50/// Evaluate a condition expression using a custom resolver for value lookups.
51///
52/// The `resolver` closure takes a dotted path (e.g., `"contact.name"`) and
53/// returns the resolved value. This allows callers to provide merged views
54/// (e.g., local variables overlaid on global state) without cloning the
55/// entire state tree.
56pub fn evaluate_with_resolver<'a, F>(condition: &ConditionExpr, resolver: F) -> Result<bool>
57where
58    F: Fn(&str) -> Option<Cow<'a, Value>>,
59{
60    // Single-term conditions carry no logical operators, so the traversal in
61    // `count_logical_operators` is pure overhead for the most common template
62    // conditions.
63    if matches!(
64        condition.expr,
65        Some(condition_expr::Expr::Identifier(_)) | Some(condition_expr::Expr::Predicate(_))
66    ) {
67        return evaluate_expr(condition, &resolver);
68    }
69
70    let (logical_op_count, has_mixed_ops) = count_logical_operators(condition);
71
72    if logical_op_count > 5 {
73        return Err(ExpressionError::TooManyOperators(logical_op_count));
74    }
75
76    if has_mixed_ops {
77        return Err(ExpressionError::MixedOperators);
78    }
79
80    evaluate_expr(condition, &resolver)
81}
82
83/// Inline capacity of [`PendingExprs`].
84///
85/// The depth-first walk keeps at most one pending node per open compound plus
86/// the node being visited, so any condition within the five-operator limit
87/// fits inline and only trees that will be rejected anyway reach the heap.
88const INLINE_PENDING_EXPRS: usize = 8;
89
90/// LIFO stack of sub-expressions still to visit, kept on the call stack.
91///
92/// Compound conditions are evaluated on every render of every `<if>` and
93/// boolean attribute, so the operator check must not allocate per call.
94struct PendingExprs<'a> {
95    inline: [Option<&'a ConditionExpr>; INLINE_PENDING_EXPRS],
96    inline_len: usize,
97    overflow: Vec<&'a ConditionExpr>,
98}
99
100impl<'a> PendingExprs<'a> {
101    fn new(root: &'a ConditionExpr) -> Self {
102        let mut inline = [None; INLINE_PENDING_EXPRS];
103        inline[0] = Some(root);
104        Self {
105            inline,
106            inline_len: 1,
107            overflow: Vec::new(),
108        }
109    }
110
111    fn push(&mut self, expr: &'a ConditionExpr) {
112        if self.inline_len < INLINE_PENDING_EXPRS {
113            self.inline[self.inline_len] = Some(expr);
114            self.inline_len += 1;
115        } else {
116            self.overflow.push(expr);
117        }
118    }
119
120    fn pop(&mut self) -> Option<&'a ConditionExpr> {
121        if let Some(expr) = self.overflow.pop() {
122            return Some(expr);
123        }
124        if self.inline_len == 0 {
125            return None;
126        }
127        self.inline_len -= 1;
128        self.inline[self.inline_len].take()
129    }
130}
131
132// Helper function to count logical operators and check if they're mixed
133fn count_logical_operators(condition: &ConditionExpr) -> (usize, bool) {
134    let mut count = 0;
135    let mut last_op: Option<i32> = None;
136    let mut has_mixed = false;
137
138    // We need to use a stack to avoid recursion
139    let mut stack = PendingExprs::new(condition);
140
141    while let Some(expr) = stack.pop() {
142        match &expr.expr {
143            Some(condition_expr::Expr::Compound(compound)) => {
144                count += 1;
145
146                // Check if we're mixing operators
147                if let Some(last) = last_op {
148                    if last != compound.op {
149                        has_mixed = true;
150                    }
151                } else {
152                    last_op = Some(compound.op);
153                }
154
155                // Push sub-expressions to stack
156                if let Some(right) = compound.right.as_ref() {
157                    stack.push(right);
158                }
159                if let Some(left) = compound.left.as_ref() {
160                    stack.push(left);
161                }
162            }
163            Some(condition_expr::Expr::Not(not_cond)) => {
164                if let Some(inner) = not_cond.condition.as_ref() {
165                    stack.push(inner);
166                }
167            }
168            _ => {} // Predicates and identifiers don't have logical operators
169        }
170    }
171
172    (count, has_mixed)
173}
174
175// Iterative evaluation of expressions using a resolver closure
176fn evaluate_expr<'a, F>(condition: &ConditionExpr, resolver: &F) -> Result<bool>
177where
178    F: Fn(&str) -> Option<Cow<'a, Value>>,
179{
180    match &condition.expr {
181        Some(condition_expr::Expr::Predicate(pred)) => evaluate_predicate(pred, resolver),
182        Some(condition_expr::Expr::Not(not_cond)) => {
183            let inner = not_cond.condition.as_ref().ok_or_else(|| {
184                ExpressionError::Evaluation("Not condition missing inner expression".to_string())
185            })?;
186            let result = evaluate_expr(inner, resolver)?;
187            Ok(!result)
188        }
189        Some(condition_expr::Expr::Compound(compound)) => evaluate_compound(compound, resolver),
190        Some(condition_expr::Expr::Identifier(id)) if id.value == "true" => Ok(true),
191        Some(condition_expr::Expr::Identifier(id)) if id.value == "false" => Ok(false),
192        Some(condition_expr::Expr::Identifier(id)) => {
193            if let Some(val) = resolver(&id.value) {
194                match val.as_ref() {
195                    Value::Bool(b) => Ok(*b),
196                    Value::Null => Ok(false),
197                    Value::Number(n) => Ok(!(n.as_f64() == Some(0.0))),
198                    Value::String(s) => Ok(!s.is_empty()),
199                    Value::Array(a) => Ok(!a.is_empty()),
200                    Value::Object(o) => Ok(!o.is_empty()),
201                }
202            } else {
203                Ok(false)
204            }
205        }
206        None => Err(ExpressionError::Evaluation(
207            "Empty condition expression".to_string(),
208        )),
209    }
210}
211
212fn evaluate_compound<'a, F>(compound: &CompoundCondition, resolver: &F) -> Result<bool>
213where
214    F: Fn(&str) -> Option<Cow<'a, Value>>,
215{
216    let left = compound.left.as_ref().ok_or_else(|| {
217        ExpressionError::Evaluation("Compound missing left expression".to_string())
218    })?;
219    let right = compound.right.as_ref().ok_or_else(|| {
220        ExpressionError::Evaluation("Compound missing right expression".to_string())
221    })?;
222
223    let left_result = evaluate_expr(left, resolver)?;
224    let op = LogicalOperator::try_from(compound.op).map_err(|_| {
225        ExpressionError::Evaluation(format!("Invalid logical operator: {}", compound.op))
226    })?;
227
228    match op {
229        LogicalOperator::And => {
230            if !left_result {
231                return Ok(false);
232            }
233            evaluate_expr(right, resolver)
234        }
235        LogicalOperator::Or => {
236            if left_result {
237                return Ok(true);
238            }
239            evaluate_expr(right, resolver)
240        }
241        LogicalOperator::Unspecified => Err(ExpressionError::Evaluation(
242            "Unspecified logical operator".to_string(),
243        )),
244    }
245}
246
247fn evaluate_predicate<'a, F>(predicate: &Predicate, resolver: &F) -> Result<bool>
248where
249    F: Fn(&str) -> Option<Cow<'a, Value>>,
250{
251    let left_val = match resolver(&predicate.left) {
252        Some(val) => val,
253        None => return Err(ExpressionError::MissingValue(predicate.left.clone())),
254    };
255
256    let right_val = if is_literal(&predicate.right) {
257        parse_literal(&predicate.right)?
258    } else {
259        match resolver(&predicate.right) {
260            Some(val) => Operand::Value(val),
261            None => return Err(ExpressionError::MissingValue(predicate.right.clone())),
262        }
263    };
264
265    let op = ComparisonOperator::try_from(predicate.operator).map_err(|_| {
266        ExpressionError::Evaluation(format!(
267            "Invalid comparison operator: {}",
268            predicate.operator
269        ))
270    })?;
271
272    compare_values(left_val.as_ref(), &op, &right_val)
273}
274
275enum Operand<'a> {
276    Value(Cow<'a, Value>),
277    // Quoted literals already live in the condition, so comparison needs no
278    // temporary Value::String or retained copy of the protocol's text.
279    String(&'a str),
280}
281
282impl Operand<'_> {
283    fn equals(&self, left: &Value) -> bool {
284        match self {
285            Self::Value(right) => left == right.as_ref(),
286            Self::String(right) => left.as_str() == Some(*right),
287        }
288    }
289
290    fn number(&self) -> Result<f64> {
291        match self {
292            Self::Value(value) => extract_number(value),
293            Self::String(value) => parse_string_number(value),
294        }
295    }
296}
297
298// Check if a string is a literal value
299fn is_literal(s: &str) -> bool {
300    // A string is a literal if:
301    // - It starts with a quote (single or double)
302    // - It's a number (starts with a digit or negative sign followed by a digit)
303    // - It's a boolean ("true" or "false")
304    s.starts_with('"')
305        || s.starts_with('\'')
306        || s.starts_with(|c: char| {
307            c.is_ascii_digit()
308                || (c == '-' && s.len() > 1 && s.chars().nth(1).is_some_and(|c| c.is_ascii_digit()))
309        })
310        || s == "true"
311        || s == "false"
312}
313
314// Quoted literals borrow their text; scalar literals retain JSON value semantics.
315fn parse_literal(s: &str) -> Result<Operand<'_>> {
316    // Handle quoted strings
317    if s.len() >= 2
318        && ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')))
319    {
320        return Ok(Operand::String(&s[1..s.len() - 1]));
321    }
322
323    // Handle booleans
324    if s == "true" {
325        return Ok(Operand::Value(Cow::Owned(Value::Bool(true))));
326    }
327    if s == "false" {
328        return Ok(Operand::Value(Cow::Owned(Value::Bool(false))));
329    }
330
331    // Handle numbers
332    if let Ok(num) = s.parse::<i64>() {
333        return Ok(Operand::Value(Cow::Owned(Value::Number(num.into()))));
334    }
335
336    if let Ok(num) = s.parse::<f64>() {
337        // Create a JSON number from f64, handling error if it's not representable
338        match serde_json::Number::from_f64(num) {
339            Some(n) => return Ok(Operand::Value(Cow::Owned(Value::Number(n)))),
340            None => return Err(unrepresentable_literal_error(s)),
341        }
342    }
343
344    // If we get here, it's not a recognized literal
345    Err(invalid_literal_error(s))
346}
347
348#[cold]
349#[inline(never)]
350fn unrepresentable_literal_error(s: &str) -> ExpressionError {
351    ExpressionError::TypeError(format!("Cannot convert {s} to JSON number"))
352}
353
354#[cold]
355#[inline(never)]
356fn invalid_literal_error(s: &str) -> ExpressionError {
357    ExpressionError::TypeError(format!("Invalid literal: {s}"))
358}
359
360// Compare a JSON value with a resolved operand.
361fn compare_values(left: &Value, op: &ComparisonOperator, right: &Operand<'_>) -> Result<bool> {
362    match op {
363        ComparisonOperator::Equal => Ok(right.equals(left)),
364        ComparisonOperator::NotEqual => Ok(!right.equals(left)),
365
366        // Handle numeric comparisons
367        ComparisonOperator::GreaterThan => compare_ordered(left, right, |a, b| a > b),
368        ComparisonOperator::LessThan => compare_ordered(left, right, |a, b| a < b),
369        ComparisonOperator::GreaterThanOrEqual => compare_ordered(left, right, |a, b| a >= b),
370        ComparisonOperator::LessThanOrEqual => compare_ordered(left, right, |a, b| a <= b),
371        ComparisonOperator::Unspecified => Err(ExpressionError::Evaluation(
372            "Unspecified comparison operator".to_string(),
373        )),
374    }
375}
376
377// Helper for ordered comparisons
378fn compare_ordered<F>(left: &Value, right: &Operand<'_>, compare_fn: F) -> Result<bool>
379where
380    F: Fn(&f64, &f64) -> bool,
381{
382    // Extract numeric values
383    let left_num = extract_number(left)?;
384    let right_num = right.number()?;
385
386    Ok(compare_fn(&left_num, &right_num))
387}
388
389// Extract a numeric value from a JSON value
390fn extract_number(val: &Value) -> Result<f64> {
391    match val {
392        Value::Number(n) => {
393            if let Some(f) = n.as_f64() {
394                Ok(f)
395            } else {
396                Err(ExpressionError::TypeError(format!(
397                    "Cannot convert number to f64: {:?}",
398                    n
399                )))
400            }
401        }
402        Value::String(s) => parse_string_number(s),
403        Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
404        _ => Err(ExpressionError::TypeError(format!(
405            "Cannot convert to number: {:?}",
406            val
407        ))),
408    }
409}
410
411fn parse_string_number(s: &str) -> Result<f64> {
412    s.parse::<f64>().map_err(|_| string_number_error(s))
413}
414
415#[cold]
416#[inline(never)]
417fn string_number_error(s: &str) -> ExpressionError {
418    ExpressionError::TypeError(format!("Cannot convert string to number: {s}"))
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use std::borrow::Cow;
425    use webui_protocol::{ComparisonOperator, ConditionExpr, LogicalOperator};
426    use webui_test_utils::test_json;
427
428    #[test]
429    fn boolean_literals_do_not_resolve_state_keys() {
430        let state = test_json!({"true": false, "false": true});
431        assert_eq!(
432            evaluate(&ConditionExpr::identifier("true"), &state).unwrap(),
433            true
434        );
435        assert_eq!(
436            evaluate(&ConditionExpr::identifier("false"), &state).unwrap(),
437            false
438        );
439    }
440
441    #[test]
442    fn test_simple_identifier() {
443        // Test true identifier
444        let condition = ConditionExpr::identifier("flag");
445
446        let state = test_json!({
447            "flag": true
448        });
449
450        let result = evaluate(&condition, &state);
451        assert!(
452            matches!(result, Ok(true)),
453            "Expected Ok(true), got {:?}",
454            result
455        );
456
457        // Test false identifier
458        let condition = ConditionExpr::identifier("flag");
459
460        let state = test_json!({
461            "flag": false
462        });
463
464        let result = evaluate(&condition, &state);
465        assert!(
466            matches!(result, Ok(false)),
467            "Expected Ok(false), got {:?}",
468            result
469        );
470
471        // Test non-boolean identifier treated as boolean
472        let condition = ConditionExpr::identifier("name");
473
474        let state = test_json!({
475            "name": "John"
476        });
477
478        let result = evaluate(&condition, &state);
479        assert!(
480            matches!(result, Ok(true)),
481            "Expected Ok(true), got {:?}",
482            result
483        ); // Non-empty string is truthy
484    }
485
486    #[test]
487    fn test_predicate() {
488        let condition = ConditionExpr::predicate("age", ComparisonOperator::GreaterThan, "18");
489
490        // Test with age > 18
491        let state = test_json!({
492            "age": 21
493        });
494
495        let result = evaluate(&condition, &state);
496        assert!(
497            matches!(result, Ok(true)),
498            "Expected Ok(true), got {:?}",
499            result
500        );
501
502        // Test with age < 18
503        let state = test_json!({
504            "age": 16
505        });
506
507        let result = evaluate(&condition, &state);
508        assert!(
509            matches!(result, Ok(false)),
510            "Expected Ok(false), got {:?}",
511            result
512        );
513    }
514
515    #[test]
516    fn test_not_expression() {
517        let condition = ConditionExpr::negated(ConditionExpr::identifier("flag"));
518
519        // Test with flag = true
520        let state = test_json!({
521            "flag": true
522        });
523
524        let result = evaluate(&condition, &state);
525        assert!(
526            matches!(result, Ok(false)),
527            "Expected Ok(false), got {:?}",
528            result
529        );
530
531        // Test with flag = false
532        let state = test_json!({
533            "flag": false
534        });
535
536        let result = evaluate(&condition, &state);
537        assert!(
538            matches!(result, Ok(true)),
539            "Expected Ok(true), got {:?}",
540            result
541        );
542    }
543
544    #[test]
545    fn test_compound_expression() {
546        // Test AND
547        let condition = ConditionExpr::compound(
548            ConditionExpr::identifier("isAdmin"),
549            LogicalOperator::And,
550            ConditionExpr::identifier("isActive"),
551        );
552
553        let state = test_json!({
554            "isAdmin": true,
555            "isActive": true
556        });
557
558        let result = evaluate(&condition, &state);
559        assert!(
560            matches!(result, Ok(true)),
561            "Expected Ok(true), got {:?}",
562            result
563        );
564
565        // Test OR
566        let condition = ConditionExpr::compound(
567            ConditionExpr::identifier("isAdmin"),
568            LogicalOperator::Or,
569            ConditionExpr::identifier("isEditor"),
570        );
571
572        let state = test_json!({
573            "isAdmin": false,
574            "isEditor": true
575        });
576
577        let result = evaluate(&condition, &state);
578        assert!(
579            matches!(result, Ok(true)),
580            "Expected Ok(true), got {:?}",
581            result
582        );
583    }
584
585    #[test]
586    fn test_mixed_operators_error() {
587        // Create a condition with mixed operators (AND and OR)
588        let condition = ConditionExpr::compound(
589            ConditionExpr::compound(
590                ConditionExpr::identifier("a"),
591                LogicalOperator::And,
592                ConditionExpr::identifier("b"),
593            ),
594            LogicalOperator::Or,
595            ConditionExpr::identifier("c"),
596        );
597
598        let state = test_json!({
599            "a": true, "b": true, "c": true
600        });
601
602        let result = evaluate(&condition, &state);
603        assert!(matches!(result, Err(ExpressionError::MixedOperators)));
604    }
605
606    #[test]
607    fn test_too_many_operators_error() {
608        // Create a condition with more than 5 operators
609        let mut condition = ConditionExpr::identifier("a");
610
611        // Add 6 logical operators (exceeding the limit of 5)
612        for i in 0..6 {
613            condition = ConditionExpr::compound(
614                condition,
615                LogicalOperator::And,
616                ConditionExpr::identifier(format!("var{}", i)),
617            );
618        }
619
620        let state = test_json!({
621            "a": true, "var0": true, "var1": true,
622            "var2": true, "var3": true, "var4": true, "var5": true
623        });
624
625        let result = evaluate(&condition, &state);
626        assert!(matches!(result, Err(ExpressionError::TooManyOperators(_))));
627    }
628
629    #[test]
630    fn test_comparison_operators() {
631        let state = test_json!({
632            "value": 10
633        });
634
635        // Test each comparison operator
636        let ops = [
637            (ComparisonOperator::GreaterThan, "5", true),
638            (ComparisonOperator::GreaterThan, "15", false),
639            (ComparisonOperator::LessThan, "15", true),
640            (ComparisonOperator::LessThan, "5", false),
641            (ComparisonOperator::Equal, "10", true),
642            (ComparisonOperator::Equal, "11", false),
643            (ComparisonOperator::NotEqual, "11", true),
644            (ComparisonOperator::NotEqual, "10", false),
645            (ComparisonOperator::GreaterThanOrEqual, "10", true),
646            (ComparisonOperator::GreaterThanOrEqual, "11", false),
647            (ComparisonOperator::LessThanOrEqual, "10", true),
648            (ComparisonOperator::LessThanOrEqual, "9", false),
649        ];
650
651        for (op, right, expected) in ops.iter() {
652            let condition = ConditionExpr::predicate("value", *op, *right);
653
654            let result = evaluate(&condition, &state);
655            assert!(
656                matches!(result, Ok(val) if val == *expected),
657                "Failed for operator {:?} with right value {}: expected Ok({}), got {:?}",
658                op,
659                right,
660                expected,
661                result
662            );
663        }
664    }
665
666    #[test]
667    fn test_string_comparison() {
668        let state = test_json!({
669            "name": "John"
670        });
671
672        // Test string equality
673        let condition = ConditionExpr::predicate("name", ComparisonOperator::Equal, "\"John\"");
674
675        let result = evaluate(&condition, &state);
676        assert!(
677            matches!(result, Ok(true)),
678            "Expected Ok(true), got {:?}",
679            result
680        );
681
682        // Test string inequality
683        let condition = ConditionExpr::predicate("name", ComparisonOperator::NotEqual, "\"Jane\"");
684
685        let result = evaluate(&condition, &state);
686        assert!(
687            matches!(result, Ok(true)),
688            "Expected Ok(true), got {:?}",
689            result
690        );
691    }
692
693    #[test]
694    fn quoted_literals_match_string_state_operands() {
695        let values = [
696            Value::Null,
697            test_json!(false),
698            test_json!(true),
699            test_json!(0),
700            test_json!(-0.0),
701            test_json!(42),
702            test_json!(i64::MIN),
703            test_json!(u64::MAX),
704            test_json!(0.5),
705            test_json!(""),
706            test_json!("active"),
707            test_json!("42"),
708            test_json!("NaN"),
709            test_json!([]),
710            test_json!({}),
711        ];
712        let literals = ["", "active", "42", "-0", "1e300", "NaN", "é水", r"a\nb"];
713        let operators = [
714            ComparisonOperator::Equal,
715            ComparisonOperator::NotEqual,
716            ComparisonOperator::GreaterThan,
717            ComparisonOperator::LessThan,
718            ComparisonOperator::GreaterThanOrEqual,
719            ComparisonOperator::LessThanOrEqual,
720            ComparisonOperator::Unspecified,
721        ];
722
723        for left in values {
724            for literal in literals {
725                let state = test_json!({ "left": left, "right": literal });
726                for op in operators {
727                    let variable = ConditionExpr::predicate("left", op, "right");
728                    let expected = evaluate(&variable, &state).map_err(|error| error.to_string());
729                    for quote in ['\'', '"'] {
730                        let condition = ConditionExpr::predicate(
731                            "left",
732                            op,
733                            format!("{quote}{literal}{quote}"),
734                        );
735                        assert_eq!(
736                            evaluate(&condition, &state).map_err(|error| error.to_string()),
737                            expected,
738                            "literal {literal:?}, left {left:?}, operator {op:?}"
739                        );
740                    }
741                }
742            }
743        }
744    }
745
746    #[test]
747    fn quoted_literals_borrow_the_condition_text() {
748        for text in ["''", "\"\"", "'active'", "\"é水\"", r"'a\nb'"] {
749            let Ok(Operand::String(value)) = parse_literal(text) else {
750                panic!("expected a borrowed string literal for {text:?}");
751            };
752            let expected = &text[1..text.len() - 1];
753            assert_eq!(value, expected);
754            assert_eq!(value.as_ptr(), expected.as_ptr());
755        }
756    }
757
758    #[test]
759    fn malformed_quoted_literals_return_errors() {
760        let state = test_json!({ "value": "active" });
761        for right in ["'", "\"", "'active", "\"active", "'active\"", "\"active'"] {
762            let condition = ConditionExpr::predicate("value", ComparisonOperator::Equal, right);
763            assert_eq!(
764                evaluate(&condition, &state).map_err(|error| error.to_string()),
765                Err(format!("Type error: Invalid literal: {right}"))
766            );
767        }
768    }
769
770    #[test]
771    fn quoted_literals_preserve_error_order_and_short_circuiting() {
772        let state = test_json!({ "value": "active", "enabled": false });
773        let missing = ConditionExpr::predicate("absent", ComparisonOperator::Unspecified, "'");
774        assert!(matches!(
775            evaluate(&missing, &state),
776            Err(ExpressionError::MissingValue(name)) if name == "absent"
777        ));
778
779        let malformed = ConditionExpr::predicate("value", ComparisonOperator::Unspecified, "'");
780        assert!(matches!(
781            evaluate(&malformed, &state),
782            Err(ExpressionError::TypeError(_))
783        ));
784
785        let short_circuit = ConditionExpr::compound(
786            ConditionExpr::identifier("enabled"),
787            LogicalOperator::And,
788            malformed,
789        );
790        assert_eq!(evaluate(&short_circuit, &state).ok(), Some(false));
791    }
792
793    #[test]
794    fn test_dotted_path() {
795        let state = test_json!({
796            "user": {
797                "profile": {
798                    "age": 25,
799                    "name": "John"
800                }
801            }
802        });
803
804        // Test nested property access
805        let condition =
806            ConditionExpr::predicate("user.profile.age", ComparisonOperator::GreaterThan, "18");
807
808        let result = evaluate(&condition, &state);
809        assert!(
810            matches!(result, Ok(true)),
811            "Expected Ok(true), got {:?}",
812            result
813        );
814    }
815
816    #[test]
817    fn test_borrowed_resolver_value() {
818        let condition = ConditionExpr::identifier("flag");
819        let value = Value::Bool(true);
820
821        let result = evaluate_with_resolver(&condition, |path| {
822            if path == "flag" {
823                Some(Cow::Borrowed(&value))
824            } else {
825                None
826            }
827        });
828
829        assert!(matches!(result, Ok(true)));
830    }
831
832    #[test]
833    fn test_missing_value() {
834        let state = test_json!({
835            "user": {
836                "name": "John"
837            }
838        });
839
840        // Test with a missing value
841        let condition = ConditionExpr::predicate("user.age", ComparisonOperator::GreaterThan, "18");
842
843        let result = evaluate(&condition, &state);
844        assert!(matches!(result, Err(ExpressionError::MissingValue(_))));
845    }
846
847    // === Identifier Edge Cases ===
848
849    #[test]
850    fn test_missing_identifier_is_falsy_before_negation() {
851        let condition = ConditionExpr::identifier("notExist");
852        let negated = ConditionExpr::negated(condition.clone());
853        let state = test_json!({ "flag": true });
854
855        assert!(
856            matches!(evaluate(&condition, &state), Ok(false)),
857            "a missing identifier must be a falsy operand"
858        );
859        assert!(
860            matches!(evaluate(&negated, &state), Ok(true)),
861            "negating a missing identifier must evaluate to true"
862        );
863    }
864
865    #[test]
866    fn test_zero_field() {
867        let condition = ConditionExpr::identifier("zero");
868        let state = test_json!({ "zero": 0 });
869
870        let result = evaluate(&condition, &state);
871        assert!(
872            matches!(result, Ok(false)),
873            "Expected Ok(false), got {:?}",
874            result
875        );
876    }
877
878    #[test]
879    fn test_empty_string_field() {
880        let condition = ConditionExpr::identifier("empty");
881        let state = test_json!({ "empty": "" });
882
883        let result = evaluate(&condition, &state);
884        assert!(
885            matches!(result, Ok(false)),
886            "Expected Ok(false), got {:?}",
887            result
888        );
889    }
890
891    #[test]
892    fn test_nonempty_array() {
893        let condition = ConditionExpr::identifier("myList");
894        let state = test_json!({ "myList": [1, 2, 3] });
895
896        let result = evaluate(&condition, &state);
897        assert!(
898            matches!(result, Ok(true)),
899            "Expected Ok(true), got {:?}",
900            result
901        );
902    }
903
904    #[test]
905    fn test_empty_array() {
906        let condition = ConditionExpr::identifier("emptyList");
907        let state = test_json!({ "emptyList": [] });
908
909        let result = evaluate(&condition, &state);
910        assert!(
911            matches!(result, Ok(false)),
912            "Expected Ok(false), got {:?}",
913            result
914        );
915    }
916
917    // === Deep Dotted Path ===
918
919    #[test]
920    fn test_dotted_path_deep() {
921        let condition = ConditionExpr::identifier("outer.nested.deep.value");
922        let state = test_json!({
923            "outer": { "nested": { "deep": { "value": true } } }
924        });
925
926        let result = evaluate(&condition, &state);
927        assert!(
928            matches!(result, Ok(true)),
929            "Expected Ok(true), got {:?}",
930            result
931        );
932    }
933
934    // === Comparison Edge Cases ===
935
936    #[test]
937    fn test_string_eq() {
938        let condition =
939            ConditionExpr::predicate("appearance", ComparisonOperator::Equal, "\"hub\"");
940        let state = test_json!({ "appearance": "hub" });
941
942        let result = evaluate(&condition, &state);
943        assert!(
944            matches!(result, Ok(true)),
945            "Expected Ok(true), got {:?}",
946            result
947        );
948    }
949
950    #[test]
951    fn test_string_ne() {
952        let condition =
953            ConditionExpr::predicate("appearance", ComparisonOperator::NotEqual, "\"full-page\"");
954        let state = test_json!({ "appearance": "hub" });
955
956        let result = evaluate(&condition, &state);
957        assert!(
958            matches!(result, Ok(true)),
959            "Expected Ok(true), got {:?}",
960            result
961        );
962    }
963
964    #[test]
965    fn test_number_eq() {
966        let condition = ConditionExpr::predicate("x", ComparisonOperator::Equal, "5");
967        let state = test_json!({ "x": 5 });
968
969        let result = evaluate(&condition, &state);
970        assert!(
971            matches!(result, Ok(true)),
972            "Expected Ok(true), got {:?}",
973            result
974        );
975    }
976
977    #[test]
978    fn test_less_than() {
979        let condition = ConditionExpr::predicate("x", ComparisonOperator::LessThan, "y");
980        let state = test_json!({ "x": 5, "y": 10 });
981
982        let result = evaluate(&condition, &state);
983        assert!(
984            matches!(result, Ok(true)),
985            "Expected Ok(true), got {:?}",
986            result
987        );
988    }
989
990    #[test]
991    fn test_greater_than() {
992        let condition = ConditionExpr::predicate("y", ComparisonOperator::GreaterThan, "x");
993        let state = test_json!({ "x": 5, "y": 10 });
994
995        let result = evaluate(&condition, &state);
996        assert!(
997            matches!(result, Ok(true)),
998            "Expected Ok(true), got {:?}",
999            result
1000        );
1001    }
1002
1003    #[test]
1004    fn test_less_equal() {
1005        let condition = ConditionExpr::predicate("x", ComparisonOperator::LessThanOrEqual, "5");
1006        let state = test_json!({ "x": 5 });
1007
1008        let result = evaluate(&condition, &state);
1009        assert!(
1010            matches!(result, Ok(true)),
1011            "Expected Ok(true), got {:?}",
1012            result
1013        );
1014    }
1015
1016    #[test]
1017    fn test_greater_equal() {
1018        let condition = ConditionExpr::predicate("y", ComparisonOperator::GreaterThanOrEqual, "10");
1019        let state = test_json!({ "y": 10 });
1020
1021        let result = evaluate(&condition, &state);
1022        assert!(
1023            matches!(result, Ok(true)),
1024            "Expected Ok(true), got {:?}",
1025            result
1026        );
1027    }
1028
1029    #[test]
1030    fn test_nested_eq() {
1031        let condition = ConditionExpr::predicate("outer.inner", ComparisonOperator::Equal, "42");
1032        let state = test_json!({ "outer": { "inner": 42 } });
1033
1034        let result = evaluate(&condition, &state);
1035        assert!(
1036            matches!(result, Ok(true)),
1037            "Expected Ok(true), got {:?}",
1038            result
1039        );
1040    }
1041
1042    // === Short-Circuit Evaluation ===
1043
1044    #[test]
1045    fn test_and_true_true() {
1046        let condition = ConditionExpr::compound(
1047            ConditionExpr::identifier("isEnabled"),
1048            LogicalOperator::And,
1049            ConditionExpr::identifier("x"),
1050        );
1051        let state = test_json!({ "isEnabled": true, "x": 5 });
1052
1053        let result = evaluate(&condition, &state);
1054        assert!(
1055            matches!(result, Ok(true)),
1056            "Expected Ok(true), got {:?}",
1057            result
1058        );
1059    }
1060
1061    #[test]
1062    fn test_and_true_false() {
1063        let condition = ConditionExpr::compound(
1064            ConditionExpr::identifier("isEnabled"),
1065            LogicalOperator::And,
1066            ConditionExpr::identifier("isDisabled"),
1067        );
1068        let state = test_json!({ "isEnabled": true, "isDisabled": false });
1069
1070        let result = evaluate(&condition, &state);
1071        assert!(
1072            matches!(result, Ok(false)),
1073            "Expected Ok(false), got {:?}",
1074            result
1075        );
1076    }
1077
1078    #[test]
1079    fn test_or_false_false() {
1080        let condition = ConditionExpr::compound(
1081            ConditionExpr::identifier("isDisabled"),
1082            LogicalOperator::Or,
1083            ConditionExpr::identifier("zero"),
1084        );
1085        let state = test_json!({ "isDisabled": false, "zero": 0 });
1086
1087        let result = evaluate(&condition, &state);
1088        assert!(
1089            matches!(result, Ok(false)),
1090            "Expected Ok(false), got {:?}",
1091            result
1092        );
1093    }
1094
1095    #[test]
1096    fn test_or_false_true() {
1097        let condition = ConditionExpr::compound(
1098            ConditionExpr::identifier("isDisabled"),
1099            LogicalOperator::Or,
1100            ConditionExpr::identifier("isEnabled"),
1101        );
1102        let state = test_json!({ "isDisabled": false, "isEnabled": true });
1103
1104        let result = evaluate(&condition, &state);
1105        assert!(
1106            matches!(result, Ok(true)),
1107            "Expected Ok(true), got {:?}",
1108            result
1109        );
1110    }
1111
1112    // === Complex Expressions ===
1113
1114    #[test]
1115    fn test_appearance_and_actions() {
1116        let condition = ConditionExpr::compound(
1117            ConditionExpr::predicate("appearance", ComparisonOperator::Equal, "\"hub\""),
1118            LogicalOperator::And,
1119            ConditionExpr::identifier("actions.trailing"),
1120        );
1121        let state = test_json!({
1122            "appearance": "hub",
1123            "actions": { "trailing": true }
1124        });
1125
1126        let result = evaluate(&condition, &state);
1127        assert!(
1128            matches!(result, Ok(true)),
1129            "Expected Ok(true), got {:?}",
1130            result
1131        );
1132    }
1133
1134    #[test]
1135    fn test_negated_binary() {
1136        let condition = ConditionExpr::negated(ConditionExpr::predicate(
1137            "appearance",
1138            ComparisonOperator::Equal,
1139            "\"hub\"",
1140        ));
1141        let state = test_json!({ "appearance": "sidepanel" });
1142
1143        let result = evaluate(&condition, &state);
1144        assert!(
1145            matches!(result, Ok(true)),
1146            "Expected Ok(true), got {:?}",
1147            result
1148        );
1149    }
1150
1151    // === Single-term fast path ===
1152
1153    #[test]
1154    fn test_single_term_matches_operator_guard_semantics() {
1155        let state = test_json!({
1156            "flag": true,
1157            "off": false,
1158            "count": 0,
1159            "name": "Alice",
1160            "user": { "age": 25 }
1161        });
1162
1163        // Single identifiers and predicates carry no logical operators, so they
1164        // must evaluate normally and never surface an operator-limit error.
1165        let cases: [(ConditionExpr, bool); 6] = [
1166            (ConditionExpr::identifier("flag"), true),
1167            (ConditionExpr::identifier("off"), false),
1168            (ConditionExpr::identifier("count"), false),
1169            (ConditionExpr::identifier("name"), true),
1170            (
1171                ConditionExpr::predicate("user.age", ComparisonOperator::GreaterThan, "18"),
1172                true,
1173            ),
1174            (
1175                ConditionExpr::predicate("name", ComparisonOperator::Equal, "'Bob'"),
1176                false,
1177            ),
1178        ];
1179
1180        for (condition, expected) in cases {
1181            assert_eq!(
1182                evaluate(&condition, &state).ok(),
1183                Some(expected),
1184                "single-term condition {:?} must evaluate to {}",
1185                condition,
1186                expected
1187            );
1188        }
1189    }
1190
1191    #[test]
1192    fn test_single_term_predicate_still_reports_missing_value() {
1193        let state = test_json!({ "user": { "name": "John" } });
1194        let condition = ConditionExpr::predicate("user.age", ComparisonOperator::LessThan, "18");
1195
1196        assert!(matches!(
1197            evaluate(&condition, &state),
1198            Err(ExpressionError::MissingValue(_))
1199        ));
1200    }
1201
1202    #[test]
1203    fn test_negated_tree_still_enforces_operator_limit() {
1204        // A `Not` wrapper must not be mistaken for a single term: the operator
1205        // guard has to keep walking the tree underneath it.
1206        let mut inner = ConditionExpr::identifier("a");
1207        for i in 0..6 {
1208            inner = ConditionExpr::compound(
1209                inner,
1210                LogicalOperator::And,
1211                ConditionExpr::identifier(format!("var{}", i)),
1212            );
1213        }
1214        let condition = ConditionExpr::negated(inner);
1215
1216        let state = test_json!({
1217            "a": true, "var0": true, "var1": true,
1218            "var2": true, "var3": true, "var4": true, "var5": true
1219        });
1220
1221        assert!(matches!(
1222            evaluate(&condition, &state),
1223            Err(ExpressionError::TooManyOperators(6))
1224        ));
1225    }
1226
1227    #[test]
1228    fn test_operator_count_is_exact_beyond_inline_stack() {
1229        // A left-deep tree keeps one pending node per open compound, so twelve
1230        // operators spill past the inline stack; the reported count must
1231        // still cover every node.
1232        let mut condition = ConditionExpr::identifier("a");
1233        for i in 0..12 {
1234            condition = ConditionExpr::compound(
1235                condition,
1236                LogicalOperator::Or,
1237                ConditionExpr::identifier(format!("var{}", i)),
1238            );
1239        }
1240
1241        let state = test_json!({ "a": true });
1242
1243        assert!(matches!(
1244            evaluate(&condition, &state),
1245            Err(ExpressionError::TooManyOperators(12))
1246        ));
1247    }
1248
1249    #[test]
1250    fn test_negated_tree_still_rejects_mixed_operators() {
1251        let condition = ConditionExpr::negated(ConditionExpr::compound(
1252            ConditionExpr::compound(
1253                ConditionExpr::identifier("a"),
1254                LogicalOperator::And,
1255                ConditionExpr::identifier("b"),
1256            ),
1257            LogicalOperator::Or,
1258            ConditionExpr::identifier("c"),
1259        ));
1260
1261        let state = test_json!({ "a": true, "b": true, "c": true });
1262
1263        assert!(matches!(
1264            evaluate(&condition, &state),
1265            Err(ExpressionError::MixedOperators)
1266        ));
1267    }
1268
1269    #[test]
1270    fn test_single_term_fast_path_uses_same_resolver_contract() {
1271        let value = Value::String("active".to_string());
1272        let lookups = std::cell::Cell::new(0usize);
1273
1274        let condition = ConditionExpr::predicate("status", ComparisonOperator::Equal, "'active'");
1275        let result = evaluate_with_resolver(&condition, |path| {
1276            lookups.set(lookups.get() + 1);
1277            if path == "status" {
1278                Some(Cow::Borrowed(&value))
1279            } else {
1280                None
1281            }
1282        });
1283
1284        assert!(matches!(result, Ok(true)));
1285        assert_eq!(
1286            lookups.get(),
1287            1,
1288            "a single-term predicate must resolve exactly one path"
1289        );
1290    }
1291}