Skip to content

Commit 793ca03

Browse files
authored
feat(offline-mode): Add support for offline handler (#16)
* feat(offline-mode): Add support for offline handler * bump minor version
1 parent 38a115a commit 793ca03

File tree

6 files changed

+262
-8
lines changed

6 files changed

+262
-8
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "flagsmith"
3-
version = "1.3.0"
3+
version = "1.4.0"
44
authors = ["Gagan Trivedi <[email protected]>"]
55
edition = "2021"
66
license = "BSD-3-Clause"

src/flagsmith/analytics.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
use flume;
12
use log::{debug, warn};
23
use reqwest::header::HeaderMap;
34
use serde_json;
4-
use flume;
55
use std::{collections::HashMap, thread};
66

7-
use std::sync::{Arc, RwLock};
7+
use std::sync::{Arc, RwLock};
88
static ANALYTICS_TIMER_IN_MILLI: u64 = 10 * 1000;
99

1010
#[derive(Clone, Debug)]

src/flagsmith/mod.rs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use self::analytics::AnalyticsProcessor;
2+
use self::models::{Flag, Flags};
3+
use super::error;
14
use flagsmith_flag_engine::engine;
25
use flagsmith_flag_engine::environments::builders::build_environment_struct;
36
use flagsmith_flag_engine::environments::Environment;
@@ -7,14 +10,14 @@ use flagsmith_flag_engine::segments::Segment;
710
use log::debug;
811
use reqwest::header::{self, HeaderMap};
912
use serde_json::json;
13+
use std::sync::mpsc::{self, SyncSender, TryRecvError};
1014
use std::sync::{Arc, Mutex};
1115
use std::{thread, time::Duration};
16+
1217
mod analytics;
18+
1319
pub mod models;
14-
use self::analytics::AnalyticsProcessor;
15-
use self::models::{Flag, Flags};
16-
use super::error;
17-
use std::sync::mpsc::{self, SyncSender, TryRecvError};
20+
pub mod offline_handler;
1821

1922
const DEFAULT_API_URL: &str = "https://edge.api.flagsmith.com/api/v1/";
2023

@@ -26,6 +29,8 @@ pub struct FlagsmithOptions {
2629
pub environment_refresh_interval_mills: u64,
2730
pub enable_analytics: bool,
2831
pub default_flag_handler: Option<fn(&str) -> Flag>,
32+
pub offline_handler: Option<Box<dyn offline_handler::OfflineHandler + Send + Sync>>,
33+
pub offline_mode: bool,
2934
}
3035

3136
impl Default for FlagsmithOptions {
@@ -38,6 +43,8 @@ impl Default for FlagsmithOptions {
3843
enable_analytics: false,
3944
environment_refresh_interval_mills: 60 * 1000,
4045
default_flag_handler: None,
46+
offline_handler: None,
47+
offline_mode: false,
4148
}
4249
}
4350
}
@@ -75,6 +82,20 @@ impl Flagsmith {
7582
let environment_flags_url = format!("{}flags/", flagsmith_options.api_url);
7683
let identities_url = format!("{}identities/", flagsmith_options.api_url);
7784
let environment_url = format!("{}environment-document/", flagsmith_options.api_url);
85+
86+
if flagsmith_options.offline_mode && flagsmith_options.offline_handler.is_none() {
87+
panic!("offline_handler must be set to use offline_mode")
88+
}
89+
if flagsmith_options.default_flag_handler.is_some()
90+
&& flagsmith_options.offline_handler.is_some()
91+
{
92+
panic!("default_flag_handler cannot be used with offline_handler")
93+
}
94+
if flagsmith_options.enable_local_evaluation && flagsmith_options.offline_handler.is_some()
95+
{
96+
panic!("offline_handler cannot be used with local evaluation")
97+
}
98+
7899
// Initialize analytics processor
79100
let analytics_processor = match flagsmith_options.enable_analytics {
80101
true => Some(AnalyticsProcessor::new(
@@ -85,10 +106,12 @@ impl Flagsmith {
85106
)),
86107
false => None,
87108
};
109+
88110
// Put the environment model behind mutex to
89111
// to share it safely between threads
90112
let ds = Arc::new(Mutex::new(DataStore { environment: None }));
91113
let (tx, rx) = mpsc::sync_channel::<u32>(1);
114+
92115
let flagsmith = Flagsmith {
93116
client: client.clone(),
94117
environment_flags_url,
@@ -100,10 +123,23 @@ impl Flagsmith {
100123
_polling_thread_tx: tx,
101124
};
102125

126+
if flagsmith.options.offline_handler.is_some() {
127+
let mut data = flagsmith.datastore.lock().unwrap();
128+
data.environment = Some(
129+
flagsmith
130+
.options
131+
.offline_handler
132+
.as_ref()
133+
.unwrap()
134+
.get_environment(),
135+
)
136+
}
137+
103138
// Create a thread to update environment document
104139
// If enabled
105140
let environment_refresh_interval_mills =
106141
flagsmith.options.environment_refresh_interval_mills;
142+
107143
if flagsmith.options.enable_local_evaluation {
108144
let ds = Arc::clone(&ds);
109145
thread::spawn(move || loop {
@@ -369,7 +405,7 @@ mod tests {
369405
}"#;
370406

371407
#[test]
372-
fn client_implements_send_and_sync(){
408+
fn client_implements_send_and_sync() {
373409
// Given
374410
fn implements_send_and_sync<T: Send + Sync>() {}
375411
// Then

src/flagsmith/offline_handler.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use flagsmith_flag_engine::environments::Environment;
2+
use std::fs;
3+
4+
pub trait OfflineHandler {
5+
fn get_environment(&self) -> Environment;
6+
}
7+
8+
pub struct LocalFileHandler {
9+
environment: Environment,
10+
}
11+
12+
impl LocalFileHandler {
13+
pub fn new(environment_document_path: &str) -> Result<Self, std::io::Error> {
14+
// Read the environment document from the specified path
15+
let environment_document = fs::read(environment_document_path)?;
16+
17+
// Deserialize the JSON into EnvironmentModel
18+
let environment: Environment = serde_json::from_slice(&environment_document)?;
19+
20+
// Create and initialize the LocalFileHandler
21+
let handler = LocalFileHandler { environment };
22+
23+
Ok(handler)
24+
}
25+
}
26+
27+
impl OfflineHandler for LocalFileHandler {
28+
fn get_environment(&self) -> Environment {
29+
self.environment.clone()
30+
}
31+
}
32+
33+
#[cfg(test)]
34+
mod tests {
35+
use super::*;
36+
37+
#[test]
38+
fn test_local_file_handler() {
39+
let handler = LocalFileHandler::new("tests/fixtures/environment.json").unwrap();
40+
41+
let environment = handler.get_environment();
42+
assert_eq!(environment.api_key, "B62qaMZNwfiqT76p38ggrQ");
43+
}
44+
}

tests/fixtures/environment.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"api_key": "B62qaMZNwfiqT76p38ggrQ",
3+
"project": {
4+
"name": "Test project",
5+
"organisation": {
6+
"feature_analytics": false,
7+
"name": "Test Org",
8+
"id": 1,
9+
"persist_trait_data": true,
10+
"stop_serving_flags": false
11+
},
12+
"id": 1,
13+
"hide_disabled_flags": false,
14+
"segments": [
15+
{
16+
"id": 1,
17+
"name": "Test Segment",
18+
"feature_states":[],
19+
"rules": [
20+
{
21+
"type": "ALL",
22+
"conditions": [],
23+
"rules": [
24+
{
25+
"type": "ALL",
26+
"rules": [],
27+
"conditions": [
28+
{
29+
"operator": "EQUAL",
30+
"property_": "foo",
31+
"value": "bar"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
]
38+
}
39+
]
40+
},
41+
"segment_overrides": [],
42+
"id": 1,
43+
"feature_states": [
44+
{
45+
"multivariate_feature_state_values": [],
46+
"feature_state_value": "some_value",
47+
"id": 1,
48+
"featurestate_uuid": "40eb539d-3713-4720-bbd4-829dbef10d51",
49+
"feature": {
50+
"name": "feature_1",
51+
"type": "STANDARD",
52+
"id": 1
53+
},
54+
"segment_id": null,
55+
"enabled": true
56+
}
57+
]
58+
}

tests/integration_test.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use flagsmith::flagsmith::offline_handler;
12
use flagsmith::{Flagsmith, FlagsmithOptions};
23
use flagsmith_flag_engine::identities::Trait;
34
use flagsmith_flag_engine::types::{FlagsmithValue, FlagsmithValueType};
@@ -15,6 +16,43 @@ use fixtures::local_eval_flagsmith;
1516
use fixtures::mock_server;
1617
use fixtures::ENVIRONMENT_KEY;
1718

19+
#[rstest]
20+
#[should_panic(expected = "default_flag_handler cannot be used with offline_handler")]
21+
fn test_flagsmith_panics_if_both_default_handler_and_offline_hanlder_are_set(
22+
default_flag_handler: fn(&str) -> flagsmith::Flag,
23+
) {
24+
let handler =
25+
offline_handler::LocalFileHandler::new("tests/fixtures/environment.json").unwrap();
26+
let flagsmith_options = FlagsmithOptions {
27+
default_flag_handler: Some(default_flag_handler),
28+
offline_handler: Some(Box::new(handler)),
29+
..Default::default()
30+
};
31+
Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
32+
}
33+
34+
#[rstest]
35+
#[should_panic(expected = "offline_handler must be set to use offline_mode")]
36+
fn test_flagsmith_panics_if_offline_mode_is_used_without_offline_hanlder() {
37+
let flagsmith_options = FlagsmithOptions {
38+
offline_mode: true,
39+
..Default::default()
40+
};
41+
Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
42+
}
43+
44+
#[rstest]
45+
#[should_panic(expected = "offline_handler cannot be used with local evaluation")]
46+
fn test_flagsmith_should_panic_if_local_evaluation_mode_is_used_with_offline_handler() {
47+
let handler =
48+
offline_handler::LocalFileHandler::new("tests/fixtures/environment.json").unwrap();
49+
let flagsmith_options = FlagsmithOptions {
50+
enable_local_evaluation: true,
51+
offline_handler: Some(Box::new(handler)),
52+
..Default::default()
53+
};
54+
Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
55+
}
1856
#[rstest]
1957
fn test_get_environment_flags_uses_local_environment_when_available(
2058
mock_server: MockServer,
@@ -82,6 +120,84 @@ fn test_get_environment_flags_calls_api_when_no_local_environment(
82120
);
83121
api_mock.assert();
84122
}
123+
124+
#[rstest]
125+
fn test_offline_mode() {
126+
// Given
127+
let handler =
128+
offline_handler::LocalFileHandler::new("tests/fixtures/environment.json").unwrap();
129+
let flagsmith_options = FlagsmithOptions {
130+
offline_handler: Some(Box::new(handler)),
131+
..Default::default()
132+
};
133+
134+
let flagsmith = Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
135+
136+
// When
137+
let env_flags = flagsmith.get_environment_flags().unwrap().all_flags();
138+
let identity_flags = flagsmith
139+
.get_identity_flags("test_identity", None)
140+
.unwrap()
141+
.all_flags();
142+
143+
// Then
144+
assert_eq!(env_flags.len(), 1);
145+
assert_eq!(env_flags[0].feature_name, fixtures::FEATURE_1_NAME);
146+
assert_eq!(env_flags[0].feature_id, fixtures::FEATURE_1_ID);
147+
assert_eq!(
148+
env_flags[0].value_as_string().unwrap(),
149+
fixtures::FEATURE_1_STR_VALUE
150+
);
151+
152+
// And
153+
assert_eq!(identity_flags.len(), 1);
154+
assert_eq!(identity_flags[0].feature_name, fixtures::FEATURE_1_NAME);
155+
assert_eq!(identity_flags[0].feature_id, fixtures::FEATURE_1_ID);
156+
assert_eq!(
157+
identity_flags[0].value_as_string().unwrap(),
158+
fixtures::FEATURE_1_STR_VALUE
159+
);
160+
}
161+
162+
#[rstest]
163+
fn test_offline_handler_is_used_if_request_fails(mock_server: MockServer) {
164+
let url = mock_server.url("/api/v1/");
165+
let handler =
166+
offline_handler::LocalFileHandler::new("tests/fixtures/environment.json").unwrap();
167+
let flagsmith_options = FlagsmithOptions {
168+
api_url: url,
169+
offline_handler: Some(Box::new(handler)),
170+
..Default::default()
171+
};
172+
173+
let flagsmith = Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
174+
175+
// When
176+
let env_flags = flagsmith.get_environment_flags().unwrap().all_flags();
177+
let identity_flags = flagsmith
178+
.get_identity_flags("test_identity", None)
179+
.unwrap()
180+
.all_flags();
181+
182+
// Then
183+
assert_eq!(env_flags.len(), 1);
184+
assert_eq!(env_flags[0].feature_name, fixtures::FEATURE_1_NAME);
185+
assert_eq!(env_flags[0].feature_id, fixtures::FEATURE_1_ID);
186+
assert_eq!(
187+
env_flags[0].value_as_string().unwrap(),
188+
fixtures::FEATURE_1_STR_VALUE
189+
);
190+
191+
// And
192+
assert_eq!(identity_flags.len(), 1);
193+
assert_eq!(identity_flags[0].feature_name, fixtures::FEATURE_1_NAME);
194+
assert_eq!(identity_flags[0].feature_id, fixtures::FEATURE_1_ID);
195+
assert_eq!(
196+
identity_flags[0].value_as_string().unwrap(),
197+
fixtures::FEATURE_1_STR_VALUE
198+
);
199+
}
200+
85201
#[rstest]
86202
fn test_get_identity_flags_uses_local_environment_when_available(
87203
mock_server: MockServer,

0 commit comments

Comments
 (0)