Skip to content

Commit c6b5a56

Browse files
committed
wip: Add evaluation context
1 parent 5402536 commit c6b5a56

File tree

12 files changed

+1410
-290
lines changed

12 files changed

+1410
-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: 165 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -1,213 +1,195 @@
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+
// Process segments
29+
for segment_context in ec.segments.values() {
30+
if !is_context_in_segment(ec, segment_context) {
31+
continue;
32+
}
33+
34+
// Add segment to results
35+
segments.push(SegmentResult {
36+
name: segment_context.name.clone(),
37+
metadata: segment_context.metadata.clone(),
38+
});
39+
40+
// Process segment overrides
41+
for override_fc in &segment_context.overrides {
42+
let feature_name = &override_fc.name;
43+
44+
// Check if we should update the segment feature context
45+
let should_update = if let Some(existing) = segment_feature_contexts.get(feature_name) {
46+
let existing_priority = get_priority_or_default(existing.feature_context.priority);
47+
let override_priority = get_priority_or_default(override_fc.priority);
48+
override_priority < existing_priority
49+
} else {
50+
true
51+
};
52+
53+
if should_update {
54+
segment_feature_contexts.insert(
55+
feature_name.clone(),
56+
FeatureContextWithSegment {
57+
feature_context: override_fc.clone(),
58+
segment_name: segment_context.name.clone(),
59+
},
60+
);
61+
}
62+
}
5163
}
52-
return feature_states.collect();
53-
}
5464

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());
65+
(segments, segment_feature_contexts)
7266
}
7367

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();
68+
/// Gets flag results from feature contexts and segment overrides
69+
fn get_flag_results(
70+
ec: &EngineEvaluationContext,
71+
segment_feature_contexts: &HashMap<String, FeatureContextWithSegment>,
72+
) -> HashMap<String, FlagResult> {
73+
let mut flags = HashMap::new();
74+
75+
// Get identity key if identity exists
76+
// If identity key is not provided, construct it from environment key and identifier
77+
let identity_key: Option<String> = ec.identity.as_ref().map(|i| {
78+
if i.key.is_empty() {
79+
format!("{}_{}", ec.environment.key, i.identifier)
80+
} else {
81+
i.key.clone()
82+
}
83+
});
8084

81-
// Get feature states from the environment
82-
for fs in environment.feature_states.clone() {
83-
feature_states.insert(fs.feature.clone(), fs);
85+
// Process all features
86+
for feature_context in ec.features.values() {
87+
// Check if we have a segment override for this feature
88+
if let Some(segment_fc) = segment_feature_contexts.get(&feature_context.name) {
89+
// Use segment override
90+
let fc = &segment_fc.feature_context;
91+
let reason = format!("TARGETING_MATCH; segment={}", segment_fc.segment_name);
92+
flags.insert(
93+
feature_context.name.clone(),
94+
FlagResult {
95+
enabled: fc.enabled,
96+
name: fc.name.clone(),
97+
reason,
98+
value: fc.value.clone(),
99+
metadata: fc.metadata.clone(),
100+
},
101+
);
102+
} else {
103+
// Use default feature context
104+
let flag_result = get_flag_result_from_feature_context(feature_context, identity_key.as_ref());
105+
flags.insert(feature_context.name.clone(), flag_result);
106+
}
84107
}
85108

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-
}
109+
flags
110+
}
111+
112+
pub fn get_evaluation_result(ec: &EngineEvaluationContext) -> EvaluationResult {
113+
// Process segments
114+
let (segments, segment_feature_contexts) = get_matching_segments_and_overrides(ec);
115+
116+
// Get flag results
117+
let flags = get_flag_results(ec, &segment_feature_contexts);
118+
119+
EvaluationResult { flags, segments }
120+
}
121+
122+
/// Creates a FlagResult from a FeatureContext
123+
fn get_flag_result_from_feature_context(
124+
feature_context: &FeatureContext,
125+
identity_key: Option<&String>,
126+
) -> FlagResult {
127+
let mut reason = "DEFAULT".to_string();
128+
let mut value = feature_context.value.clone();
129+
130+
// Handle multivariate features
131+
if !feature_context.variants.is_empty()
132+
&& identity_key.is_some()
133+
&& !feature_context.key.is_empty()
134+
{
135+
// Sort variants by priority (lower priority value = higher priority)
136+
let mut sorted_variants = feature_context.variants.clone();
137+
sorted_variants.sort_by(|a, b| {
138+
let pa = get_priority_or_default(a.priority);
139+
let pb = get_priority_or_default(b.priority);
140+
pa.partial_cmp(&pb).unwrap()
141+
});
142+
143+
// Calculate hash percentage for the identity and feature combination
144+
let object_ids = vec![feature_context.key.as_str(), identity_key.unwrap().as_str()];
145+
let hash_percentage = hashing::get_hashed_percentage_for_object_ids(object_ids, 1);
146+
147+
// Select variant based on weighted distribution
148+
let mut cumulative_weight = 0.0;
149+
for variant in &sorted_variants {
150+
cumulative_weight += variant.weight;
151+
if (hash_percentage as f64) <= cumulative_weight {
152+
value = variant.value.clone();
153+
reason = format!("SPLIT; weight={}", variant.weight);
154+
break;
96155
}
97-
feature_states.insert(feature_state.feature.clone(), feature_state);
98156
}
99157
}
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);
158+
159+
FlagResult {
160+
enabled: feature_context.enabled,
161+
name: feature_context.name.clone(),
162+
value,
163+
reason,
164+
metadata: feature_context.metadata.clone(),
103165
}
104-
return feature_states;
105166
}
106167

107168
#[cfg(test)]
108169
mod tests {
109170
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-
}"#;
171+
use crate::engine_eval::context::EnvironmentContext;
161172

162173
#[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);
174+
fn test_get_priority_or_default() {
175+
assert_eq!(get_priority_or_default(Some(1.0)), 1.0);
176+
assert_eq!(get_priority_or_default(None), f64::INFINITY);
170177
}
171178

172179
#[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)
179-
}
180-
181-
#[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-
}
180+
fn test_get_evaluation_result_empty_context() {
181+
let ec = EngineEvaluationContext {
182+
environment: EnvironmentContext {
183+
key: "test".to_string(),
184+
name: "test".to_string(),
185+
},
186+
features: HashMap::new(),
187+
segments: HashMap::new(),
188+
identity: None,
189+
};
191190

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)
191+
let result = get_evaluation_result(&ec);
192+
assert_eq!(result.flags.len(), 0);
193+
assert_eq!(result.segments.len(), 0);
212194
}
213195
}

0 commit comments

Comments
 (0)