1use 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#[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
42pub fn evaluate(condition: &ConditionExpr, state: &Value) -> Result<bool> {
44 evaluate_with_resolver(condition, |path| find_value_by_dotted_path_ref(path, state))
45}
46
47pub 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
70fn 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 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 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 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 _ => {} }
108 }
109
110 (count, has_mixed)
111}
112
113fn 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
211fn is_literal(s: &str) -> bool {
213 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
227fn parse_literal(s: &str) -> Result<Value> {
229 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 if s == "true" {
237 return Ok(Value::Bool(true));
238 }
239 if s == "false" {
240 return Ok(Value::Bool(false));
241 }
242
243 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 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 Err(ExpressionError::TypeError(format!(
263 "Invalid literal: {}",
264 s
265 )))
266}
267
268fn 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 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
285fn compare_ordered<F>(left: &Value, right: &Value, compare_fn: F) -> Result<bool>
287where
288 F: Fn(&f64, &f64) -> bool,
289{
290 let left_num = extract_number(left)?;
292 let right_num = extract_number(right)?;
293
294 Ok(compare_fn(&left_num, &right_num))
295}
296
297fn 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 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 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 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 ); }
376
377 #[test]
378 fn test_predicate() {
379 let condition = ConditionExpr::predicate("age", ComparisonOperator::GreaterThan, "18");
380
381 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 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 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 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 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 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 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 let mut condition = ConditionExpr::identifier("a");
501
502 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 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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}