Skip to content

Commit 19a235b

Browse files
authored
feat: migrate to new flag engine with context value support (#39)
* fix: remove segment for get_enviornment_flags * fix: improve error handling * bump engine * cleanup
1 parent 8606c34 commit 19a235b

File tree

7 files changed

+256
-113
lines changed

7 files changed

+256
-113
lines changed

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ url = "2.1"
2121
chrono = { version = "0.4" }
2222
log = "0.4"
2323
flume = "0.10.14"
24-
25-
flagsmith-flag-engine = "0.4.0"
24+
flagsmith-flag-engine = "0.5.0"
2625

2726
[dev-dependencies]
2827
httpmock = "0.6"

src/error.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::convert::From;
2+
use std::error;
23
use std::fmt;
34

45
/// Wraps several types of errors.
@@ -25,12 +26,14 @@ impl Error{
2526
impl fmt::Display for Error {
2627
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2728
match self.kind {
28-
ErrorKind::FlagsmithClientError => write!(f, "Flagsmith API error: {}", &self.msg),
29-
ErrorKind::FlagsmithAPIError => write!(f, "Flagsmith client error: {}", &self.msg),
29+
ErrorKind::FlagsmithClientError => write!(f, "Flagsmith client error: {}", &self.msg),
30+
ErrorKind::FlagsmithAPIError => write!(f, "Flagsmith API error: {}", &self.msg),
3031
}
3132
}
3233
}
3334

35+
impl error::Error for Error {}
36+
3437
impl From<url::ParseError> for Error {
3538
fn from(e: url::ParseError) -> Self {
3639
Error::new(ErrorKind::FlagsmithClientError, e.to_string())

src/flagsmith/mod.rs

Lines changed: 82 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use self::analytics::AnalyticsProcessor;
22
use self::models::{Flag, Flags};
33
use super::error;
4-
use flagsmith_flag_engine::engine;
4+
use flagsmith_flag_engine::engine::get_evaluation_result;
5+
use flagsmith_flag_engine::engine_eval::{
6+
add_identity_to_context, environment_to_context, EngineEvaluationContext, SegmentSource,
7+
};
58
use flagsmith_flag_engine::environments::builders::build_environment_struct;
69
use flagsmith_flag_engine::environments::Environment;
7-
use flagsmith_flag_engine::identities::{Identity, Trait};
8-
use flagsmith_flag_engine::segments::evaluator::get_identity_segments;
10+
use flagsmith_flag_engine::identities::Trait;
911
use flagsmith_flag_engine::segments::Segment;
1012
use log::debug;
1113
use models::SDKTrait;
@@ -70,7 +72,7 @@ pub struct Flagsmith {
7072

7173
struct DataStore {
7274
environment: Option<Environment>,
73-
identities_with_overrides_by_identifier: HashMap<String, Identity>,
75+
evaluation_context: Option<EngineEvaluationContext>,
7476
}
7577

7678
impl Flagsmith {
@@ -108,6 +110,9 @@ impl Flagsmith {
108110
{
109111
panic!("offline_handler cannot be used with local evaluation")
110112
}
113+
if flagsmith_options.enable_local_evaluation && !environment_key.starts_with("ser.") {
114+
panic!("In order to use local evaluation, please use a server-side environment key (starts with 'ser.')")
115+
}
111116

112117
// Initialize analytics processor
113118
let analytics_processor = match flagsmith_options.enable_analytics {
@@ -122,7 +127,10 @@ impl Flagsmith {
122127

123128
// Put the environment model behind mutex to
124129
// to share it safely between threads
125-
let ds = Arc::new(Mutex::new(DataStore { environment: None, identities_with_overrides_by_identifier: HashMap::new() }));
130+
let ds = Arc::new(Mutex::new(DataStore {
131+
environment: None,
132+
evaluation_context: None,
133+
}));
126134
let (tx, rx) = mpsc::sync_channel::<u32>(1);
127135

128136
let flagsmith = Flagsmith {
@@ -138,14 +146,18 @@ impl Flagsmith {
138146

139147
if flagsmith.options.offline_handler.is_some() {
140148
let mut data = flagsmith.datastore.lock().unwrap();
141-
data.environment = Some(
142-
flagsmith
143-
.options
144-
.offline_handler
145-
.as_ref()
146-
.unwrap()
147-
.get_environment(),
148-
)
149+
let environment = flagsmith
150+
.options
151+
.offline_handler
152+
.as_ref()
153+
.unwrap()
154+
.get_environment();
155+
156+
// Create evaluation context from offline environment
157+
let eval_context = environment_to_context(environment.clone());
158+
data.evaluation_context = Some(eval_context);
159+
160+
data.environment = Some(environment);
149161
}
150162

151163
// Create a thread to update environment document
@@ -155,8 +167,10 @@ impl Flagsmith {
155167

156168
if flagsmith.options.enable_local_evaluation {
157169
// Update environment once...
158-
update_environment(&client, &ds, &environment_url).unwrap();
159-
170+
if let Err(e) = update_environment(&client, &ds, &environment_url) {
171+
log::warn!("Failed to fetch environment on initialization: {}. Will retry in background.", e);
172+
}
173+
160174
// ...and continue updating in the background
161175
let ds = Arc::clone(&ds);
162176
thread::spawn(move || loop {
@@ -168,17 +182,19 @@ impl Flagsmith {
168182
Err(TryRecvError::Empty) => {}
169183
}
170184
thread::sleep(Duration::from_millis(environment_refresh_interval_mills));
171-
update_environment(&client, &ds, &environment_url).unwrap();
185+
if let Err(e) = update_environment(&client, &ds, &environment_url) {
186+
log::warn!("Failed to update environment: {}. Will retry on next interval.", e);
187+
}
172188
});
173189
}
174190
return flagsmith;
175191
}
176192
//Returns `Flags` struct holding all the flags for the current environment.
177193
pub fn get_environment_flags(&self) -> Result<models::Flags, error::Error> {
178194
let data = self.datastore.lock().unwrap();
179-
if data.environment.is_some() {
180-
let environment = data.environment.as_ref().unwrap();
181-
return Ok(self.get_environment_flags_from_document(environment));
195+
if data.evaluation_context.is_some() {
196+
let eval_context = data.evaluation_context.as_ref().unwrap();
197+
return Ok(self.get_environment_flags_from_document(eval_context));
182198
}
183199
return self.default_handler_if_err(self.get_environment_flags_from_api());
184200
}
@@ -211,12 +227,11 @@ impl Flagsmith {
211227
) -> Result<Flags, error::Error> {
212228
let data = self.datastore.lock().unwrap();
213229
let traits = traits.unwrap_or(vec![]);
214-
if data.environment.is_some() {
215-
let environment = data.environment.as_ref().unwrap();
230+
if data.evaluation_context.is_some() {
231+
let eval_context = data.evaluation_context.as_ref().unwrap();
216232
let engine_traits: Vec<Trait> = traits.into_iter().map(|t| t.into()).collect();
217233
return self.get_identity_flags_from_document(
218-
environment,
219-
&data.identities_with_overrides_by_identifier,
234+
eval_context,
220235
identifier,
221236
engine_traits,
222237
);
@@ -234,17 +249,33 @@ impl Flagsmith {
234249
traits: Option<Vec<Trait>>,
235250
) -> Result<Vec<Segment>, error::Error> {
236251
let data = self.datastore.lock().unwrap();
237-
if data.environment.is_none() {
252+
if data.evaluation_context.is_none() {
238253
return Err(error::Error::new(
239254
error::ErrorKind::FlagsmithClientError,
240255
"Local evaluation required to obtain identity segments.".to_string(),
241256
));
242257
}
243-
let environment = data.environment.as_ref().unwrap();
244-
let identities_with_overrides_by_identifier = &data.identities_with_overrides_by_identifier;
245-
let identity_model =
246-
self.get_identity_model(&environment, &identities_with_overrides_by_identifier, identifier, traits.clone().unwrap_or(vec![]))?;
247-
let segments = get_identity_segments(environment, &identity_model, traits.as_ref());
258+
let eval_context = data.evaluation_context.as_ref().unwrap();
259+
let traits = traits.unwrap_or(vec![]);
260+
261+
let context_with_identity = add_identity_to_context(eval_context, identifier, &traits);
262+
263+
let result = get_evaluation_result(&context_with_identity);
264+
265+
let segments: Vec<Segment> = result
266+
.segments
267+
.iter()
268+
.filter(|seg_result| {
269+
seg_result.metadata.source == SegmentSource::Api
270+
})
271+
.map(|seg_result| Segment {
272+
id: seg_result.metadata.segment_id.unwrap_or(0) as u32,
273+
name: seg_result.name.clone(),
274+
rules: vec![],
275+
feature_states: vec![],
276+
})
277+
.collect();
278+
248279
return Ok(segments);
249280
}
250281

@@ -268,12 +299,19 @@ impl Flagsmith {
268299
}
269300
}
270301
}
271-
fn get_environment_flags_from_document(&self, environment: &Environment) -> models::Flags {
272-
return models::Flags::from_feature_states(
273-
&environment.feature_states,
302+
fn get_environment_flags_from_document(&self, eval_context: &EngineEvaluationContext) -> models::Flags {
303+
// Clear segments and identity for environment evaluation
304+
let environment_eval_ctx = EngineEvaluationContext {
305+
environment: eval_context.environment.clone(),
306+
features: eval_context.features.clone(),
307+
segments: HashMap::new(),
308+
identity: None,
309+
};
310+
let result = get_evaluation_result(&environment_eval_ctx);
311+
return models::Flags::from_evaluation_result(
312+
&result,
274313
self.analytics_processor.clone(),
275314
self.options.default_flag_handler,
276-
None,
277315
);
278316
}
279317
pub fn update_environment(&mut self) -> Result<(), error::Error> {
@@ -282,41 +320,22 @@ impl Flagsmith {
282320

283321
fn get_identity_flags_from_document(
284322
&self,
285-
environment: &Environment,
286-
identities_with_overrides_by_identifier: &HashMap<String, Identity>,
323+
eval_context: &EngineEvaluationContext,
287324
identifier: &str,
288325
traits: Vec<Trait>,
289326
) -> Result<Flags, error::Error> {
290-
let identity = self.get_identity_model(environment, identities_with_overrides_by_identifier, identifier, traits.clone())?;
291-
let feature_states =
292-
engine::get_identity_feature_states(environment, &identity, Some(traits.as_ref()));
293-
let flags = Flags::from_feature_states(
294-
&feature_states,
327+
let context_with_identity = add_identity_to_context(eval_context, identifier, &traits);
328+
329+
let result = get_evaluation_result(&context_with_identity);
330+
331+
let flags = Flags::from_evaluation_result(
332+
&result,
295333
self.analytics_processor.clone(),
296334
self.options.default_flag_handler,
297-
Some(&identity.composite_key()),
298335
);
299336
return Ok(flags);
300337
}
301338

302-
fn get_identity_model(
303-
&self,
304-
environment: &Environment,
305-
identities_with_overrides_by_identifier: &HashMap<String, Identity>,
306-
identifier: &str,
307-
traits: Vec<Trait>,
308-
) -> Result<Identity, error::Error> {
309-
let mut identity: Identity;
310-
311-
if identities_with_overrides_by_identifier.contains_key(identifier) {
312-
identity = identities_with_overrides_by_identifier.get(identifier).unwrap().clone();
313-
} else {
314-
identity = Identity::new(identifier.to_string(), environment.api_key.clone());
315-
}
316-
317-
identity.identity_traits = traits;
318-
return Ok(identity.to_owned())
319-
}
320339
fn get_identity_flags_from_api(
321340
&self,
322341
identifier: &str,
@@ -396,9 +415,10 @@ fn update_environment(
396415
&client,
397416
environment_url.clone(),
398417
)?);
399-
for identity in &environment.as_ref().unwrap().identity_overrides {
400-
data.identities_with_overrides_by_identifier.insert(identity.identifier.clone(), identity.clone());
401-
}
418+
419+
let eval_context = environment_to_context(environment.as_ref().unwrap().clone());
420+
data.evaluation_context = Some(eval_context);
421+
402422
data.environment = environment;
403423
return Ok(());
404424
}

src/flagsmith/models.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::flagsmith::analytics::AnalyticsProcessor;
22
use core::f64;
3+
use flagsmith_flag_engine::engine_eval::EvaluationResult;
34
use flagsmith_flag_engine::features::FeatureState;
45
use flagsmith_flag_engine::identities::Trait;
56
use flagsmith_flag_engine::types::{FlagsmithValue, FlagsmithValueType};
@@ -115,6 +116,29 @@ impl Flags {
115116
});
116117
}
117118

119+
pub fn from_evaluation_result(
120+
result: &EvaluationResult,
121+
analytics_processor: Option<AnalyticsProcessor>,
122+
default_flag_handler: Option<fn(&str) -> Flag>,
123+
) -> Flags {
124+
let mut flags: HashMap<String, Flag> = HashMap::new();
125+
for (feature_name, flag_result) in &result.flags {
126+
let flag = Flag {
127+
feature_name: flag_result.name.clone(),
128+
is_default: false,
129+
enabled: flag_result.enabled,
130+
value: flag_result.value.clone(),
131+
feature_id: flag_result.metadata.feature_id,
132+
};
133+
flags.insert(feature_name.clone(), flag);
134+
}
135+
return Flags {
136+
flags,
137+
analytics_processor,
138+
default_flag_handler,
139+
};
140+
}
141+
118142
// Returns a vector of all `Flag` structs
119143
pub fn all_flags(&self) -> Vec<Flag> {
120144
return self.flags.clone().into_values().collect();
@@ -224,7 +248,7 @@ mod tests {
224248
fn can_create_flag_from_feature_state() {
225249
// Given
226250
let feature_state: FeatureState =
227-
serde_json::from_str(FEATURE_STATE_JSON_STRING.clone()).unwrap();
251+
serde_json::from_str(FEATURE_STATE_JSON_STRING).unwrap();
228252
// When
229253
let flag = Flag::from_feature_state(feature_state.clone(), None);
230254
// Then

tests/fixtures/environment.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,30 @@
5454
"segment_id": null,
5555
"enabled": true
5656
}
57+
],
58+
"identity_overrides": [
59+
{
60+
"identifier": "overridden-id",
61+
"identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
62+
"created_date": "2019-08-27T14:53:45.698555Z",
63+
"updated_at": "2023-07-14 16:12:00.000000",
64+
"environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
65+
"identity_features": [
66+
{
67+
"id": 1,
68+
"feature": {
69+
"id": 1,
70+
"name": "some_feature",
71+
"type": "STANDARD"
72+
},
73+
"featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
74+
"feature_state_value": "some-overridden-value",
75+
"enabled": false,
76+
"environment": 1,
77+
"identity": null,
78+
"feature_segment": null
79+
}
80+
]
81+
}
5782
]
5883
}

0 commit comments

Comments
 (0)