Skip to content

Commit c655d52

Browse files
authored
feat: Add evaluation context (#23)
*feat: Add evaluation context * fix multivariate segment override * feat: Add ConditionValue enum - Implement ConditionValue enum to handle string or array values - Supports Single(String) and Multiple(Vec<String>) variants - Custom deserializer handles JSON arrays, JSON array strings, and comma-separated strings - Helper methods: as_string(), as_vec(), contains_string() - Simplify IN operator implementation - Use ConditionValue's contains_string() for string matching - Remove redundant JSON array parsing logic * chore: Fix clippy warnings in engine_eval module - Fix collapsible_if warning: collapse nested if statements - Fix manual_strip warning: use strip_suffix instead of manual slicing - Fix unwrap_or_default warning: use or_default() instead of or_insert_with(Vec::new) * test: Add reason field validation in engine evaluation tests - Compare flag reason field in addition to enabled and value - Ensure evaluation reasons (DEFAULT, TARGETING_MATCH, SPLIT) are correct * refactor: improve engine_tests * process segment in order * better error messages on test failure
1 parent 5402536 commit c655d52

File tree

12 files changed

+1320
-290
lines changed

12 files changed

+1320
-290
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ maintenance = { status = "actively-developed" }
1313
[dependencies]
1414
serde = { version = "1.0", features = ["derive"] }
1515
serde_json = "1.0"
16+
serde_json_path = "0.7"
1617
chrono = { version = "0.4", features = ["serde"] }
1718
md-5 = "0.10.1"
1819
num-bigint = "0.4"
1920
num-traits = "0.2.14"
2021
uuid = { version = "0.8", features = ["serde", "v4"] }
2122
regex = "1"
2223
semver = "1.0"
24+
sha2 = "0.10"
2325

2426
[dev-dependencies]
2527
rstest = "0.12.0"
28+
json_comments = "0.2"

src/engine.rs

Lines changed: 169 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -1,213 +1,199 @@
1-
use super::environments;
2-
use super::error;
3-
use super::features;
4-
use super::identities;
5-
use super::segments::evaluator;
6-
use crate::features::Feature;
7-
use crate::features::FeatureState;
1+
use crate::engine_eval::context::{EngineEvaluationContext, FeatureContext};
2+
use crate::engine_eval::result::{EvaluationResult, FlagResult, SegmentResult};
3+
use crate::engine_eval::segment_evaluator::is_context_in_segment;
4+
use crate::utils::hashing;
85
use std::collections::HashMap;
96

10-
//Returns a vector of feature states for a given environment
11-
pub fn get_environment_feature_states(
12-
environment: environments::Environment,
13-
) -> Vec<features::FeatureState> {
14-
if environment.project.hide_disabled_flags {
15-
return environment
16-
.feature_states
17-
.iter()
18-
.filter(|fs| fs.enabled)
19-
.map(|fs| fs.clone())
20-
.collect();
21-
}
22-
return environment.feature_states;
7+
/// Holds a feature context with its associated segment name for priority comparison
8+
struct FeatureContextWithSegment {
9+
feature_context: FeatureContext,
10+
segment_name: String,
2311
}
2412

25-
// Returns a specific feature state for a given feature_name in a given environment
26-
// If exists else returns a FeatureStateNotFound error
27-
pub fn get_environment_feature_state(
28-
environment: environments::Environment,
29-
feature_name: &str,
30-
) -> Result<features::FeatureState, error::Error> {
31-
let fs = environment
32-
.feature_states
33-
.iter()
34-
.filter(|fs| fs.feature.name == feature_name)
35-
.next()
36-
.ok_or(error::Error::new(error::ErrorKind::FeatureStateNotFound));
37-
return Ok(fs?.clone());
13+
/// Helper to get priority or default
14+
fn get_priority_or_default(priority: Option<f64>) -> f64 {
15+
priority.unwrap_or(f64::INFINITY) // Weakest possible priority
3816
}
3917

40-
// Returns a vector of feature state models based on the environment, any matching
41-
// segments and any specific identity overrides
42-
pub fn get_identity_feature_states(
43-
environment: &environments::Environment,
44-
identity: &identities::Identity,
45-
override_traits: Option<&Vec<identities::Trait>>,
46-
) -> Vec<features::FeatureState> {
47-
let feature_states =
48-
get_identity_feature_states_map(environment, identity, override_traits).into_values();
49-
if environment.project.hide_disabled_flags {
50-
return feature_states.filter(|fs| fs.enabled).collect();
18+
/// Gets matching segments and their overrides
19+
fn 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+
}
5169
}
52-
return feature_states.collect();
53-
}
5470

55-
// Returns a specific feature state based on the environment, any matching
56-
// segments and any specific identity overrides
57-
// If exists else returns a FeatureStateNotFound error
58-
pub fn get_identity_feature_state(
59-
environment: &environments::Environment,
60-
identity: &identities::Identity,
61-
feature_name: &str,
62-
override_traits: Option<&Vec<identities::Trait>>,
63-
) -> Result<features::FeatureState, error::Error> {
64-
let feature_states =
65-
get_identity_feature_states_map(environment, identity, override_traits).into_values();
66-
let fs = feature_states
67-
.filter(|fs| fs.feature.name == feature_name)
68-
.next()
69-
.ok_or(error::Error::new(error::ErrorKind::FeatureStateNotFound));
70-
71-
return Ok(fs?.clone());
71+
(segments, segment_feature_contexts)
7272
}
7373

74-
fn get_identity_feature_states_map(
75-
environment: &environments::Environment,
76-
identity: &identities::Identity,
77-
override_traits: Option<&Vec<identities::Trait>>,
78-
) -> HashMap<Feature, FeatureState> {
79-
let mut feature_states: HashMap<Feature, FeatureState> = HashMap::new();
74+
/// Gets flag results from feature contexts and segment overrides
75+
fn 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+
});
8090

81-
// Get feature states from the environment
82-
for fs in environment.feature_states.clone() {
83-
feature_states.insert(fs.feature.clone(), fs);
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+
}
84110
}
85111

