Skip to content

Commit 06150ff

Browse files
authored
feat: Identity overrides in local evaluation mode (#20)
* chore: Bump engine version * feat: Identity overrides in local evaluation mode * fix: Update environment on client init
1 parent 811fa96 commit 06150ff

File tree

2 files changed

+157
-39
lines changed

2 files changed

+157
-39
lines changed

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ serde = { version = "1.0", features = ["derive"] }
1818
serde_json = "1.0"
1919
reqwest = { version = "0.11", features = ["json", "blocking"] }
2020
url = "2.1"
21-
chrono = { version = "0.4"}
21+
chrono = { version = "0.4" }
2222
log = "0.4"
2323
flume = "0.10.14"
2424

25-
flagsmith-flag-engine = "0.3.0"
25+
flagsmith-flag-engine = "0.4.0"
2626

2727
[dev-dependencies]
2828
httpmock = "0.6"
29-
rstest = "0.12.0"
29+
rstest = "0.12.0"

src/flagsmith/mod.rs

Lines changed: 154 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use flagsmith_flag_engine::segments::Segment;
1010
use log::debug;
1111
use reqwest::header::{self, HeaderMap};
1212
use serde_json::json;
13+
use std::collections::HashMap;
1314
use std::sync::mpsc::{self, SyncSender, TryRecvError};
1415
use std::sync::{Arc, Mutex};
1516
use std::{thread, time::Duration};
@@ -62,6 +63,7 @@ pub struct Flagsmith {
6263

6364
struct DataStore {
6465
environment: Option<Environment>,
66+
identities_with_overrides_by_identifier: HashMap<String, Identity>,
6567
}
6668

6769
impl Flagsmith {
@@ -109,7 +111,7 @@ impl Flagsmith {
109111

110112
// Put the environment model behind mutex to
111113
// to share it safely between threads
112-
let ds = Arc::new(Mutex::new(DataStore { environment: None }));
114+
let ds = Arc::new(Mutex::new(DataStore { environment: None, identities_with_overrides_by_identifier: HashMap::new() }));
113115
let (tx, rx) = mpsc::sync_channel::<u32>(1);
114116

115117
let flagsmith = Flagsmith {
@@ -141,6 +143,10 @@ impl Flagsmith {
141143
flagsmith.options.environment_refresh_interval_mills;
142144

143145
if flagsmith.options.enable_local_evaluation {
146+
// Update environment once...
147+
update_environment(&client, &ds, &environment_url).unwrap();
148+
149+
// ...and continue updating in the background
144150
let ds = Arc::clone(&ds);
145151
thread::spawn(move || loop {
146152
match rx.try_recv() {
@@ -150,14 +156,8 @@ impl Flagsmith {
150156
}
151157
Err(TryRecvError::Empty) => {}
152158
}
153-
154-
let environment = Some(
155-
get_environment_from_api(&client, environment_url.clone())
156-
.expect("updating environment document failed"),
157-
);
158-
let mut data = ds.lock().unwrap();
159-
data.environment = environment;
160159
thread::sleep(Duration::from_millis(environment_refresh_interval_mills));
160+
update_environment(&client, &ds, &environment_url).unwrap();
161161
});
162162
}
163163
return flagsmith;
@@ -201,7 +201,7 @@ impl Flagsmith {
201201
let traits = traits.unwrap_or(vec![]);
202202
if data.environment.is_some() {
203203
let environment = data.environment.as_ref().unwrap();
204-
return self.get_identity_flags_from_document(environment, identifier, traits);
204+
return self.get_identity_flags_from_document(environment, &data.identities_with_overrides_by_identifier, identifier, traits);
205205
}
206206
return self.default_handler_if_err(self.get_identity_flags_from_api(identifier, traits));
207207
}
@@ -219,8 +219,9 @@ impl Flagsmith {
219219
));
220220
}
221221
let environment = data.environment.as_ref().unwrap();
222+
let identities_with_overrides_by_identifier = &data.identities_with_overrides_by_identifier;
222223
let identity_model =
223-
self.build_identity_model(environment, identifier, traits.clone().unwrap_or(vec![]))?;
224+
self.get_identity_model(&environment, &identities_with_overrides_by_identifier, identifier, traits.clone().unwrap_or(vec![]))?;
224225
let segments = get_identity_segments(environment, &identity_model, traits.as_ref());
225226
return Ok(segments);
226227
}
@@ -254,21 +255,17 @@ impl Flagsmith {
254255
);
255256
}
256257
pub fn update_environment(&mut self) -> Result<(), error::Error> {
257-
let mut data = self.datastore.lock().unwrap();
258-
data.environment = Some(get_environment_from_api(
259-
&self.client,
260-
self.environment_url.clone(),
261-
)?);
262-
return Ok(());
258+
return update_environment(&self.client, &self.datastore, &self.environment_url);
263259
}
264260

265261
fn get_identity_flags_from_document(
266262
&self,
267263
environment: &Environment,
264+
identities_with_overrides_by_identifier: &HashMap<String, Identity>,
268265
identifier: &str,
269266
traits: Vec<Trait>,
270267
) -> Result<Flags, error::Error> {
271-
let identity = self.build_identity_model(environment, identifier, traits.clone())?;
268+
let identity = self.get_identity_model(environment, identities_with_overrides_by_identifier, identifier, traits.clone())?;
272269
let feature_states =
273270
engine::get_identity_feature_states(environment, &identity, Some(traits.as_ref()));
274271
let flags = Flags::from_feature_states(
@@ -280,15 +277,23 @@ impl Flagsmith {
280277
return Ok(flags);
281278
}
282279

283-
fn build_identity_model(
280+
fn get_identity_model(
284281
&self,
285282
environment: &Environment,
283+
identities_with_overrides_by_identifier: &HashMap<String, Identity>,
286284
identifier: &str,
287285
traits: Vec<Trait>,
288286
) -> Result<Identity, error::Error> {
289-
let mut identity = Identity::new(identifier.to_string(), environment.api_key.clone());
287+
let mut identity: Identity;
288+
289+
if identities_with_overrides_by_identifier.contains_key(identifier) {
290+
identity = identities_with_overrides_by_identifier.get(identifier).unwrap().clone();
291+
} else {
292+
identity = Identity::new(identifier.to_string(), environment.api_key.clone());
293+
}
294+
290295
identity.identity_traits = traits;
291-
Ok(identity)
296+
return Ok(identity.to_owned())
292297
}
293298
fn get_identity_flags_from_api(
294299
&self,
@@ -358,6 +363,23 @@ fn get_environment_from_api(
358363
return Ok(environment);
359364
}
360365

366+
fn update_environment(
367+
client: &reqwest::blocking::Client,
368+
datastore: &Arc<Mutex<DataStore>>,
369+
environment_url: &String,
370+
) -> Result<(), error::Error> {
371+
let mut data = datastore.lock().unwrap();
372+
let environment = Some(get_environment_from_api(
373+
&client,
374+
environment_url.clone(),
375+
)?);
376+
for identity in &environment.as_ref().unwrap().identity_overrides {
377+
data.identities_with_overrides_by_identifier.insert(identity.identifier.clone(), identity.clone());
378+
}
379+
data.environment = environment;
380+
return Ok(());
381+
}
382+
361383
fn get_json_response(
362384
client: &reqwest::blocking::Client,
363385
method: reqwest::Method,
@@ -385,24 +407,87 @@ mod tests {
385407
use httpmock::prelude::*;
386408

387409
static ENVIRONMENT_JSON: &str = r#"{
388-
"api_key": "B62qaMZNwfiqT76p38ggrQ",
389-
"project": {
390-
"name": "Test project",
391-
"organisation": {
392-
"feature_analytics": false,
393-
"name": "Test Org",
394-
"id": 1,
395-
"persist_trait_data": true,
396-
"stop_serving_flags": false
410+
"api_key": "B62qaMZNwfiqT76p38ggrQ",
411+
"project": {
412+
"name": "Test project",
413+
"organisation": {
414+
"feature_analytics": false,
415+
"name": "Test Org",
416+
"id": 1,
417+
"persist_trait_data": true,
418+
"stop_serving_flags": false
419+
},
420+
"segments": [],
421+
"id": 1,
422+
"hide_disabled_flags": false
423+
},
424+
"segment_overrides": [],
425+
"id": 1,
426+
"feature_states": [
427+
{
428+
"multivariate_feature_state_values": [],
429+
"feature_state_value": "some-value",
430+
"id": 1,
431+
"featurestate_uuid": "40eb539d-3713-4720-bbd4-829dbef10d51",
432+
"feature": {
433+
"name": "some_feature",
434+
"type": "STANDARD",
435+
"id": 1
436+
},
437+
"segment_id": null,
438+
"enabled": true
439+
},
440+
{
441+
"feature": {
442+
"id": 83755,
443+
"name": "test_mv",
444+
"type": "MULTIVARIATE"
445+
},
446+
"enabled": false,
447+
"django_id": 482285,
448+
"feature_segment": null,
449+
"featurestate_uuid": "c3af5fbf-39ba-422c-a846-f2fea952b37c",
450+
"feature_state_value": "1111",
451+
"multivariate_feature_state_values": [
452+
{
453+
"multivariate_feature_option": {
454+
"value": "8888",
455+
"id": 11516
397456
},
457+
"percentage_allocation": 100.0,
458+
"id": 38451,
459+
"mv_fs_value_uuid": "a4299c73-2430-47e4-9185-42accd01686c"
460+
}
461+
]
462+
}
463+
],
464+
"updated_at": "2023-07-14 16:12:00.000000",
465+
"identity_overrides": [
466+
{
467+
"identifier": "overridden-id",
468+
"identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
469+
"created_date": "2019-08-27T14:53:45.698555Z",
470+
"updated_at": "2023-07-14 16:12:00.000000",
471+
"environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
472+
"identity_features": [
473+
{
398474
"id": 1,
399-
"hide_disabled_flags": false,
400-
"segments": []
401-
},
402-
"segment_overrides": [],
403-
"id": 1,
404-
"feature_states": []
405-
}"#;
475+
"feature": {
476+
"id": 1,
477+
"name": "some_feature",
478+
"type": "STANDARD"
479+
},
480+
"featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
481+
"feature_state_value": "some-overridden-value",
482+
"enabled": false,
483+
"environment": 1,
484+
"identity": null,
485+
"feature_segment": null
486+
}
487+
]
488+
}
489+
]
490+
}"#;
406491

407492
#[test]
408493
fn client_implements_send_and_sync() {
@@ -472,4 +557,37 @@ mod tests {
472557
// for each subsequent refresh
473558
api_mock.assert_hits(3);
474559
}
560+
561+
#[test]
562+
fn test_local_evaluation_identity_override_evaluate_expected() {
563+
// Given
564+
let environment_key = "ser.test_environment_key";
565+
let response_body: serde_json::Value = serde_json::from_str(ENVIRONMENT_JSON).unwrap();
566+
567+
let mock_server = MockServer::start();
568+
mock_server.mock(|when, then| {
569+
when.method(GET)
570+
.path("/api/v1/environment-document/")
571+
.header("X-Environment-Key", environment_key);
572+
then.status(200).json_body(response_body);
573+
});
574+
575+
let url = mock_server.url("/api/v1/");
576+
577+
let flagsmith_options = FlagsmithOptions {
578+
api_url: url,
579+
environment_refresh_interval_mills: 100,
580+
enable_local_evaluation: true,
581+
..Default::default()
582+
};
583+
584+
// When
585+
let mut _flagsmith = Flagsmith::new(environment_key.to_string(), flagsmith_options);
586+
587+
// Then
588+
let flags = _flagsmith.get_environment_flags();
589+
let identity_flags = _flagsmith.get_identity_flags("overridden-id", None);
590+
assert_eq!(flags.unwrap().get_feature_value_as_string("some_feature").unwrap().to_owned(), "some-value");
591+
assert_eq!(identity_flags.unwrap().get_feature_value_as_string("some_feature").unwrap().to_owned(), "some-overridden-value");
592+
}
475593
}

0 commit comments

Comments
 (0)