fix: respect rule type for sub-rules in segment evaluation#36
fix: respect rule type for sub-rules in segment evaluation#36
Conversation
Sub-rules were always evaluated with ALL logic, ignoring the parent rule's type field (ANY/ALL/NONE). Now uses type-aware matching for sub-rules in both the new context-based evaluator and the legacy evaluator. Also fixes the NONE rule type in the legacy evaluator which was hardcoded to always return true. Bumps engine-test-data from v3.5.0 to v3.7.0 which includes test cases for this fix. Equivalent fix to flagsmith-engine#313. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| matches_sub_rules_by_rule_type(ec, &rule.rules, &rule.rule_type, segment_key) | ||
| } | ||
|
|
||
| fn matches_sub_rules_by_rule_type( |
There was a problem hiding this comment.
matches_sub_rules_by_rule_type in src/engine_eval/segment_evaluator.rs and the new sub-rule block in src/segments/evaluator.rs implement the same operation with two different idioms. The
legacy evaluator uses iterator combinators directly:
match rule.segment_rule_type.as_str() {
constants::ANY_RULE => sub_rules_iterator.any(|result| result),
constants::ALL_RULE => sub_rules_iterator.all(|result| result),
constants::NONE_RULE => !sub_rules_iterator.any(|result| result),
_ => false,
}
…while the engine_eval version uses an imperative loop with *rule_type != SegmentRuleType::Any as the fallthrough. That works (All != Any → true, None != Any → true, Any != Any → false), but
it forces the reader to derive that "!= Any" is the right default for both All and None.
Suggest matching the legacy form so both evaluators read identically:
fn matches_sub_rules_by_rule_type(
ec: &EngineEvaluationContext,
sub_rules: &[SegmentRule],
rule_type: &SegmentRuleType,
segment_key: &str,
) -> bool {
let mut results = sub_rules
.iter()
.map(|sr| context_matches_segment_rule(ec, sr, segment_key));
match rule_type {
SegmentRuleType::All => results.all(|r| r),
SegmentRuleType::Any => results.any(|r| r),
SegmentRuleType::None => !results.any(|r| r),
}
}
Same short-circuiting, no clever fallthrough, and the two evaluators stay in sync — which matters since they're meant to encode identical semantics.
| let mut sub_rules_iterator = rule.rules.iter().map(|sub_rule| { | ||
| traits_match_segment_rule(&identity_traits, sub_rule.as_ref(), segment_id, identity_id) | ||
| }); | ||
| match rule.segment_rule_type.as_str() { |
There was a problem hiding this comment.
In src/segments/evaluator.rs, traits_match_segment_rule now matches on rule.segment_rule_type.as_str() twice — once over rules_iterator for conditions, then again over sub_rules_iterator for
sub-rules. The two arms are identical:
constants::ANY_RULE => iter.any(|r| r),
constants::ALL_RULE => iter.all(|r| r),
constants::NONE_RULE => !iter.any(|r| r),
_ => false,
Minor, but worth lifting into a small helper so the combine logic lives in one place. Naming it matches_by_rule_type to fit the existing matches_conditions_by_rule_type /
matches_sub_rules_by_rule_type convention:
fn matches_by_rule_type(rule_type: &str, mut results: impl Iterator<Item = bool>) -> bool {
match rule_type {
constants::ANY_RULE => results.any(|r| r),
constants::ALL_RULE => results.all(|r| r),
constants::NONE_RULE => !results.any(|r| r),
_ => false,
}
}
Then the body becomes:
if !matches_by_rule_type(rule.segment_rule_type.as_str(), rules_iterator) {
return false;
}
if rule.rules.is_empty() {
return true;
}
matches_by_rule_type(rule.segment_rule_type.as_str(), sub_rules_iterator)
Same short-circuiting, one source of truth for the ANY/ALL/NONE mapping, and if a new rule type is added later there's only one place to update
Address review feedback: - Replace imperative loop in matches_sub_rules_by_rule_type with iterator combinators (all/any/!any) matching the legacy form - Extract matches_by_rule_type helper in legacy evaluator to DRY up the duplicated ANY/ALL/NONE match blocks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
engine_eval/segment_evaluator.rs): sub-rules now respect the parent rule's type (ANY/ALL/NONE) instead of hardcoding ALLsegments/evaluator.rs): same sub-rules fix + fixed NONE rule type for conditions (was hardcoded totrue)Equivalent fix to flagsmith-engine#313.
Test plan