86-
// Override with any feature states defined by matching segments
87-
let identity_segments =
88-
evaluator::get_identity_segments(environment, identity, override_traits);
89-
for matching_segments in identity_segments {
90-
for feature_state in matching_segments.feature_states {
91-
let existing = feature_states.get(&feature_state.feature);
92-
if existing.is_some() {
93-
if existing.unwrap().is_higher_segment_priority(&feature_state) {
94-
continue;
95-
}
112+
flags
113+
}
114+
115+
pub 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
126+
fn 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;
96159
}
97-
feature_states.insert(feature_state.feature.clone(), feature_state);
98160
}
99161
}
100-
// Override with any feature states defined directly the identity
101-
for feature_state in identity.identity_features.clone() {
102-
feature_states.insert(feature_state.feature.clone(), feature_state);
162+
163+
FlagResult {
164+
enabled: feature_context.enabled,
165+
name: feature_context.name.clone(),
166+
value,
167+
reason,
168+
metadata: feature_context.metadata.clone(),
103169
}
104-
return feature_states;
105170
}
106171

107172
#[cfg(test)]
108173
mod tests {
109174
use super::*;
110-
static IDENTITY_JSON: &str = r#"{
111-
"identifier": "test_user",
112-
"environment_api_key": "test_api_key",
113-
"created_date": "2022-03-02T12:31:05.309861",
114-
"identity_features": [],
115-
"identity_traits": [],
116-
"identity_uuid":""
117-
}"#;
118-
static ENVIRONMENT_JSON: &str = r#"
119-
{
120-
"api_key": "test_key",
121-
"project": {
122-
"name": "Test project",
123-
"organisation": {
124-
"feature_analytics": false,
125-
"name": "Test Org",
126-
"id": 1,
127-
"persist_trait_data": true,
128-
"stop_serving_flags": false
129-
},
130-
"id": 1,
131-
"hide_disabled_flags": true,
132-
"segments": []
133-
},
134-
"segment_overrides": [],
135-
"id": 1,
136-
"feature_states": [
137-
{
138-
"multivariate_feature_state_values": [],
139-
"feature_state_value": true,
140-
"django_id": 1,
141-
"feature": {
142-
"name": "feature1",
143-
"type": null,
144-
"id": 1
145-
},
146-
"enabled": false
147-
},
148-
{
149-
"multivariate_feature_state_values": [],
150-
"feature_state_value": null,
151-
"django_id": 2,
152-
"feature": {
153-
"name": "feature_2",
154-
"type": null,
155-
"id": 2
156-
},
157-
"enabled": true
158-
}
159-
]
160-
}"#;
175+
use crate::engine_eval::context::EnvironmentContext;
161176

162177
#[test]
163-
fn get_environment_feature_states_only_return_enabled_fs_if_hide_disabled_flags_is_true() {
164-
let environment: environments::Environment =
165-
serde_json::from_str(ENVIRONMENT_JSON).unwrap();
166-
167-
let environment_feature_states = get_environment_feature_states(environment);
168-
assert_eq!(environment_feature_states.len(), 1);
169-
assert_eq!(environment_feature_states[0].django_id.unwrap(), 2);
170-
}
171-
172-
#[test]
173-
fn get_environment_feature_state_returns_correct_feature_state() {
174-
let environment: environments::Environment =
175-
serde_json::from_str(ENVIRONMENT_JSON).unwrap();
176-
let feature_name = "feature_2";
177-
let feature_state = get_environment_feature_state(environment, feature_name).unwrap();
178-
assert_eq!(feature_state.feature.name, feature_name)
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);
179181
}
180182

181183
#[test]
182-
fn get_environment_feature_state_returns_error_if_feature_state_does_not_exists() {
183-
let environment: environments::Environment =
184-
serde_json::from_str(ENVIRONMENT_JSON).unwrap();
185-
let feature_name = "feature_that_does_not_exists";
186-
let err = get_environment_feature_state(environment, feature_name)
187-
.err()
188-
.unwrap();
189-
assert_eq!(err.kind, error::ErrorKind::FeatureStateNotFound)
190-
}
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+
};
191194

192-
#[test]
193-
fn get_identity_feature_state_returns_correct_feature_state() {
194-
let environment: environments::Environment =
195-
serde_json::from_str(ENVIRONMENT_JSON).unwrap();
196-
let feature_name = "feature_2";
197-
let identity: identities::Identity = serde_json::from_str(IDENTITY_JSON).unwrap();
198-
let feature_state =
199-
get_identity_feature_state(&environment, &identity, feature_name, None).unwrap();
200-
assert_eq!(feature_state.feature.name, feature_name)
201-
}
202-
#[test]
203-
fn get_identity_feature_state_returns_error_if_feature_state_does_not_exists() {
204-
let environment: environments::Environment =
205-
serde_json::from_str(ENVIRONMENT_JSON).unwrap();
206-
let feature_name = "feature_that_does_not_exists";
207-
let identity: identities::Identity = serde_json::from_str(IDENTITY_JSON).unwrap();
208-
let err = get_identity_feature_state(&environment, &identity, feature_name, None)
209-
.err()
210-
.unwrap();
211-
assert_eq!(err.kind, error::ErrorKind::FeatureStateNotFound)
195+
let result = get_evaluation_result(&ec);
196+
assert_eq!(result.flags.len(), 0);
197+
assert_eq!(result.segments.len(), 0);
212198
}
213199
}

0 commit comments

Comments
 (0)