flagsmith_flag_engine/
engine.rs1use 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
7struct FeatureContextWithSegment {
9 feature_context: FeatureContext,
10 segment_name: String,
11}
12
13fn get_priority_or_default(priority: Option<f64>) -> f64 {
15 priority.unwrap_or(f64::INFINITY) }
17
18fn 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 let mut segment_keys: Vec<_> = ec.segments.keys().collect();
30 segment_keys.sort();
31
32 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 segments.push(SegmentResult {
42 name: segment_context.name.clone(),
43 metadata: segment_context.metadata.clone(),
44 });
45
46 for override_fc in &segment_context.overrides {
48 let feature_name = &override_fc.name;
49
50 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
74fn 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 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 for feature_context in ec.features.values() {
93 if let Some(segment_fc) = segment_feature_contexts.get(&feature_context.name) {
95 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 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 let (segments, segment_feature_contexts) = get_matching_segments_and_overrides(ec);
118
119 let flags = get_flag_results(ec, &segment_feature_contexts);
121
122 EvaluationResult { flags, segments }
123}
124
125fn 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 if !feature_context.variants.is_empty()
136 && identity_key.is_some()
137 && !feature_context.key.is_empty()
138 {
139 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 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 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}