flagsmith_flag_engine/
engine.rs

1use crate::engine_eval::context::{EngineEvaluationContext, FeatureContext};
2use crate::engine_eval::result::{EvaluationResult, FlagResult, SegmentResult};
3use crate::engine_eval::segment_evaluator::is_context_in_segment;
4use crate::utils::hashing;
5use std::collections::HashMap;
6
7/// Holds a feature context with its associated segment name for priority comparison
8struct FeatureContextWithSegment {
9    feature_context: FeatureContext,
10    segment_name: String,
11}
12
13/// Helper to get priority or default
14fn get_priority_or_default(priority: Option<f64>) -> f64 {
15    priority.unwrap_or(f64::INFINITY) // Weakest possible priority
16}
17
18/// Gets matching segments and their overrides
19fn get_matching_segments_and_overrides(
20    ec: &EngineEvaluationContext,
21) -> (
22    Vec<SegmentResult>,
23    HashMap<String, FeatureContextWithSegment>,
24) {
25    let mut segments = Vec::new();
26    let mut segment_feature_contexts: HashMap<String, FeatureContextWithSegment> = HashMap::new();
27
28    // Sort segment keys for deterministic ordering
29    let mut segment_keys: Vec<_> = ec.segments.keys().collect();
30    segment_keys.sort();
31
32    // Process segments in sorted order
33    for segment_key in segment_keys {
34        let segment_context = &ec.segments[segment_key];
35
36        if !is_context_in_segment(ec, segment_context) {
37            continue;
38        }
39
40        // Add segment to results
41        segments.push(SegmentResult {
42            name: segment_context.name.clone(),
43            metadata: segment_context.metadata.clone(),
44        });
45
46        // Process segment overrides
47        for override_fc in &segment_context.overrides {
48            let feature_name = &override_fc.name;
49
50            // Check if we should update the segment feature context
51            let should_update = if let Some(existing) = segment_feature_contexts.get(feature_name) {
52                let existing_priority = get_priority_or_default(existing.feature_context.priority);
53                let override_priority = get_priority_or_default(override_fc.priority);
54                override_priority < existing_priority
55            } else {
56                true
57            };
58
59            if should_update {
60                segment_feature_contexts.insert(
61                    feature_name.clone(),
62                    FeatureContextWithSegment {
63                        feature_context: override_fc.clone(),
64                        segment_name: segment_context.name.clone(),
65                    },
66                );
67            }
68        }
69    }
70
71    (segments, segment_feature_contexts)
72}
73
74/// Gets flag results from feature contexts and segment overrides
75fn get_flag_results(
76    ec: &EngineEvaluationContext,
77    segment_feature_contexts: &HashMap<String, FeatureContextWithSegment>,
78) -> HashMap<String, FlagResult> {
79    let mut flags = HashMap::new();
80
81    // Get identity key if identity exists
82    // If identity key is not provided, construct it from environment key and identifier
83    let identity_key: Option<String> = ec.identity.as_ref().map(|i| {
84        if i.key.is_empty() {
85            format!("{}_{}", ec.environment.key, i.identifier)
86        } else {
87            i.key.clone()
88        }
89    });
90
91    // Process all features
92    for feature_context in ec.features.values() {
93        // Check if we have a segment override for this feature
94        if let Some(segment_fc) = segment_feature_contexts.get(&feature_context.name) {
95            // Use segment override with multivariate evaluation
96            let fc = &segment_fc.feature_context;
97            let reason = format!("TARGETING_MATCH; segment={}", segment_fc.segment_name);
98            let flag_result =
99                get_flag_result_from_feature_context(fc, identity_key.as_ref(), reason);
100            flags.insert(feature_context.name.clone(), flag_result);
101        } else {
102            // Use default feature context
103            let flag_result = get_flag_result_from_feature_context(
104                feature_context,
105                identity_key.as_ref(),
106                "DEFAULT".to_string(),
107            );
108            flags.insert(feature_context.name.clone(), flag_result);
109        }
110    }
111
112    flags
113}
114
115pub fn get_evaluation_result(ec: &EngineEvaluationContext) -> EvaluationResult {
116    // Process segments
117    let (segments, segment_feature_contexts) = get_matching_segments_and_overrides(ec);
118
119    // Get flag results
120    let flags = get_flag_results(ec, &segment_feature_contexts);
121
122    EvaluationResult { flags, segments }
123}
124
125/// Creates a FlagResult from a FeatureContext
126fn get_flag_result_from_feature_context(
127    feature_context: &FeatureContext,
128    identity_key: Option<&String>,
129    default_reason: String,
130) -> FlagResult {
131    let mut reason = default_reason;
132    let mut value = feature_context.value.clone();
133
134    // Handle multivariate features
135    if !feature_context.variants.is_empty()
136        && identity_key.is_some()
137        && !feature_context.key.is_empty()
138    {
139        // Sort variants by priority (lower priority value = higher priority)
140        let mut sorted_variants = feature_context.variants.clone();
141        sorted_variants.sort_by(|a, b| {
142            let pa = get_priority_or_default(a.priority);
143            let pb = get_priority_or_default(b.priority);
144            pa.partial_cmp(&pb).unwrap()
145        });
146
147        // Calculate hash percentage for the identity and feature combination
148        let object_ids = vec![feature_context.key.as_str(), identity_key.unwrap().as_str()];
149        let hash_percentage = hashing::get_hashed_percentage_for_object_ids(object_ids, 1);
150
151        // Select variant based on weighted distribution
152        let mut cumulative_weight = 0.0;
153        for variant in &sorted_variants {
154            cumulative_weight += variant.weight;
155            if (hash_percentage as f64) <= cumulative_weight {
156                value = variant.value.clone();
157                reason = format!("SPLIT; weight={}", variant.weight);
158                break;
159            }
160        }
161    }
162
163    FlagResult {
164        enabled: feature_context.enabled,
165        name: feature_context.name.clone(),
166        value,
167        reason,
168        metadata: feature_context.metadata.clone(),
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::engine_eval::context::EnvironmentContext;
176
177    #[test]
178    fn test_get_priority_or_default() {
179        assert_eq!(get_priority_or_default(Some(1.0)), 1.0);
180        assert_eq!(get_priority_or_default(None), f64::INFINITY);
181    }
182
183    #[test]
184    fn test_get_evaluation_result_empty_context() {
185        let ec = EngineEvaluationContext {
186            environment: EnvironmentContext {
187                key: "test".to_string(),
188                name: "test".to_string(),
189            },
190            features: HashMap::new(),
191            segments: HashMap::new(),
192            identity: None,
193        };
194
195        let result = get_evaluation_result(&ec);
196        assert_eq!(result.flags.len(), 0);
197        assert_eq!(result.segments.len(), 0);
198    }
199}