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> {
47 evaluate_with_resolver(condition, |path| find_value_by_dotted_path_ref(path, state))
48}
49
50pub fn evaluate_with_resolver<'a, F>(condition: &ConditionExpr, resolver: F) -> Result<bool>
57where
58 F: Fn(&str) -> Option<Cow<'a, Value>>,
59{
60 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
83const INLINE_PENDING_EXPRS: usize = 8;
89
90struct 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
132fn 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 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 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 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 _ => {} }
170 }
171
172 (count, has_mixed)
173}
174
175fn 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 Cow::Owned(parse_literal(&predicate.right)?)
258 } else {
259 match resolver(&predicate.right) {
260 Some(val) => 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.as_ref())
273}
274
275fn is_literal(s: &str) -> bool {
277 s.starts_with('"')
282 || s.starts_with('\'')
283 || s.starts_with(|c: char| {
284 c.is_ascii_digit()
285 || (c == '-' && s.len() > 1 && s.chars().nth(1).is_some_and(|c| c.is_ascii_digit()))
286 })
287 || s == "true"
288 || s == "false"
289}
290
291fn parse_literal(s: &str) -> Result<Value> {
293 if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
295 let content = &s[1..s.len() - 1];
296 return Ok(Value::String(content.to_string()));
297 }
298
299 if s == "true" {
301 return Ok(Value::Bool(true));
302 }
303 if s == "false" {
304 return Ok(Value::Bool(false));
305 }
306
307 if let Ok(num) = s.parse::<i64>() {
309 return Ok(Value::Number(num.into()));
310 }
311
312 if let Ok(num) = s.parse::<f64>() {
313 match serde_json::Number::from_f64(num) {
315 Some(n) => return Ok(Value::Number(n)),
316 None => {
317 return Err(ExpressionError::TypeError(format!(
318 "Cannot convert {} to JSON number",
319 s
320 )))
321 }
322 }
323 }
324
325 Err(ExpressionError::TypeError(format!(
327 "Invalid literal: {}",
328 s
329 )))
330}
331
332fn compare_values(left: &Value, op: &ComparisonOperator, right: &Value) -> Result<bool> {
334 match op {
335 ComparisonOperator::Equal => Ok(left == right),
336 ComparisonOperator::NotEqual => Ok(left != right),
337
338 ComparisonOperator::GreaterThan => compare_ordered(left, right, |a, b| a > b),
340 ComparisonOperator::LessThan => compare_ordered(left, right, |a, b| a < b),
341 ComparisonOperator::GreaterThanOrEqual => compare_ordered(left, right, |a, b| a >= b),
342 ComparisonOperator::LessThanOrEqual => compare_ordered(left, right, |a, b| a <= b),
343 ComparisonOperator::Unspecified => Err(ExpressionError::Evaluation(
344 "Unspecified comparison operator".to_string(),
345 )),
346 }
347}
348
349fn compare_ordered<F>(left: &Value, right: &Value, compare_fn: F) -> Result<bool>
351where
352 F: Fn(&f64, &f64) -> bool,
353{
354 let left_num = extract_number(left)?;
356 let right_num = extract_number(right)?;
357
358 Ok(compare_fn(&left_num, &right_num))
359}
360
361fn extract_number(val: &Value) -> Result<f64> {
363 match val {
364 Value::Number(n) => {
365 if let Some(f) = n.as_f64() {
366 Ok(f)
367 } else {
368 Err(ExpressionError::TypeError(format!(
369 "Cannot convert number to f64: {:?}",
370 n
371 )))
372 }
373 }
374 Value::String(s) => match s.parse::<f64>() {
375 Ok(num) => Ok(num),
376 Err(_) => Err(ExpressionError::TypeError(format!(
377 "Cannot convert string to number: {}",
378 s
379 ))),
380 },
381 Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
382 _ => Err(ExpressionError::TypeError(format!(
383 "Cannot convert to number: {:?}",
384 val
385 ))),
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use std::borrow::Cow;
393 use webui_protocol::{ComparisonOperator, ConditionExpr, LogicalOperator};
394 use webui_test_utils::test_json;
395
396 #[test]
397 fn boolean_literals_do_not_resolve_state_keys() {
398 let state = test_json!({"true": false, "false": true});
399 assert_eq!(
400 evaluate(&ConditionExpr::identifier("true"), &state).unwrap(),
401 true
402 );
403 assert_eq!(
404 evaluate(&ConditionExpr::identifier("false"), &state).unwrap(),
405 false
406 );
407 }
408
409 #[test]
410 fn test_simple_identifier() {
411 let condition = ConditionExpr::identifier("flag");
413
414 let state = test_json!({
415 "flag": true
416 });
417
418 let result = evaluate(&condition, &state);
419 assert!(
420 matches!(result, Ok(true)),
421 "Expected Ok(true), got {:?}",
422 result
423 );
424
425 let condition = ConditionExpr::identifier("flag");
427
428 let state = test_json!({
429 "flag": false
430 });
431
432 let result = evaluate(&condition, &state);
433 assert!(
434 matches!(result, Ok(false)),
435 "Expected Ok(false), got {:?}",
436 result
437 );
438
439 let condition = ConditionExpr::identifier("name");
441
442 let state = test_json!({
443 "name": "John"
444 });
445
446 let result = evaluate(&condition, &state);
447 assert!(
448 matches!(result, Ok(true)),
449 "Expected Ok(true), got {:?}",
450 result
451 ); }
453
454 #[test]
455 fn test_predicate() {
456 let condition = ConditionExpr::predicate("age", ComparisonOperator::GreaterThan, "18");
457
458 let state = test_json!({
460 "age": 21
461 });
462
463 let result = evaluate(&condition, &state);
464 assert!(
465 matches!(result, Ok(true)),
466 "Expected Ok(true), got {:?}",
467 result
468 );
469
470 let state = test_json!({
472 "age": 16
473 });
474
475 let result = evaluate(&condition, &state);
476 assert!(
477 matches!(result, Ok(false)),
478 "Expected Ok(false), got {:?}",
479 result
480 );
481 }
482
483 #[test]
484 fn test_not_expression() {
485 let condition = ConditionExpr::negated(ConditionExpr::identifier("flag"));
486
487 let state = test_json!({
489 "flag": true
490 });
491
492 let result = evaluate(&condition, &state);
493 assert!(
494 matches!(result, Ok(false)),
495 "Expected Ok(false), got {:?}",
496 result
497 );
498
499 let state = test_json!({
501 "flag": false
502 });
503
504 let result = evaluate(&condition, &state);
505 assert!(
506 matches!(result, Ok(true)),
507 "Expected Ok(true), got {:?}",
508 result
509 );
510 }
511
512 #[test]
513 fn test_compound_expression() {
514 let condition = ConditionExpr::compound(
516 ConditionExpr::identifier("isAdmin"),
517 LogicalOperator::And,
518 ConditionExpr::identifier("isActive"),
519 );
520
521 let state = test_json!({
522 "isAdmin": true,
523 "isActive": true
524 });
525
526 let result = evaluate(&condition, &state);
527 assert!(
528 matches!(result, Ok(true)),
529 "Expected Ok(true), got {:?}",
530 result
531 );
532
533 let condition = ConditionExpr::compound(
535 ConditionExpr::identifier("isAdmin"),
536 LogicalOperator::Or,
537 ConditionExpr::identifier("isEditor"),
538 );
539
540 let state = test_json!({
541 "isAdmin": false,
542 "isEditor": true
543 });
544
545 let result = evaluate(&condition, &state);
546 assert!(
547 matches!(result, Ok(true)),
548 "Expected Ok(true), got {:?}",
549 result
550 );
551 }
552
553 #[test]
554 fn test_mixed_operators_error() {
555 let condition = ConditionExpr::compound(
557 ConditionExpr::compound(
558 ConditionExpr::identifier("a"),
559 LogicalOperator::And,
560 ConditionExpr::identifier("b"),
561 ),
562 LogicalOperator::Or,
563 ConditionExpr::identifier("c"),
564 );
565
566 let state = test_json!({
567 "a": true, "b": true, "c": true
568 });
569
570 let result = evaluate(&condition, &state);
571 assert!(matches!(result, Err(ExpressionError::MixedOperators)));
572 }
573
574 #[test]
575 fn test_too_many_operators_error() {
576 let mut condition = ConditionExpr::identifier("a");
578
579 for i in 0..6 {
581 condition = ConditionExpr::compound(
582 condition,
583 LogicalOperator::And,
584 ConditionExpr::identifier(format!("var{}", i)),
585 );
586 }
587
588 let state = test_json!({
589 "a": true, "var0": true, "var1": true,
590 "var2": true, "var3": true, "var4": true, "var5": true
591 });
592
593 let result = evaluate(&condition, &state);
594 assert!(matches!(result, Err(ExpressionError::TooManyOperators(_))));
595 }
596
597 #[test]
598 fn test_comparison_operators() {
599 let state = test_json!({
600 "value": 10
601 });
602
603 let ops = [
605 (ComparisonOperator::GreaterThan, "5", true),
606 (ComparisonOperator::GreaterThan, "15", false),
607 (ComparisonOperator::LessThan, "15", true),
608 (ComparisonOperator::LessThan, "5", false),
609 (ComparisonOperator::Equal, "10", true),
610 (ComparisonOperator::Equal, "11", false),
611 (ComparisonOperator::NotEqual, "11", true),
612 (ComparisonOperator::NotEqual, "10", false),
613 (ComparisonOperator::GreaterThanOrEqual, "10", true),
614 (ComparisonOperator::GreaterThanOrEqual, "11", false),
615 (ComparisonOperator::LessThanOrEqual, "10", true),
616 (ComparisonOperator::LessThanOrEqual, "9", false),
617 ];
618
619 for (op, right, expected) in ops.iter() {
620 let condition = ConditionExpr::predicate("value", *op, *right);
621
622 let result = evaluate(&condition, &state);
623 assert!(
624 matches!(result, Ok(val) if val == *expected),
625 "Failed for operator {:?} with right value {}: expected Ok({}), got {:?}",
626 op,
627 right,
628 expected,
629 result
630 );
631 }
632 }
633
634 #[test]
635 fn test_string_comparison() {
636 let state = test_json!({
637 "name": "John"
638 });
639
640 let condition = ConditionExpr::predicate("name", ComparisonOperator::Equal, "\"John\"");
642
643 let result = evaluate(&condition, &state);
644 assert!(
645 matches!(result, Ok(true)),
646 "Expected Ok(true), got {:?}",
647 result
648 );
649
650 let condition = ConditionExpr::predicate("name", ComparisonOperator::NotEqual, "\"Jane\"");
652
653 let result = evaluate(&condition, &state);
654 assert!(
655 matches!(result, Ok(true)),
656 "Expected Ok(true), got {:?}",
657 result
658 );
659 }
660
661 #[test]
662 fn test_dotted_path() {
663 let state = test_json!({
664 "user": {
665 "profile": {
666 "age": 25,
667 "name": "John"
668 }
669 }
670 });
671
672 let condition =
674 ConditionExpr::predicate("user.profile.age", ComparisonOperator::GreaterThan, "18");
675
676 let result = evaluate(&condition, &state);
677 assert!(
678 matches!(result, Ok(true)),
679 "Expected Ok(true), got {:?}",
680 result
681 );
682 }
683
684 #[test]
685 fn test_borrowed_resolver_value() {
686 let condition = ConditionExpr::identifier("flag");
687 let value = Value::Bool(true);
688
689 let result = evaluate_with_resolver(&condition, |path| {
690 if path == "flag" {
691 Some(Cow::Borrowed(&value))
692 } else {
693 None
694 }
695 });
696
697 assert!(matches!(result, Ok(true)));
698 }
699
700 #[test]
701 fn test_missing_value() {
702 let state = test_json!({
703 "user": {
704 "name": "John"
705 }
706 });
707
708 let condition = ConditionExpr::predicate("user.age", ComparisonOperator::GreaterThan, "18");
710
711 let result = evaluate(&condition, &state);
712 assert!(matches!(result, Err(ExpressionError::MissingValue(_))));
713 }
714
715 #[test]
718 fn test_missing_identifier_is_falsy_before_negation() {
719 let condition = ConditionExpr::identifier("notExist");
720 let negated = ConditionExpr::negated(condition.clone());
721 let state = test_json!({ "flag": true });
722
723 assert!(
724 matches!(evaluate(&condition, &state), Ok(false)),
725 "a missing identifier must be a falsy operand"
726 );
727 assert!(
728 matches!(evaluate(&negated, &state), Ok(true)),
729 "negating a missing identifier must evaluate to true"
730 );
731 }
732
733 #[test]
734 fn test_zero_field() {
735 let condition = ConditionExpr::identifier("zero");
736 let state = test_json!({ "zero": 0 });
737
738 let result = evaluate(&condition, &state);
739 assert!(
740 matches!(result, Ok(false)),
741 "Expected Ok(false), got {:?}",
742 result
743 );
744 }
745
746 #[test]
747 fn test_empty_string_field() {
748 let condition = ConditionExpr::identifier("empty");
749 let state = test_json!({ "empty": "" });
750
751 let result = evaluate(&condition, &state);
752 assert!(
753 matches!(result, Ok(false)),
754 "Expected Ok(false), got {:?}",
755 result
756 );
757 }
758
759 #[test]
760 fn test_nonempty_array() {
761 let condition = ConditionExpr::identifier("myList");
762 let state = test_json!({ "myList": [1, 2, 3] });
763
764 let result = evaluate(&condition, &state);
765 assert!(
766 matches!(result, Ok(true)),
767 "Expected Ok(true), got {:?}",
768 result
769 );
770 }
771
772 #[test]
773 fn test_empty_array() {
774 let condition = ConditionExpr::identifier("emptyList");
775 let state = test_json!({ "emptyList": [] });
776
777 let result = evaluate(&condition, &state);
778 assert!(
779 matches!(result, Ok(false)),
780 "Expected Ok(false), got {:?}",
781 result
782 );
783 }
784
785 #[test]
788 fn test_dotted_path_deep() {
789 let condition = ConditionExpr::identifier("outer.nested.deep.value");
790 let state = test_json!({
791 "outer": { "nested": { "deep": { "value": true } } }
792 });
793
794 let result = evaluate(&condition, &state);
795 assert!(
796 matches!(result, Ok(true)),
797 "Expected Ok(true), got {:?}",
798 result
799 );
800 }
801
802 #[test]
805 fn test_string_eq() {
806 let condition =
807 ConditionExpr::predicate("appearance", ComparisonOperator::Equal, "\"hub\"");
808 let state = test_json!({ "appearance": "hub" });
809
810 let result = evaluate(&condition, &state);
811 assert!(
812 matches!(result, Ok(true)),
813 "Expected Ok(true), got {:?}",
814 result
815 );
816 }
817
818 #[test]
819 fn test_string_ne() {
820 let condition =
821 ConditionExpr::predicate("appearance", ComparisonOperator::NotEqual, "\"full-page\"");
822 let state = test_json!({ "appearance": "hub" });
823
824 let result = evaluate(&condition, &state);
825 assert!(
826 matches!(result, Ok(true)),
827 "Expected Ok(true), got {:?}",
828 result
829 );
830 }
831
832 #[test]
833 fn test_number_eq() {
834 let condition = ConditionExpr::predicate("x", ComparisonOperator::Equal, "5");
835 let state = test_json!({ "x": 5 });
836
837 let result = evaluate(&condition, &state);
838 assert!(
839 matches!(result, Ok(true)),
840 "Expected Ok(true), got {:?}",
841 result
842 );
843 }
844
845 #[test]
846 fn test_less_than() {
847 let condition = ConditionExpr::predicate("x", ComparisonOperator::LessThan, "y");
848 let state = test_json!({ "x": 5, "y": 10 });
849
850 let result = evaluate(&condition, &state);
851 assert!(
852 matches!(result, Ok(true)),
853 "Expected Ok(true), got {:?}",
854 result
855 );
856 }
857
858 #[test]
859 fn test_greater_than() {
860 let condition = ConditionExpr::predicate("y", ComparisonOperator::GreaterThan, "x");
861 let state = test_json!({ "x": 5, "y": 10 });
862
863 let result = evaluate(&condition, &state);
864 assert!(
865 matches!(result, Ok(true)),
866 "Expected Ok(true), got {:?}",
867 result
868 );
869 }
870
871 #[test]
872 fn test_less_equal() {
873 let condition = ConditionExpr::predicate("x", ComparisonOperator::LessThanOrEqual, "5");
874 let state = test_json!({ "x": 5 });
875
876 let result = evaluate(&condition, &state);
877 assert!(
878 matches!(result, Ok(true)),
879 "Expected Ok(true), got {:?}",
880 result
881 );
882 }
883
884 #[test]
885 fn test_greater_equal() {
886 let condition = ConditionExpr::predicate("y", ComparisonOperator::GreaterThanOrEqual, "10");
887 let state = test_json!({ "y": 10 });
888
889 let result = evaluate(&condition, &state);
890 assert!(
891 matches!(result, Ok(true)),
892 "Expected Ok(true), got {:?}",
893 result
894 );
895 }
896
897 #[test]
898 fn test_nested_eq() {
899 let condition = ConditionExpr::predicate("outer.inner", ComparisonOperator::Equal, "42");
900 let state = test_json!({ "outer": { "inner": 42 } });
901
902 let result = evaluate(&condition, &state);
903 assert!(
904 matches!(result, Ok(true)),
905 "Expected Ok(true), got {:?}",
906 result
907 );
908 }
909
910 #[test]
913 fn test_and_true_true() {
914 let condition = ConditionExpr::compound(
915 ConditionExpr::identifier("isEnabled"),
916 LogicalOperator::And,
917 ConditionExpr::identifier("x"),
918 );
919 let state = test_json!({ "isEnabled": true, "x": 5 });
920
921 let result = evaluate(&condition, &state);
922 assert!(
923 matches!(result, Ok(true)),
924 "Expected Ok(true), got {:?}",
925 result
926 );
927 }
928
929 #[test]
930 fn test_and_true_false() {
931 let condition = ConditionExpr::compound(
932 ConditionExpr::identifier("isEnabled"),
933 LogicalOperator::And,
934 ConditionExpr::identifier("isDisabled"),
935 );
936 let state = test_json!({ "isEnabled": true, "isDisabled": false });
937
938 let result = evaluate(&condition, &state);
939 assert!(
940 matches!(result, Ok(false)),
941 "Expected Ok(false), got {:?}",
942 result
943 );
944 }
945
946 #[test]
947 fn test_or_false_false() {
948 let condition = ConditionExpr::compound(
949 ConditionExpr::identifier("isDisabled"),
950 LogicalOperator::Or,
951 ConditionExpr::identifier("zero"),
952 );
953 let state = test_json!({ "isDisabled": false, "zero": 0 });
954
955 let result = evaluate(&condition, &state);
956 assert!(
957 matches!(result, Ok(false)),
958 "Expected Ok(false), got {:?}",
959 result
960 );
961 }
962
963 #[test]
964 fn test_or_false_true() {
965 let condition = ConditionExpr::compound(
966 ConditionExpr::identifier("isDisabled"),
967 LogicalOperator::Or,
968 ConditionExpr::identifier("isEnabled"),
969 );
970 let state = test_json!({ "isDisabled": false, "isEnabled": true });
971
972 let result = evaluate(&condition, &state);
973 assert!(
974 matches!(result, Ok(true)),
975 "Expected Ok(true), got {:?}",
976 result
977 );
978 }
979
980 #[test]
983 fn test_appearance_and_actions() {
984 let condition = ConditionExpr::compound(
985 ConditionExpr::predicate("appearance", ComparisonOperator::Equal, "\"hub\""),
986 LogicalOperator::And,
987 ConditionExpr::identifier("actions.trailing"),
988 );
989 let state = test_json!({
990 "appearance": "hub",
991 "actions": { "trailing": true }
992 });
993
994 let result = evaluate(&condition, &state);
995 assert!(
996 matches!(result, Ok(true)),
997 "Expected Ok(true), got {:?}",
998 result
999 );
1000 }
1001
1002 #[test]
1003 fn test_negated_binary() {
1004 let condition = ConditionExpr::negated(ConditionExpr::predicate(
1005 "appearance",
1006 ComparisonOperator::Equal,
1007 "\"hub\"",
1008 ));
1009 let state = test_json!({ "appearance": "sidepanel" });
1010
1011 let result = evaluate(&condition, &state);
1012 assert!(
1013 matches!(result, Ok(true)),
1014 "Expected Ok(true), got {:?}",
1015 result
1016 );
1017 }
1018
1019 #[test]
1022 fn test_single_term_matches_operator_guard_semantics() {
1023 let state = test_json!({
1024 "flag": true,
1025 "off": false,
1026 "count": 0,
1027 "name": "Alice",
1028 "user": { "age": 25 }
1029 });
1030
1031 let cases: [(ConditionExpr, bool); 6] = [
1034 (ConditionExpr::identifier("flag"), true),
1035 (ConditionExpr::identifier("off"), false),
1036 (ConditionExpr::identifier("count"), false),
1037 (ConditionExpr::identifier("name"), true),
1038 (
1039 ConditionExpr::predicate("user.age", ComparisonOperator::GreaterThan, "18"),
1040 true,
1041 ),
1042 (
1043 ConditionExpr::predicate("name", ComparisonOperator::Equal, "'Bob'"),
1044 false,
1045 ),
1046 ];
1047
1048 for (condition, expected) in cases {
1049 assert_eq!(
1050 evaluate(&condition, &state).ok(),
1051 Some(expected),
1052 "single-term condition {:?} must evaluate to {}",
1053 condition,
1054 expected
1055 );
1056 }
1057 }
1058
1059 #[test]
1060 fn test_single_term_predicate_still_reports_missing_value() {
1061 let state = test_json!({ "user": { "name": "John" } });
1062 let condition = ConditionExpr::predicate("user.age", ComparisonOperator::LessThan, "18");
1063
1064 assert!(matches!(
1065 evaluate(&condition, &state),
1066 Err(ExpressionError::MissingValue(_))
1067 ));
1068 }
1069
1070 #[test]
1071 fn test_negated_tree_still_enforces_operator_limit() {
1072 let mut inner = ConditionExpr::identifier("a");
1075 for i in 0..6 {
1076 inner = ConditionExpr::compound(
1077 inner,
1078 LogicalOperator::And,
1079 ConditionExpr::identifier(format!("var{}", i)),
1080 );
1081 }
1082 let condition = ConditionExpr::negated(inner);
1083
1084 let state = test_json!({
1085 "a": true, "var0": true, "var1": true,
1086 "var2": true, "var3": true, "var4": true, "var5": true
1087 });
1088
1089 assert!(matches!(
1090 evaluate(&condition, &state),
1091 Err(ExpressionError::TooManyOperators(6))
1092 ));
1093 }
1094
1095 #[test]
1096 fn test_operator_count_is_exact_beyond_inline_stack() {
1097 let mut condition = ConditionExpr::identifier("a");
1101 for i in 0..12 {
1102 condition = ConditionExpr::compound(
1103 condition,
1104 LogicalOperator::Or,
1105 ConditionExpr::identifier(format!("var{}", i)),
1106 );
1107 }
1108
1109 let state = test_json!({ "a": true });
1110
1111 assert!(matches!(
1112 evaluate(&condition, &state),
1113 Err(ExpressionError::TooManyOperators(12))
1114 ));
1115 }
1116
1117 #[test]
1118 fn test_negated_tree_still_rejects_mixed_operators() {
1119 let condition = ConditionExpr::negated(ConditionExpr::compound(
1120 ConditionExpr::compound(
1121 ConditionExpr::identifier("a"),
1122 LogicalOperator::And,
1123 ConditionExpr::identifier("b"),
1124 ),
1125 LogicalOperator::Or,
1126 ConditionExpr::identifier("c"),
1127 ));
1128
1129 let state = test_json!({ "a": true, "b": true, "c": true });
1130
1131 assert!(matches!(
1132 evaluate(&condition, &state),
1133 Err(ExpressionError::MixedOperators)
1134 ));
1135 }
1136
1137 #[test]
1138 fn test_single_term_fast_path_uses_same_resolver_contract() {
1139 let value = Value::String("active".to_string());
1140 let lookups = std::cell::Cell::new(0usize);
1141
1142 let condition = ConditionExpr::predicate("status", ComparisonOperator::Equal, "'active'");
1143 let result = evaluate_with_resolver(&condition, |path| {
1144 lookups.set(lookups.get() + 1);
1145 if path == "status" {
1146 Some(Cow::Borrowed(&value))
1147 } else {
1148 None
1149 }
1150 });
1151
1152 assert!(matches!(result, Ok(true)));
1153 assert_eq!(
1154 lookups.get(),
1155 1,
1156 "a single-term predicate must resolve exactly one path"
1157 );
1158 }
1159}