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 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 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
298fn is_literal(s: &str) -> bool {
300 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
314fn parse_literal(s: &str) -> Result<Operand<'_>> {
316 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 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 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 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 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
360fn 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 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
377fn compare_ordered<F>(left: &Value, right: &Operand<'_>, compare_fn: F) -> Result<bool>
379where
380 F: Fn(&f64, &f64) -> bool,
381{
382 let left_num = extract_number(left)?;
384 let right_num = right.number()?;
385
386 Ok(compare_fn(&left_num, &right_num))
387}
388
389fn 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 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 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 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 ); }
485
486 #[test]
487 fn test_predicate() {
488 let condition = ConditionExpr::predicate("age", ComparisonOperator::GreaterThan, "18");
489
490 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 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 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 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 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 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 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 let mut condition = ConditionExpr::identifier("a");
610
611 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 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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}