flagsmith_flag_engine/engine_eval/
segment_evaluator.rs

1use super::context::{
2    Condition, ConditionOperator, EngineEvaluationContext, SegmentContext, SegmentRule,
3    SegmentRuleType,
4};
5use crate::types::FlagsmithValue;
6use crate::utils::hashing;
7use regex::Regex;
8use semver::Version;
9use serde_json_path::JsonPath;
10
11/// Determines if the given evaluation context matches the segment rules
12pub fn is_context_in_segment(ec: &EngineEvaluationContext, segment: &SegmentContext) -> bool {
13    if segment.rules.is_empty() {
14        return false;
15    }
16
17    // All top-level rules must match
18    for rule in &segment.rules {
19        if !context_matches_segment_rule(ec, rule, &segment.key) {
20            return false;
21        }
22    }
23
24    true
25}
26
27/// Checks if the context matches a segment rule
28fn context_matches_segment_rule(
29    ec: &EngineEvaluationContext,
30    rule: &SegmentRule,
31    segment_key: &str,
32) -> bool {
33    // Check conditions if present
34    if !rule.conditions.is_empty()
35        && !matches_conditions_by_rule_type(ec, &rule.conditions, &rule.rule_type, segment_key)
36    {
37        return false;
38    }
39
40    // Check nested rules
41    for nested_rule in &rule.rules {
42        if !context_matches_segment_rule(ec, nested_rule, segment_key) {
43            return false;
44        }
45    }
46
47    true
48}
49
50/// Checks if conditions match according to the rule type
51fn matches_conditions_by_rule_type(
52    ec: &EngineEvaluationContext,
53    conditions: &[Condition],
54    rule_type: &SegmentRuleType,
55    segment_key: &str,
56) -> bool {
57    for condition in conditions {
58        let condition_matches = context_matches_condition(ec, condition, segment_key);
59
60        match rule_type {
61            SegmentRuleType::All => {
62                if !condition_matches {
63                    return false; // Short-circuit: ALL requires all conditions to match
64                }
65            }
66            SegmentRuleType::None => {
67                if condition_matches {
68                    return false; // Short-circuit: NONE requires no conditions to match
69                }
70            }
71            SegmentRuleType::Any => {
72                if condition_matches {
73                    return true; // Short-circuit: ANY requires at least one condition to match
74                }
75            }
76        }
77    }
78
79    // If we reach here: ALL/NONE passed all checks, ANY found no matches
80    *rule_type != SegmentRuleType::Any
81}
82
83/// Checks if the context matches a specific condition
84fn context_matches_condition(
85    ec: &EngineEvaluationContext,
86    condition: &Condition,
87    segment_key: &str,
88) -> bool {
89    let context_value = if !condition.property.is_empty() {
90        get_context_value(ec, &condition.property)
91    } else {
92        None
93    };
94
95    match condition.operator {
96        ConditionOperator::PercentageSplit => {
97            match_percentage_split(ec, condition, segment_key, context_value.as_ref())
98        }
99        ConditionOperator::In => match_in_operator(condition, context_value.as_ref()),
100        ConditionOperator::IsNotSet => context_value.is_none(),
101        ConditionOperator::IsSet => context_value.is_some(),
102        _ => {
103            if let Some(ref ctx_val) = context_value {
104                parse_and_match(&condition.operator, ctx_val, &condition.value.as_string())
105            } else {
106                false
107            }
108        }
109    }
110}
111
112/// Gets a value from the context by property name or JSONPath
113fn get_context_value(ec: &EngineEvaluationContext, property: &str) -> Option<FlagsmithValue> {
114    // If property starts with $., try to parse it as a JSONPath expression
115    if property.starts_with("$.") {
116        if let Some(value) = get_value_from_jsonpath(ec, property) {
117            return Some(value);
118        }
119        // If JSONPath parsing fails, fall through to treat it as a trait name
120    }
121
122    // Check traits by property name
123    if let Some(ref identity) = ec.identity {
124        if let Some(trait_value) = identity.traits.get(property) {
125            return Some(trait_value.clone());
126        }
127    }
128
129    None
130}
131
132/// Gets a value from the context using JSONPath
133fn get_value_from_jsonpath(ec: &EngineEvaluationContext, path: &str) -> Option<FlagsmithValue> {
134    // Parse the JSONPath expression
135    let json_path = match JsonPath::parse(path) {
136        Ok(p) => p,
137        Err(_) => return None,
138    };
139
140    // Serialize the context to JSON
141    let context_json = match serde_json::to_value(ec) {
142        Ok(v) => v,
143        Err(_) => return None,
144    };
145
146    // Query the JSON using the path
147    let result = json_path.query(&context_json);
148
149    // Get the first match (if any)
150    let node_list = result.all();
151    if node_list.is_empty() {
152        return None;
153    }
154
155    // Extract the value from the first match
156    let value = node_list[0];
157
158    // Convert to FlagsmithValue based on the JSON type
159    match value {
160        serde_json::Value::String(s) => Some(FlagsmithValue {
161            value: s.clone(),
162            value_type: crate::types::FlagsmithValueType::String,
163        }),
164        serde_json::Value::Number(n) => {
165            if n.is_f64() {
166                Some(FlagsmithValue {
167                    value: n.to_string(),
168                    value_type: crate::types::FlagsmithValueType::Float,
169                })
170            } else {
171                Some(FlagsmithValue {
172                    value: n.to_string(),
173                    value_type: crate::types::FlagsmithValueType::Integer,
174                })
175            }
176        }
177        serde_json::Value::Bool(b) => Some(FlagsmithValue {
178            value: b.to_string(),
179            value_type: crate::types::FlagsmithValueType::Bool,
180        }),
181        _ => None,
182    }
183}
184
185/// Matches percentage split condition
186fn match_percentage_split(
187    ec: &EngineEvaluationContext,
188    condition: &Condition,
189    segment_key: &str,
190    context_value: Option<&FlagsmithValue>,
191) -> bool {
192    let float_value = match condition.value.as_string().parse::<f64>() {
193        Ok(v) => v,
194        Err(_) => return false,
195    };
196
197    // Build object IDs based on context
198    let context_str = context_value.map(|v| v.value.clone());
199    let object_ids: Vec<&str> = if let Some(ref ctx_str) = context_str {
200        vec![segment_key, ctx_str.as_str()]
201    } else if let Some(ref identity) = ec.identity {
202        vec![segment_key, &identity.key]
203    } else {
204        return false;
205    };
206
207    let hash_percentage = hashing::get_hashed_percentage_for_object_ids(object_ids, 1);
208    (hash_percentage as f64) <= float_value
209}
210
211/// Matches IN operator
212fn match_in_operator(condition: &Condition, context_value: Option<&FlagsmithValue>) -> bool {
213    if context_value.is_none() {
214        return false;
215    }
216
217    let ctx_value = context_value.unwrap();
218
219    // IN operator only works with string values, not booleans
220    use crate::types::FlagsmithValueType;
221    if ctx_value.value_type == FlagsmithValueType::Bool {
222        return false;
223    }
224
225    let trait_value = &ctx_value.value;
226
227    // Use the ConditionValue's contains_string method for simple string matching
228    condition.value.contains_string(trait_value)
229}
230
231/// Parses and matches values based on the operator using type-aware strategy
232fn parse_and_match(
233    operator: &ConditionOperator,
234    trait_value: &FlagsmithValue,
235    condition_value: &str,
236) -> bool {
237    use crate::types::FlagsmithValueType;
238
239    // Handle special operators that work across all types
240    match operator {
241        ConditionOperator::Modulo => return evaluate_modulo(&trait_value.value, condition_value),
242        ConditionOperator::Regex => return evaluate_regex(&trait_value.value, condition_value),
243        ConditionOperator::Contains => return trait_value.value.contains(condition_value),
244        ConditionOperator::NotContains => return !trait_value.value.contains(condition_value),
245        _ => {}
246    }
247
248    // Use type-aware strategy based on trait value type
249    match trait_value.value_type {
250        FlagsmithValueType::Bool => compare_bool(operator, &trait_value.value, condition_value),
251        FlagsmithValueType::Integer => {
252            compare_integer(operator, &trait_value.value, condition_value)
253        }
254        FlagsmithValueType::Float => compare_float(operator, &trait_value.value, condition_value),
255        FlagsmithValueType::String => compare_string(operator, &trait_value.value, condition_value),
256        _ => false,
257    }
258}
259
260/// Parses a boolean string value with optional integer conversion
261/// NOTE: Historical engine behavior - only "1" is treated as true, "0" is NOT treated as false
262fn parse_bool(s: &str, allow_int_conversion: bool) -> Option<bool> {
263    match s.to_lowercase().as_str() {
264        "true" => Some(true),
265        "1" if allow_int_conversion => Some(true),
266        "false" => Some(false),
267        _ => None,
268    }
269}
270
271/// Compares boolean values
272fn compare_bool(operator: &ConditionOperator, trait_value: &str, condition_value: &str) -> bool {
273    if let (Some(b1), Some(b2)) = (
274        parse_bool(trait_value, true),
275        parse_bool(condition_value, true),
276    ) {
277        match operator {
278            ConditionOperator::Equal => b1 == b2,
279            ConditionOperator::NotEqual => b1 != b2,
280            _ => false,
281        }
282    } else {
283        false
284    }
285}
286
287/// Compares integer values
288fn compare_integer(operator: &ConditionOperator, trait_value: &str, condition_value: &str) -> bool {
289    if let (Ok(i1), Ok(i2)) = (trait_value.parse::<i64>(), condition_value.parse::<i64>()) {
290        dispatch_operator(operator, i1, i2)
291    } else {
292        false
293    }
294}
295
296/// Compares float values
297fn compare_float(operator: &ConditionOperator, trait_value: &str, condition_value: &str) -> bool {
298    if let (Ok(f1), Ok(f2)) = (trait_value.parse::<f64>(), condition_value.parse::<f64>()) {
299        dispatch_operator(operator, f1, f2)
300    } else {
301        false
302    }
303}
304
305/// Compares string values, with special handling for semver
306fn compare_string(operator: &ConditionOperator, trait_value: &str, condition_value: &str) -> bool {
307    // Check for semver comparison
308    if let Some(version_str) = condition_value.strip_suffix(":semver") {
309        if let Ok(condition_version) = Version::parse(version_str) {
310            return evaluate_semver(operator, trait_value, &condition_version);
311        }
312        return false;
313    }
314
315    // Try parsing as boolean for string types (strict - no integer conversion)
316    if let (Some(b1), Some(b2)) = (
317        parse_bool(trait_value, false),
318        parse_bool(condition_value, false),
319    ) {
320        return match operator {
321            ConditionOperator::Equal => b1 == b2,
322            ConditionOperator::NotEqual => b1 != b2,
323            _ => false,
324        };
325    }
326
327    // Try parsing as integer
328    if let (Ok(i1), Ok(i2)) = (trait_value.parse::<i64>(), condition_value.parse::<i64>()) {
329        return dispatch_operator(operator, i1, i2);
330    }
331
332    // Try parsing as float
333    if let (Ok(f1), Ok(f2)) = (trait_value.parse::<f64>(), condition_value.parse::<f64>()) {
334        return dispatch_operator(operator, f1, f2);
335    }
336
337    // Fall back to string comparison
338    dispatch_operator(operator, trait_value, condition_value)
339}
340
341/// Dispatches the operator to the appropriate comparison function
342fn dispatch_operator<T: PartialOrd + PartialEq>(
343    operator: &ConditionOperator,
344    v1: T,
345    v2: T,
346) -> bool {
347    match operator {
348        ConditionOperator::Equal => v1 == v2,
349        ConditionOperator::NotEqual => v1 != v2,
350        ConditionOperator::GreaterThan => v1 > v2,
351        ConditionOperator::LessThan => v1 < v2,
352        ConditionOperator::GreaterThanInclusive => v1 >= v2,
353        ConditionOperator::LessThanInclusive => v1 <= v2,
354        _ => false,
355    }
356}
357
358/// Evaluates regex matching
359fn evaluate_regex(trait_value: &str, condition_value: &str) -> bool {
360    if let Ok(re) = Regex::new(condition_value) {
361        return re.is_match(trait_value);
362    }
363    false
364}
365
366/// Evaluates modulo operation
367fn evaluate_modulo(trait_value: &str, condition_value: &str) -> bool {
368    let values: Vec<&str> = condition_value.split('|').collect();
369    if values.len() != 2 {
370        return false;
371    }
372
373    let divisor = match values[0].parse::<f64>() {
374        Ok(v) => v,
375        Err(_) => return false,
376    };
377
378    let remainder = match values[1].parse::<f64>() {
379        Ok(v) => v,
380        Err(_) => return false,
381    };
382
383    let trait_value_float = match trait_value.parse::<f64>() {
384        Ok(v) => v,
385        Err(_) => return false,
386    };
387
388    // Use epsilon comparison for float equality to handle precision errors
389    const EPSILON: f64 = 1e-10;
390    ((trait_value_float % divisor) - remainder).abs() < EPSILON
391}
392
393/// Evaluates semantic version comparisons
394fn evaluate_semver(
395    operator: &ConditionOperator,
396    trait_value: &str,
397    condition_version: &Version,
398) -> bool {
399    let trait_version = match Version::parse(trait_value) {
400        Ok(v) => v,
401        Err(_) => return false,
402    };
403
404    match operator {
405        ConditionOperator::Equal => trait_version == *condition_version,
406        ConditionOperator::NotEqual => trait_version != *condition_version,
407        ConditionOperator::GreaterThan => trait_version > *condition_version,
408        ConditionOperator::LessThan => trait_version < *condition_version,
409        ConditionOperator::GreaterThanInclusive => trait_version >= *condition_version,
410        ConditionOperator::LessThanInclusive => trait_version <= *condition_version,
411        _ => false,
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_dispatch_operator_integers() {
421        assert!(dispatch_operator(&ConditionOperator::Equal, 5, 5));
422        assert!(!dispatch_operator(&ConditionOperator::Equal, 5, 6));
423        assert!(dispatch_operator(&ConditionOperator::GreaterThan, 6, 5));
424        assert!(!dispatch_operator(&ConditionOperator::GreaterThan, 5, 6));
425    }
426
427    #[test]
428    fn test_evaluate_modulo() {
429        assert!(evaluate_modulo("2", "2|0"));
430        assert!(!evaluate_modulo("3", "2|0"));
431        assert!(evaluate_modulo("35.0", "4|3"));
432    }
433}