flagsmith_flag_engine/engine_eval/
mappers.rs

1use super::context::{
2    Condition, ConditionOperator, EngineEvaluationContext, EnvironmentContext, FeatureContext,
3    FeatureMetadata, FeatureValue, IdentityContext, SegmentContext, SegmentMetadata, SegmentRule,
4    SegmentRuleType, SegmentSource,
5};
6use crate::environments::Environment;
7use crate::features::{FeatureState, MultivariateFeatureStateValue};
8use crate::identities::{Identity, Trait};
9use crate::segments::{Segment, SegmentRule as OldSegmentRule};
10use sha2::{Digest, Sha256};
11use std::collections::HashMap;
12
13/// Maps an Environment to an EngineEvaluationContext
14///
15/// # Arguments
16/// * `environment` - The environment to convert
17///
18/// # Returns
19/// A new engine evaluation context
20pub fn environment_to_context(environment: Environment) -> EngineEvaluationContext {
21    let mut ctx = EngineEvaluationContext {
22        environment: EnvironmentContext {
23            key: environment.api_key.clone(),
24            name: environment.name.clone(),
25        },
26        features: HashMap::new(),
27        segments: HashMap::new(),
28        identity: None,
29    };
30
31    // Map feature states to feature contexts
32    for fs in &environment.feature_states {
33        let fc = map_feature_state_to_feature_context(fs);
34        ctx.features.insert(fc.name.clone(), fc);
35    }
36
37    // Map project segments to segment contexts
38    for segment in &environment.project.segments {
39        let sc = map_segment_to_segment_context(segment);
40        ctx.segments.insert(sc.key.clone(), sc);
41    }
42
43    // Map identity overrides to segments
44    if !environment.identity_overrides.is_empty() {
45        let identity_segments = map_identity_overrides_to_segments(&environment.identity_overrides);
46        for (key, segment) in identity_segments {
47            ctx.segments.insert(key, segment);
48        }
49    }
50
51    ctx
52}
53
54/// Maps a FeatureState to a FeatureContext
55fn map_feature_state_to_feature_context(fs: &FeatureState) -> FeatureContext {
56    let key = if let Some(django_id) = fs.django_id {
57        django_id.to_string()
58    } else {
59        fs.featurestate_uuid.clone()
60    };
61
62    let mut fc = FeatureContext {
63        enabled: fs.enabled,
64        key,
65        name: fs.feature.name.clone(),
66        value: fs.get_value(None),
67        priority: None,
68        variants: map_multivariate_values_to_variants(&fs.multivariate_feature_state_values),
69        metadata: FeatureMetadata {
70            feature_id: fs.feature.id,
71            feature_type: fs
72                .feature
73                .feature_type
74                .clone()
75                .unwrap_or_else(|| "STANDARD".to_string()),
76        },
77    };
78
79    // Set priority if this is a segment override
80    if let Some(feature_segment) = &fs.feature_segment {
81        fc.priority = Some(feature_segment.priority as f64);
82    }
83
84    fc
85}
86
87/// Maps multivariate feature state values to FeatureValue variants
88fn map_multivariate_values_to_variants(
89    mv_values: &[MultivariateFeatureStateValue],
90) -> Vec<FeatureValue> {
91    mv_values
92        .iter()
93        .map(|mv| FeatureValue {
94            value: mv.multivariate_feature_option.value.clone(),
95            weight: mv.percentage_allocation as f64,
96            priority: None,
97        })
98        .collect()
99}
100
101/// Maps a Segment to a SegmentContext
102fn map_segment_to_segment_context(segment: &Segment) -> SegmentContext {
103    let mut sc = SegmentContext {
104        key: segment.id.to_string(),
105        name: segment.name.clone(),
106        metadata: SegmentMetadata {
107            segment_id: Some(segment.id as i32),
108            source: SegmentSource::Api,
109        },
110        overrides: vec![],
111        rules: vec![],
112    };
113
114    // Map feature state overrides
115    for fs in &segment.feature_states {
116        sc.overrides.push(map_feature_state_to_feature_context(fs));
117    }
118
119    // Map segment rules
120    for rule in &segment.rules {
121        sc.rules.push(map_segment_rule_to_rule(rule));
122    }
123
124    sc
125}
126
127/// Maps a legacy SegmentRule to the new SegmentRule format
128fn map_segment_rule_to_rule(rule: &OldSegmentRule) -> SegmentRule {
129    let rule_type = map_rule_type(&rule.segment_rule_type);
130
131    let conditions = rule
132        .conditions
133        .iter()
134        .map(|c| Condition {
135            operator: map_operator(&c.operator),
136            property: c.property.clone().unwrap_or_default(),
137            value: super::context::ConditionValue::Single(c.value.clone().unwrap_or_default()),
138        })
139        .collect();
140
141    let rules = rule
142        .rules
143        .iter()
144        .map(|r| map_segment_rule_to_rule(r))
145        .collect();
146
147    SegmentRule {
148        rule_type,
149        conditions,
150        rules,
151    }
152}
153
154/// Maps a rule type string to SegmentRuleType enum
155fn map_rule_type(rule_type: &str) -> SegmentRuleType {
156    match rule_type {
157        "ALL" => SegmentRuleType::All,
158        "ANY" => SegmentRuleType::Any,
159        "NONE" => SegmentRuleType::None,
160        _ => SegmentRuleType::All,
161    }
162}
163
164/// Maps an operator string to ConditionOperator enum
165fn map_operator(operator: &str) -> ConditionOperator {
166    match operator {
167        "EQUAL" => ConditionOperator::Equal,
168        "NOT_EQUAL" => ConditionOperator::NotEqual,
169        "GREATER_THAN" => ConditionOperator::GreaterThan,
170        "GREATER_THAN_INCLUSIVE" => ConditionOperator::GreaterThanInclusive,
171        "LESS_THAN" => ConditionOperator::LessThan,
172        "LESS_THAN_INCLUSIVE" => ConditionOperator::LessThanInclusive,
173        "CONTAINS" => ConditionOperator::Contains,
174        "NOT_CONTAINS" => ConditionOperator::NotContains,
175        "IN" => ConditionOperator::In,
176        "REGEX" => ConditionOperator::Regex,
177        "PERCENTAGE_SPLIT" => ConditionOperator::PercentageSplit,
178        "MODULO" => ConditionOperator::Modulo,
179        "IS_SET" => ConditionOperator::IsSet,
180        "IS_NOT_SET" => ConditionOperator::IsNotSet,
181        _ => ConditionOperator::Equal,
182    }
183}
184
185/// Helper struct for grouping identity overrides
186#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
187struct OverrideKey {
188    feature_name: String,
189    enabled: String,
190    feature_value: String,
191    feature_id: u32,
192    feature_type: String,
193}
194
195/// Maps identity overrides to segment contexts
196fn map_identity_overrides_to_segments(identities: &[Identity]) -> HashMap<String, SegmentContext> {
197    let mut features_to_identifiers: HashMap<String, Vec<String>> = HashMap::new();
198    let mut overrides_key_to_list: HashMap<String, Vec<OverrideKey>> = HashMap::new();
199
200    for identity in identities {
201        if identity.identity_features.is_empty() {
202            continue;
203        }
204
205        // Create override keys from features
206        let mut overrides = Vec::new();
207        for fs in &identity.identity_features {
208            // Use proper JSON serialization instead of Debug format
209            let feature_value =
210                serde_json::to_string(&fs.get_value(None)).unwrap_or_else(|_| "null".to_string());
211            overrides.push(OverrideKey {
212                feature_name: fs.feature.name.clone(),
213                enabled: fs.enabled.to_string(),
214                feature_value,
215                feature_id: fs.feature.id,
216                feature_type: fs
217                    .feature
218                    .feature_type
219                    .clone()
220                    .unwrap_or_else(|| "STANDARD".to_string()),
221            });
222        }
223
224        // Sort overrides for consistent hashing
225        overrides.sort();
226
227        // Generate hash for this set of overrides
228        let overrides_hash = generate_hash(&overrides);
229
230        // Group identifiers by their overrides
231        features_to_identifiers
232            .entry(overrides_hash.clone())
233            .or_default()
234            .push(identity.identifier.clone());
235
236        overrides_key_to_list.insert(overrides_hash, overrides);
237    }
238
239    // Create segment contexts for each unique set of overrides
240    let mut segment_contexts = HashMap::new();
241
242    for (overrides_hash, identifiers) in features_to_identifiers {
243        let overrides = overrides_key_to_list.get(&overrides_hash).unwrap();
244
245        // Create segment context
246        let mut sc = SegmentContext {
247            key: String::new(), // Identity override segments never use % Split operator
248            name: "identity_overrides".to_string(),
249            metadata: SegmentMetadata {
250                segment_id: None,
251                source: SegmentSource::IdentityOverride,
252            },
253            overrides: vec![],
254            rules: vec![SegmentRule {
255                rule_type: SegmentRuleType::All,
256                conditions: vec![Condition {
257                    operator: ConditionOperator::In,
258                    property: "$.identity.identifier".to_string(),
259                    value: super::context::ConditionValue::Multiple(identifiers),
260                }],
261                rules: vec![],
262            }],
263        };
264
265        // Create feature overrides
266        for override_key in overrides {
267            let priority = f64::NEG_INFINITY; // Strongest possible priority
268            let feature_override = FeatureContext {
269                key: String::new(), // Identity overrides never carry multivariate options
270                name: override_key.feature_name.clone(),
271                enabled: override_key.enabled == "true",
272                value: serde_json::from_str(&override_key.feature_value).unwrap_or_default(),
273                priority: Some(priority),
274                variants: vec![],
275                metadata: FeatureMetadata {
276                    feature_id: override_key.feature_id,
277                    feature_type: override_key.feature_type.clone(),
278                },
279            };
280
281            sc.overrides.push(feature_override);
282        }
283
284        segment_contexts.insert(overrides_hash, sc);
285    }
286
287    segment_contexts
288}
289
290/// Generates a hash from override keys for use as segment key
291fn generate_hash(overrides: &[OverrideKey]) -> String {
292    let mut hasher = Sha256::new();
293
294    for override_key in overrides {
295        hasher.update(format!(
296            "{}:{}:{}:{};",
297            override_key.feature_id,
298            override_key.feature_name,
299            override_key.enabled,
300            override_key.feature_value
301        ));
302    }
303
304    let result = hasher.finalize();
305    // Use safe slicing - take first 16 chars without panicking
306    let hex = format!("{:x}", result);
307    hex.chars().take(16).collect()
308}
309
310/// Adds identity data to an existing context
311///
312/// # Arguments
313/// * `context` - The context to enrich with identity data
314/// * `identifier` - The identity identifier
315/// * `traits` - The identity traits
316///
317/// # Returns
318/// A new context with identity information
319pub fn add_identity_to_context(
320    context: &EngineEvaluationContext,
321    identifier: &str,
322    traits: &[Trait],
323) -> EngineEvaluationContext {
324    let mut new_context = context.clone();
325
326    // Create traits map
327    let mut identity_traits = HashMap::new();
328    for trait_obj in traits {
329        identity_traits.insert(trait_obj.trait_key.clone(), trait_obj.trait_value.clone());
330    }
331
332    // Create identity context
333    let environment_key = &new_context.environment.key;
334    let identity = IdentityContext {
335        identifier: identifier.to_string(),
336        key: format!("{}_{}", environment_key, identifier),
337        traits: identity_traits,
338    };
339
340    new_context.identity = Some(identity);
341    new_context
342}