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
43pub fn evaluate(condition: &ConditionExpr, state: &Value) -> Result<bool> {
44    evaluate_with_resolver(condition, |path| find_value_by_dotted_path_ref(path, state))
45}
46
47/// Evaluate a condition expression using a custom resolver for value lookups.
48///
49/// The `resolver` closure takes a dotted path (e.g., `"contact.name"`) and
50/// returns the resolved value. This allows callers to provide merged views
51/// (e.g., local variables overlaid on global state) without cloning the
52/// entire state tree.
53pub fn evaluate_with_resolver<'a, F>(condition: &ConditionExpr, resolver: F) -> Result<bool>
54where
55    F: Fn(&str) -> Option<Cow<'a, Value>>,
56{
57    let (logical_op_count, has_mixed_ops) = count_logical_operators(condition);
58
59    if logical_op_count > 5 {
60        return Err(ExpressionError::TooManyOperators(logical_op_count));
61    }
62
63    if has_mixed_ops {
64        return Err(ExpressionError::MixedOperators);
65    }
66
67    evaluate_expr(condition, &resolver)
68}
69
70// Helper function to count logical operators and check if they're mixed
71fn count_logical_operators(condition: &ConditionExpr) -> (usize, bool) {
72    let mut count = 0;
73    let mut last_op: Option<i32> = None;
74    let mut has_mixed = false;
75
76    // We need to use a stack to avoid recursion
77    let mut stack = vec![condition];
78
79    while let Some(expr) = stack.pop() {
80        match &expr.expr {
81            Some(condition_expr::Expr::Compound(compound)) => {
82                count += 1;
83
84                // Check if we're mixing operators
85                if let Some(last) = last_op {
86                    if last != compound.op {
87                        has_mixed = true;
88                    }
89                } else {
90                    last_op = Some(compound.op);
91                }
92
93                // Push sub-expressions to stack
94                if let Some(right) = compound.right.as_ref() {
95                    stack.push(right);
96                }
97                if let Some(left) = compound.left.as_ref() {
98                    stack.push(left);
99                }
100            }
101            Some(condition_expr::Expr::Not(not_cond)) => {
102                if let Some(inner) = not_cond.condition.as_ref() {
103                    stack.push(inner);
104                }
105            }
106            _ => {} // Predicates and identifiers don't have logical operators
107        }
108    }
109
110    (count, has_mixed)
111}
112
113// Iterative evaluation of expressions using a resolver closure
114fn evaluate_expr<'a, F>(condition: &ConditionExpr, resolver: &F) -> Result<bool>
115where
116    F: Fn(&str) -> Option<Cow<'a, Value>>,
117{
118    match &condition.expr {
119        Some(condition_expr::Expr::Predicate(pred)) => evaluate_predicate(pred, resolver),
120        Some(condition_expr::Expr::Not(not_cond)) => {
121            let inner = not_cond.condition.as_ref().ok_or_else(|| {
122                ExpressionError::Evaluation("Not condition missing inner expression".to_string())
123            })?;
124            let result = evaluate_expr(inner, resolver)?;
125            Ok(!result)
126        }
127        Some(condition_expr::Expr::Compound(compound)) => evaluate_compound(compound, resolver),
128        Some(condition_expr::Expr::Identifier(id)) => {
129            if let Some(val) = resolver(&id.value) {
130                match val.as_ref() {
131                    Value::Bool(b) => Ok(*b),
132                    Value::Null => Ok(false),
133                    Value::Number(n) => Ok(!(n.as_f64() == Some(0.0))),
134                    Value::String(s) => Ok(!s.is_empty()),
135                    Value::Array(a) => Ok(!a.is_empty()),
136                    Value::Object(o) => Ok(!o.is_empty()),
137                }
138            } else {
139                Err(ExpressionError::MissingValue(id.value.clone()))
140            }
141        }
142        None => Err(ExpressionError::Evaluation(
143            "Empty condition expression".to_string(),
144        )),
145    }
146}
147
148fn evaluate_compound<'a, F>(compound: &CompoundCondition, resolver: &F) -> Result<bool>
149where
150    F: Fn(&str) -> Option<Cow<'a, Value>>,
151{
152    let left = compound.left.as_ref().ok_or_else(|| {
153        ExpressionError::Evaluation("Compound missing left expression".to_string())
154    })?;
155    let right = compound.right.as_ref().ok_or_else(|| {
156        ExpressionError::Evaluation("Compound missing right expression".to_string())
157    })?;
158
159    let left_result = evaluate_expr(left, resolver)?;
160    let op = LogicalOperator::try_from(compound.op).map_err(|_| {
161        ExpressionError::Evaluation(format!("Invalid logical operator: {}", compound.op))
162    })?;
163
164    match op {
165        LogicalOperator::And => {
166            if !left_result {
167                return Ok(false);
168            }
169            evaluate_expr(right, resolver)
170        }
171        LogicalOperator::Or => {
172            if left_result {
173                return Ok(true);
174            }
175            evaluate_expr(right, resolver)
176        }
177        LogicalOperator::Unspecified => Err(ExpressionError::Evaluation(
178            "Unspecified logical operator".to_string(),
179        )),
180    }
181}
182
183fn evaluate_predicate<'a, F>(predicate: &Predicate, resolver: &F) -> Result<bool>
184where
185    F: Fn(&str) -> Option<Cow<'a, Value>>,
186{
187    let left_val = match resolver(&predicate.left) {
188        Some(val) => val,
189        None => return Err(ExpressionError::MissingValue(predicate.left.clone())),
190    };
191
192    let right_val = if is_literal(&predicate.right) {
193        Cow::Owned(parse_literal(&predicate.right)?)
194    } else {
195        match resolver(&predicate.right) {
196            Some(val) => val,
197            None => return Err(ExpressionError::MissingValue(predicate.right.clone())),
198        }
199    };
200
201    let op = ComparisonOperator::try_from(predicate.operator).map_err(|_| {
202        ExpressionError::Evaluation(format!(
203            "Invalid comparison operator: {}",
204            predicate.operator
205        ))
206    })?;
207
208    compare_values(left_val.as_ref(), &op, right_val.as_ref())
209}
210
211// Check if a string is a literal value
212fn is_literal(s: &str) -> bool {
213    // A string is a literal if:
214    // - It starts with a quote (single or double)
215    // - It's a number (starts with a digit or negative sign followed by a digit)
216    // - It's a boolean ("true" or "false")
217    s.starts_with('"')
218        || s.starts_with('\'')
219        || s.starts_with(|c: char| {
220            c.is_ascii_digit()
221                || (c == '-' && s.len() > 1 && s.chars().nth(1).is_some_and(|c| c.is_ascii_digit()))
222        })
223        || s == "true"
224        || s == "false"
225}
226
227// Parse a literal string into a JSON value
228fn parse_literal(s: &str) -> Result<Value> {
229    // Handle quoted strings
230    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
231        let content = &s[1..s.len() - 1];
232        return Ok(Value::String(content.to_string()));
233    }
234
235    // Handle booleans
236    if s == "true" {
237        return Ok(Value::Bool(true));
238    }
239    if s == "false" {
240        return Ok(Value::Bool(false));
241    }
242
243    // Handle numbers
244    if let Ok(num) = s.parse::<i64>() {
245        return Ok(Value::Number(num.into()));
246    }
247
248    if let Ok(num) = s.parse::<f64>() {
249        // Create a JSON number from f64, handling error if it's not representable
250        match serde_json::Number::from_f64(num) {
251            Some(n) => return Ok(Value::Number(n)),
252            None => {
253                return Err(ExpressionError::TypeError(format!(
254                    "Cannot convert {} to JSON number",
255                    s
256                )))
257            }
258        }
259    }
260
261    // If we get here, it's not a recognized literal
262    Err(ExpressionError::TypeError(format!(
263        "Invalid literal: {}",
264        s
265    )))
266}
267
268// Compare two JSON values based on the comparison operator
269fn compare_values(left: &Value, op: &ComparisonOperator, right: &Value) -> Result<bool> {
270    match op {
271        ComparisonOperator::Equal => Ok(left == right),
272        ComparisonOperator::NotEqual => Ok(left != right),
273
274        // Handle numeric comparisons
275        ComparisonOperator::GreaterThan => compare_ordered(left, right, |a, b| a > b),
276        ComparisonOperator::LessThan => compare_ordered(left, right, |a, b| a < b),
277        ComparisonOperator::GreaterThanOrEqual => compare_ordered(left, right, |a, b| a >= b),
278        ComparisonOperator::LessThanOrEqual => compare_ordered(left, right, |a, b| a <= b),
279        ComparisonOperator::Unspecified => Err(ExpressionError::Evaluation(
280            "Unspecified comparison operator".to_string(),
281        )),
282    }
283}
284
285// Helper for ordered comparisons
286fn compare_ordered<F>(left: &Value, right: &Value, compare_fn: F) -> Result<bool>
287where
288    F: Fn(&f64, &f64) -> bool,
289{
290    // Extract numeric values
291    let left_num = extract_number(left)?;
292    let right_num = extract_number(right)?;
293
294    Ok(compare_fn(&left_num, &right_num))
295}
296
297// Extract a numeric value from a JSON value
298fn extract_number(val: &Value) -> Result<f64> {
299    match val {
300        Value::Number(n) => {
301            if let Some(f) = n.as_f64() {
302                Ok(f)
303            } else {
304                Err(ExpressionError::TypeError(format!(
305                    "Cannot convert number to f64: {:?}",
306                    n
307                )))
308            }
309        }
310        Value::String(s) => match s.parse::<f64>() {
311            Ok(num) => Ok(num),
312            Err(_) => Err(ExpressionError::TypeError(format!(
313                "Cannot convert string to number: {}",
314                s
315            ))),
316        },
317        Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
318        _ => Err(ExpressionError::TypeError(format!(
319            "Cannot convert to number: {:?}",
320            val
321        ))),
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use std::borrow::Cow;
329    use webui_protocol::{ComparisonOperator, ConditionExpr, LogicalOperator};
330    use webui_test_utils::test_json;
331
332    #[test]
333    fn test_simple_identifier() {
334        // Test true identifier
335        let condition = ConditionExpr::identifier("flag");
336
337        let state = test_json!({
338            "flag": true
339        });
340
341        let result = evaluate(&condition, &state);
342        assert!(
343            matches!(result, Ok(true)),
344            "Expected Ok(true), got {:?}",
345            result
346        );
347
348        // Test false identifier
349        let condition = ConditionExpr::identifier("flag");
350
351        let state = test_json!({
352            "flag": false
353        });
354
355        let result = evaluate(&condition, &state);
356        assert!(
357            matches!(result, Ok(false)),
358            "Expected Ok(false), got {:?}",
359            result
360        );
361
362        // Test non-boolean identifier treated as boolean
363        let condition = ConditionExpr::identifier("name");
364
365        let state = test_json!({
366            "name": "John"
367        });
368
369        let result = evaluate(&condition, &state);
370        assert!(
371            matches!(result, Ok(true)),
372            "Expected Ok(true), got {:?}",
373            result
374        ); // Non-empty string is truthy
375    }
376
377    #[test]
378    fn test_predicate() {
379        let condition = ConditionExpr::predicate("age", ComparisonOperator::GreaterThan, "18");
380
381        // Test with age > 18
382        let state = test_json!({
383            "age": 21
384        });
385
386        let result = evaluate(&condition, &state);
387        assert!(
388            matches!(result, Ok(true)),
389            "Expected Ok(true), got {:?}",
390            result
391        );
392
393        // Test with age < 18
394        let state = test_json!({
395            "age": 16
396        });
397
398        let result = evaluate(&condition, &state);
399        assert!(
400            matches!(result, Ok(false)),
401            "Expected Ok(false), got {:?}",
402            result
403        );
404    }
405
406    #[test]
407    fn test_not_expression() {
408        let condition = ConditionExpr::negated(ConditionExpr::identifier("flag"));
409
410        // Test with flag = true
411        let state = test_json!({
412            "flag": true
413        });
414
415        let result = evaluate(&condition, &state);
416        assert!(
417            matches!(result, Ok(false)),
418            "Expected Ok(false), got {:?}",
419            result
420        );
421
422        // Test with flag = false
423        let state = test_json!({
424            "flag": false
425        });
426
427        let result = evaluate(&condition, &state);
428        assert!(
429            matches!(result, Ok(true)),
430            "Expected Ok(true), got {:?}",
431            result
432        );
433    }
434
435    #[test]
436    fn test_compound_expression() {
437        // Test AND
438        let condition = ConditionExpr::compound(
439            ConditionExpr::identifier("isAdmin"),
440            LogicalOperator::And,
441            ConditionExpr::identifier("isActive"),
442        );
443
444        let state = test_json!({
445            "isAdmin": true,
446            "isActive": true
447        });
448
449        let result = evaluate(&condition, &state);
450        assert!(
451            matches!(result, Ok(true)),
452            "Expected Ok(true), got {:?}",
453            result
454        );
455
456        // Test OR
457        let condition = ConditionExpr::compound(
458            ConditionExpr::identifier("isAdmin"),
459            LogicalOperator::Or,
460            ConditionExpr::identifier("isEditor"),
461        );
462
463        let state = test_json!({
464            "isAdmin": false,
465            "isEditor": true
466        });
467
468        let result = evaluate(&condition, &state);
469        assert!(
470            matches!(result, Ok(true)),
471            "Expected Ok(true), got {:?}",
472            result
473        );
474    }
475
476    #[test]
477    fn test_mixed_operators_error() {
478        // Create a condition with mixed operators (AND and OR)
479        let condition = ConditionExpr::compound(
480            ConditionExpr::compound(
481                ConditionExpr::identifier("a"),
482                LogicalOperator::And,
483                ConditionExpr::identifier("b"),
484            ),
485            LogicalOperator::Or,
486            ConditionExpr::identifier("c"),
487        );
488
489        let state = test_json!({
490            "a": true, "b": true, "c": true
491        });
492
493        let result = evaluate(&condition, &state);
494        assert!(matches!(result, Err(ExpressionError::MixedOperators)));
495    }
496
497    #[test]
498    fn test_too_many_operators_error() {
499        // Create a condition with more than 5 operators
500        let mut condition = ConditionExpr::identifier("a");
501
502        // Add 6 logical operators (exceeding the limit of 5)
503        for i in 0..6 {
504            condition = ConditionExpr::compound(
505                condition,
506                LogicalOperator::And,
507                ConditionExpr::identifier(format!("var{}", i)),
508            );
509        }
510
511        let state = test_json!({
512            "a": true, "var0": true, "var1": true,
513            "var2": true, "var3": true, "var4": true, "var5": true
514        });
515
516        let result = evaluate(&condition, &state);
517        assert!(matches!(result, Err(ExpressionError::TooManyOperators(_))));
518    }
519
520    #[test]
521    fn test_comparison_operators() {
522        let state = test_json!({
523            "value": 10
524        });
525
526        // Test each comparison operator
527        let ops = [
528            (ComparisonOperator::GreaterThan, "5", true),
529            (ComparisonOperator::GreaterThan, "15", false),
530            (ComparisonOperator::LessThan, "15", true),
531            (ComparisonOperator::LessThan, "5", false),
532            (ComparisonOperator::Equal, "10", true),
533            (ComparisonOperator::Equal, "11", false),
534            (ComparisonOperator::NotEqual, "11", true),
535            (ComparisonOperator::NotEqual, "10", false),
536            (ComparisonOperator::GreaterThanOrEqual, "10", true),
537            (ComparisonOperator::GreaterThanOrEqual, "11", false),
538            (ComparisonOperator::LessThanOrEqual, "10", true),
539            (ComparisonOperator::LessThanOrEqual, "9", false),
540        ];
541
542        for (op, right, expected) in ops.iter() {
543            let condition = ConditionExpr::predicate("value", *op, *right);
544
545            let result = evaluate(&condition, &state);
546            assert!(
547                matches!(result, Ok(val) if val == *expected),
548                "Failed for operator {:?} with right value {}: expected Ok({}), got {:?}",
549                op,
550                right,
551                expected,
552                result
553            );
554        }
555    }
556
557    #[test]
558    fn test_string_comparison() {
559        let state = test_json!({
560            "name": "John"
561        });
562
563        // Test string equality
564        let condition = ConditionExpr::predicate("name", ComparisonOperator::Equal, "\"John\"");
565
566        let result = evaluate(&condition, &state);
567        assert!(
568            matches!(result, Ok(true)),
569            "Expected Ok(true), got {:?}",
570            result
571        );
572
573        // Test string inequality
574        let condition = ConditionExpr::predicate("name", ComparisonOperator::NotEqual, "\"Jane\"");
575
576        let result = evaluate(&condition, &state);
577        assert!(
578            matches!(result, Ok(true)),
579            "Expected Ok(true), got {:?}",
580            result
581        );
582    }
583
584    #[test]
585    fn test_dotted_path() {
586        let state = test_json!({
587            "user": {
588                "profile": {
589                    "age": 25,
590                    "name": "John"
591                }
592            }
593        });
594
595        // Test nested property access
596        let condition =
597            ConditionExpr::predicate("user.profile.age", ComparisonOperator::GreaterThan, "18");
598
599        let result = evaluate(&condition, &state);
600        assert!(
601            matches!(result, Ok(true)),
602            "Expected Ok(true), got {:?}",
603            result
604        );
605    }
606
607    #[test]
608    fn test_borrowed_resolver_value() {
609        let condition = ConditionExpr::identifier("flag");
610        let value = Value::Bool(true);
611
612        let result = evaluate_with_resolver(&condition, |path| {
613            if path == "flag" {
614                Some(Cow::Borrowed(&value))
615            } else {
616                None
617            }
618        });
619
620        assert!(matches!(result, Ok(true)));
621    }
622
623    #[test]
624    fn test_missing_value() {
625        let state = test_json!({
626            "user": {
627                "name": "John"
628            }
629        });
630
631        // Test with a missing value
632        let condition = ConditionExpr::predicate("user.age", ComparisonOperator::GreaterThan, "18");
633
634        let result = evaluate(&condition, &state);
635        assert!(matches!(result, Err(ExpressionError::MissingValue(_))));
636    }
637
638    // === Identifier Edge Cases ===
639
640    #[test]
641    fn test_missing_field() {
642        let condition = ConditionExpr::identifier("notExist");
643        let state = test_json!({ "flag": true });
644
645        let result = evaluate(&condition, &state);
646        assert!(
647            matches!(result, Err(ExpressionError::MissingValue(_))),
648            "Expected Err(MissingValue), got {:?}",
649            result
650        );
651    }
652
653    #[test]
654    fn test_zero_field() {
655        let condition = ConditionExpr::identifier("zero");
656        let state = test_json!({ "zero": 0 });
657
658        let result = evaluate(&condition, &state);
659        assert!(
660            matches!(result, Ok(false)),
661            "Expected Ok(false), got {:?}",
662            result
663        );
664    }
665
666    #[test]
667    fn test_empty_string_field() {
668        let condition = ConditionExpr::identifier("empty");
669        let state = test_json!({ "empty": "" });
670
671        let result = evaluate(&condition, &state);
672        assert!(
673            matches!(result, Ok(false)),
674            "Expected Ok(false), got {:?}",
675            result
676        );
677    }
678
679    #[test]
680    fn test_nonempty_array() {
681        let condition = ConditionExpr::identifier("myList");
682        let state = test_json!({ "myList": [1, 2, 3] });
683
684        let result = evaluate(&condition, &state);
685        assert!(
686            matches!(result, Ok(true)),
687            "Expected Ok(true), got {:?}",
688            result
689        );
690    }
691
692    #[test]
693    fn test_empty_array() {
694        let condition = ConditionExpr::identifier("emptyList");
695        let state = test_json!({ "emptyList": [] });
696
697        let result = evaluate(&condition, &state);
698        assert!(
699            matches!(result, Ok(false)),
700            "Expected Ok(false), got {:?}",
701            result
702        );
703    }
704
705    // === Deep Dotted Path ===
706
707    #[test]
708    fn test_dotted_path_deep() {
709        let condition = ConditionExpr::identifier("outer.nested.deep.value");
710        let state = test_json!({
711            "outer": { "nested": { "deep": { "value": true } } }
712        });
713
714        let result = evaluate(&condition, &state);
715        assert!(
716            matches!(result, Ok(true)),
717            "Expected Ok(true), got {:?}",
718            result
719        );
720    }
721
722    // === Comparison Edge Cases ===
723
724    #[test]
725    fn test_string_eq() {
726        let condition =
727            ConditionExpr::predicate("appearance", ComparisonOperator::Equal, "\"hub\"");
728        let state = test_json!({ "appearance": "hub" });
729
730        let result = evaluate(&condition, &state);
731        assert!(
732            matches!(result, Ok(true)),
733            "Expected Ok(true), got {:?}",
734            result
735        );
736    }
737
738    #[test]
739    fn test_string_ne() {
740        let condition =
741            ConditionExpr::predicate("appearance", ComparisonOperator::NotEqual, "\"full-page\"");
742        let state = test_json!({ "appearance": "hub" });
743
744        let result = evaluate(&condition, &state);
745        assert!(
746            matches!(result, Ok(true)),
747            "Expected Ok(true), got {:?}",
748            result
749        );
750    }
751
752    #[test]
753    fn test_number_eq() {
754        let condition = ConditionExpr::predicate("x", ComparisonOperator::Equal, "5");
755        let state = test_json!({ "x": 5 });
756
757        let result = evaluate(&condition, &state);
758        assert!(
759            matches!(result, Ok(true)),
760            "Expected Ok(true), got {:?}",
761            result
762        );
763    }
764
765    #[test]
766    fn test_less_than() {
767        let condition = ConditionExpr::predicate("x", ComparisonOperator::LessThan, "y");
768        let state = test_json!({ "x": 5, "y": 10 });
769
770        let result = evaluate(&condition, &state);
771        assert!(
772            matches!(result, Ok(true)),
773            "Expected Ok(true), got {:?}",
774            result
775        );
776    }
777
778    #[test]
779    fn test_greater_than() {
780        let condition = ConditionExpr::predicate("y", ComparisonOperator::GreaterThan, "x");
781        let state = test_json!({ "x": 5, "y": 10 });
782
783        let result = evaluate(&condition, &state);
784        assert!(
785            matches!(result, Ok(true)),
786            "Expected Ok(true), got {:?}",
787            result
788        );
789    }
790
791    #[test]
792    fn test_less_equal() {
793        let condition = ConditionExpr::predicate("x", ComparisonOperator::LessThanOrEqual, "5");
794        let state = test_json!({ "x": 5 });
795
796        let result = evaluate(&condition, &state);
797        assert!(
798            matches!(result, Ok(true)),
799            "Expected Ok(true), got {:?}",
800            result
801        );
802    }
803
804    #[test]
805    fn test_greater_equal() {
806        let condition = ConditionExpr::predicate("y", ComparisonOperator::GreaterThanOrEqual, "10");
807        let state = test_json!({ "y": 10 });
808
809        let result = evaluate(&condition, &state);
810        assert!(
811            matches!(result, Ok(true)),
812            "Expected Ok(true), got {:?}",
813            result
814        );
815    }
816
817    #[test]
818    fn test_nested_eq() {
819        let condition = ConditionExpr::predicate("outer.inner", ComparisonOperator::Equal, "42");
820        let state = test_json!({ "outer": { "inner": 42 } });
821
822        let result = evaluate(&condition, &state);
823        assert!(
824            matches!(result, Ok(true)),
825            "Expected Ok(true), got {:?}",
826            result
827        );
828    }
829
830    // === Short-Circuit Evaluation ===
831
832    #[test]
833    fn test_and_true_true() {
834        let condition = ConditionExpr::compound(
835            ConditionExpr::identifier("isEnabled"),
836            LogicalOperator::And,
837            ConditionExpr::identifier("x"),
838        );
839        let state = test_json!({ "isEnabled": true, "x": 5 });
840
841        let result = evaluate(&condition, &state);
842        assert!(
843            matches!(result, Ok(true)),
844            "Expected Ok(true), got {:?}",
845            result
846        );
847    }
848
849    #[test]
850    fn test_and_true_false() {
851        let condition = ConditionExpr::compound(
852            ConditionExpr::identifier("isEnabled"),
853            LogicalOperator::And,
854            ConditionExpr::identifier("isDisabled"),
855        );
856        let state = test_json!({ "isEnabled": true, "isDisabled": false });
857
858        let result = evaluate(&condition, &state);
859        assert!(
860            matches!(result, Ok(false)),
861            "Expected Ok(false), got {:?}",
862            result
863        );
864    }
865
866    #[test]
867    fn test_or_false_false() {
868        let condition = ConditionExpr::compound(
869            ConditionExpr::identifier("isDisabled"),
870            LogicalOperator::Or,
871            ConditionExpr::identifier("zero"),
872        );
873        let state = test_json!({ "isDisabled": false, "zero": 0 });
874
875        let result = evaluate(&condition, &state);
876        assert!(
877            matches!(result, Ok(false)),
878            "Expected Ok(false), got {:?}",
879            result
880        );
881    }
882
883    #[test]
884    fn test_or_false_true() {
885        let condition = ConditionExpr::compound(
886            ConditionExpr::identifier("isDisabled"),
887            LogicalOperator::Or,
888            ConditionExpr::identifier("isEnabled"),
889        );
890        let state = test_json!({ "isDisabled": false, "isEnabled": true });
891
892        let result = evaluate(&condition, &state);
893        assert!(
894            matches!(result, Ok(true)),
895            "Expected Ok(true), got {:?}",
896            result
897        );
898    }
899
900    // === Complex Expressions ===
901
902    #[test]
903    fn test_appearance_and_actions() {
904        let condition = ConditionExpr::compound(
905            ConditionExpr::predicate("appearance", ComparisonOperator::Equal, "\"hub\""),
906            LogicalOperator::And,
907            ConditionExpr::identifier("actions.trailing"),
908        );
909        let state = test_json!({
910            "appearance": "hub",
911            "actions": { "trailing": true }
912        });
913
914        let result = evaluate(&condition, &state);
915        assert!(
916            matches!(result, Ok(true)),
917            "Expected Ok(true), got {:?}",
918            result
919        );
920    }
921
922    #[test]
923    fn test_negated_binary() {
924        let condition = ConditionExpr::negated(ConditionExpr::predicate(
925            "appearance",
926            ComparisonOperator::Equal,
927            "\"hub\"",
928        ));
929        let state = test_json!({ "appearance": "sidepanel" });
930
931        let result = evaluate(&condition, &state);
932        assert!(
933            matches!(result, Ok(true)),
934            "Expected Ok(true), got {:?}",
935            result
936        );
937    }
938}