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
185fn match_percentage_split(
186    ec: &EngineEvaluationContext,
187    condition: &Condition,
188    segment_key: &str,
189    context_value: Option<&FlagsmithValue>,
190) -> bool {
191    let float_value = match condition.value.as_string().parse::<f64>() {
192        Ok(v) => v,
193        Err(_) => return false,
194    };
195
196    let split_key: Option<String> = if condition.property.is_empty() {
197        ec.identity.as_ref().map(|id| id.key.clone())
198    } else {
199        context_value.map(|v| v.value.clone())
200    };
201
202    let split_key = match split_key {
203        Some(key) => key,
204        None => return false,
205    };
206
207    let object_ids: Vec<&str> = vec![segment_key, &split_key];
208    let hash_percentage = hashing::get_hashed_percentage_for_object_ids(object_ids, 1);
209    (hash_percentage as f64) <= float_value
210}
211
212/// Matches IN operator
213fn match_in_operator(condition: &Condition, context_value: Option<&FlagsmithValue>) -> bool {
214    if context_value.is_none() {
215        return false;
216    }
217
218    let ctx_value = context_value.unwrap();
219
220    // IN operator only works with string values, not booleans
221    use crate::types::FlagsmithValueType;
222    if ctx_value.value_type == FlagsmithValueType::Bool {
223        return false;
224    }
225
226    let trait_value = &ctx_value.value;
227
228    // Use the ConditionValue's contains_string method for simple string matching
229    condition.value.contains_string(trait_value)
230}
231
232/// Parses and matches values based on the operator using type-aware strategy
233fn parse_and_match(
234    operator: &ConditionOperator,
235    trait_value: &FlagsmithValue,
236    condition_value: &str,
237) -> bool {
238    use crate::types::FlagsmithValueType;
239
240    // Handle special operators that work across all types
241    match operator {
242        ConditionOperator::Modulo => return evaluate_modulo(&trait_value.value, condition_value),
243        ConditionOperator::Regex => return evaluate_regex(&trait_value.value, condition_value),
244        ConditionOperator::Contains => return trait_value.value.contains(condition_value),
245        ConditionOperator::NotContains => return !trait_value.value.contains(condition_value),
246        _ => {}
247    }
248
249    // Use type-aware strategy based on trait value type
250    match trait_value.value_type {
251        FlagsmithValueType::Bool => compare_bool(operator, &trait_value.value, condition_value),
252        FlagsmithValueType::Integer => {
253            compare_integer(operator, &trait_value.value, condition_value)
254        }
255        FlagsmithValueType::Float => compare_float(operator, &trait_value.value, condition_value),
256        FlagsmithValueType::String => compare_string(operator, &trait_value.value, condition_value),
257        _ => false,
258    }
259}
260
261/// Parses a boolean string value with optional integer conversion
262/// NOTE: Historical engine behavior - only "1" is treated as true, "0" is NOT treated as false
263fn parse_bool(s: &str, allow_int_conversion: bool) -> Option<bool> {
264    match s.to_lowercase().as_str() {
265        "true" => Some(true),
266        "1" if allow_int_conversion => Some(true),
267        "false" => Some(false),
268        _ => None,
269    }
270}
271
272/// Compares boolean values
273fn compare_bool(operator: &ConditionOperator, trait_value: &str, condition_value: &str) -> bool {
274    if let (Some(b1), Some(b2)) = (
275        parse_bool(trait_value, true),
276        parse_bool(condition_value, true),
277    ) {
278        match operator {
279            ConditionOperator::Equal => b1 == b2,
280            ConditionOperator::NotEqual => b1 != b2,
281            _ => false,
282        }
283    } else {
284        false
285    }
286}
287
288/// Compares integer values
289fn compare_integer(operator: &ConditionOperator, trait_value: &str, condition_value: &str) -> bool {
290    if let (Ok(i1), Ok(i2)) = (trait_value.parse::<i64>(), condition_value.parse::<i64>()) {
291        dispatch_operator(operator, i1, i2)
292    } else {
293        false
294    }
295}
296
297/// Compares float values
298fn compare_float(operator: &ConditionOperator, trait_value: &str, condition_value: &str) -> bool {
299    if let (Ok(f1), Ok(f2)) = (trait_value.parse::<f64>(), condition_value.parse::<f64>()) {
300        dispatch_operator(operator, f1, f2)
301    } else {
302        false
303    }
304}
305
306/// Compares string values, with special handling for semver
307fn compare_string(operator: &ConditionOperator, trait_value: &str, condition_value: &str) -> bool {
308    // Check for semver comparison
309    if let Some(version_str) = condition_value.strip_suffix(":semver") {
310        if let Ok(condition_version) = Version::parse(version_str) {
311            return evaluate_semver(operator, trait_value, &condition_version);
312        }
313        return false;
314    }
315
316    // Try parsing as boolean for string types (strict - no integer conversion)
317    if let (Some(b1), Some(b2)) = (
318        parse_bool(trait_value, false),
319        parse_bool(condition_value, false),
320    ) {
321        return match operator {
322            ConditionOperator::Equal => b1 == b2,
323            ConditionOperator::NotEqual => b1 != b2,
324            _ => false,
325        };
326    }
327
328    // Try parsing as integer
329    if let (Ok(i1), Ok(i2)) = (trait_value.parse::<i64>(), condition_value.parse::<i64>()) {
330        return dispatch_operator(operator, i1, i2);
331    }
332
333    // Try parsing as float
334    if let (Ok(f1), Ok(f2)) = (trait_value.parse::<f64>(), condition_value.parse::<f64>()) {
335        return dispatch_operator(operator, f1, f2);
336    }
337
338    // Fall back to string comparison
339    dispatch_operator(operator, trait_value, condition_value)
340}
341
342/// Dispatches the operator to the appropriate comparison function
343fn dispatch_operator<T: PartialOrd + PartialEq>(
344    operator: &ConditionOperator,
345    v1: T,
346    v2: T,
347) -> bool {
348    match operator {
349        ConditionOperator::Equal => v1 == v2,
350        ConditionOperator::NotEqual => v1 != v2,
351        ConditionOperator::GreaterThan => v1 > v2,
352        ConditionOperator::LessThan => v1 < v2,
353        ConditionOperator::GreaterThanInclusive => v1 >= v2,
354        ConditionOperator::LessThanInclusive => v1 <= v2,
355        _ => false,
356    }
357}
358
359/// Evaluates regex matching
360fn evaluate_regex(trait_value: &str, condition_value: &str) -> bool {
361    if let Ok(re) = Regex::new(condition_value) {
362        return re.is_match(trait_value);
363    }
364    false
365}
366
367/// Evaluates modulo operation
368fn evaluate_modulo(trait_value: &str, condition_value: &str) -> bool {
369    let values: Vec<&str> = condition_value.split('|').collect();
370    if values.len() != 2 {
371        return false;
372    }
373
374    let divisor = match values[0].parse::<f64>() {
375        Ok(v) => v,
376        Err(_) => return false,
377    };
378
379    let remainder = match values[1].parse::<f64>() {
380        Ok(v) => v,
381        Err(_) => return false,
382    };
383
384    let trait_value_float = match trait_value.parse::<f64>() {
385        Ok(v) => v,
386        Err(_) => return false,
387    };
388
389    // Use epsilon comparison for float equality to handle precision errors
390    const EPSILON: f64 = 1e-10;
391    ((trait_value_float % divisor) - remainder).abs() < EPSILON
392}
393
394/// Evaluates semantic version comparisons
395fn evaluate_semver(
396    operator: &ConditionOperator,
397    trait_value: &str,
398    condition_version: &Version,
399) -> bool {
400    let trait_version = match Version::parse(trait_value) {
401        Ok(v) => v,
402        Err(_) => return false,
403    };
404
405    match operator {
406        ConditionOperator::Equal => trait_version == *condition_version,
407        ConditionOperator::NotEqual => trait_version != *condition_version,
408        ConditionOperator::GreaterThan => trait_version > *condition_version,
409        ConditionOperator::LessThan => trait_version < *condition_version,
410        ConditionOperator::GreaterThanInclusive => trait_version >= *condition_version,
411        ConditionOperator::LessThanInclusive => trait_version <= *condition_version,
412        _ => false,
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    #[test]
421    fn test_dispatch_operator_integers() {
422        assert!(dispatch_operator(&ConditionOperator::Equal, 5, 5));
423        assert!(!dispatch_operator(&ConditionOperator::Equal, 5, 6));
424        assert!(dispatch_operator(&ConditionOperator::GreaterThan, 6, 5));
425        assert!(!dispatch_operator(&ConditionOperator::GreaterThan, 5, 6));
426    }
427
428    #[test]
429    fn test_evaluate_modulo() {
430        assert!(evaluate_modulo("2", "2|0"));
431        assert!(!evaluate_modulo("3", "2|0"));
432        assert!(evaluate_modulo("35.0", "4|3"));
433    }
434}