Skip to content

Commit

Permalink
feat(offline-mode): Add support for offline handler (#16)
Browse files Browse the repository at this point in the history
* feat(offline-mode): Add support for offline handler

* bump minor version
  • Loading branch information
gagantrivedi authored Jan 30, 2024
1 parent 38a115a commit 793ca03
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 8 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "flagsmith"
version = "1.3.0"
version = "1.4.0"
authors = ["Gagan Trivedi <[email protected]>"]
edition = "2021"
license = "BSD-3-Clause"
Expand Down
4 changes: 2 additions & 2 deletions src/flagsmith/analytics.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use flume;
use log::{debug, warn};
use reqwest::header::HeaderMap;
use serde_json;
use flume;
use std::{collections::HashMap, thread};

use std::sync::{Arc, RwLock};
use std::sync::{Arc, RwLock};
static ANALYTICS_TIMER_IN_MILLI: u64 = 10 * 1000;

#[derive(Clone, Debug)]
Expand Down
46 changes: 41 additions & 5 deletions src/flagsmith/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use self::analytics::AnalyticsProcessor;
use self::models::{Flag, Flags};
use super::error;
use flagsmith_flag_engine::engine;
use flagsmith_flag_engine::environments::builders::build_environment_struct;
use flagsmith_flag_engine::environments::Environment;
Expand All @@ -7,14 +10,14 @@ use flagsmith_flag_engine::segments::Segment;
use log::debug;
use reqwest::header::{self, HeaderMap};
use serde_json::json;
use std::sync::mpsc::{self, SyncSender, TryRecvError};
use std::sync::{Arc, Mutex};
use std::{thread, time::Duration};

mod analytics;

pub mod models;
use self::analytics::AnalyticsProcessor;
use self::models::{Flag, Flags};
use super::error;
use std::sync::mpsc::{self, SyncSender, TryRecvError};
pub mod offline_handler;

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

Expand All @@ -26,6 +29,8 @@ pub struct FlagsmithOptions {
pub environment_refresh_interval_mills: u64,
pub enable_analytics: bool,
pub default_flag_handler: Option<fn(&str) -> Flag>,
pub offline_handler: Option<Box<dyn offline_handler::OfflineHandler + Send + Sync>>,
pub offline_mode: bool,
}

impl Default for FlagsmithOptions {
Expand All @@ -38,6 +43,8 @@ impl Default for FlagsmithOptions {
enable_analytics: false,
environment_refresh_interval_mills: 60 * 1000,
default_flag_handler: None,
offline_handler: None,
offline_mode: false,
}
}
}
Expand Down Expand Up @@ -75,6 +82,20 @@ impl Flagsmith {
let environment_flags_url = format!("{}flags/", flagsmith_options.api_url);
let identities_url = format!("{}identities/", flagsmith_options.api_url);
let environment_url = format!("{}environment-document/", flagsmith_options.api_url);

if flagsmith_options.offline_mode && flagsmith_options.offline_handler.is_none() {
panic!("offline_handler must be set to use offline_mode")
}
if flagsmith_options.default_flag_handler.is_some()
&& flagsmith_options.offline_handler.is_some()
{
panic!("default_flag_handler cannot be used with offline_handler")
}
if flagsmith_options.enable_local_evaluation && flagsmith_options.offline_handler.is_some()
{
panic!("offline_handler cannot be used with local evaluation")
}

// Initialize analytics processor
let analytics_processor = match flagsmith_options.enable_analytics {
true => Some(AnalyticsProcessor::new(
Expand All @@ -85,10 +106,12 @@ impl Flagsmith {
)),
false => None,
};

// Put the environment model behind mutex to
// to share it safely between threads
let ds = Arc::new(Mutex::new(DataStore { environment: None }));
let (tx, rx) = mpsc::sync_channel::<u32>(1);

let flagsmith = Flagsmith {
client: client.clone(),
environment_flags_url,
Expand All @@ -100,10 +123,23 @@ impl Flagsmith {
_polling_thread_tx: tx,
};

if flagsmith.options.offline_handler.is_some() {
let mut data = flagsmith.datastore.lock().unwrap();
data.environment = Some(
flagsmith
.options
.offline_handler
.as_ref()
.unwrap()
.get_environment(),
)
}

// Create a thread to update environment document
// If enabled
let environment_refresh_interval_mills =
flagsmith.options.environment_refresh_interval_mills;

if flagsmith.options.enable_local_evaluation {
let ds = Arc::clone(&ds);
thread::spawn(move || loop {
Expand Down Expand Up @@ -369,7 +405,7 @@ mod tests {
}"#;

#[test]
fn client_implements_send_and_sync(){
fn client_implements_send_and_sync() {
// Given
fn implements_send_and_sync<T: Send + Sync>() {}
// Then
Expand Down
44 changes: 44 additions & 0 deletions src/flagsmith/offline_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use flagsmith_flag_engine::environments::Environment;
use std::fs;

pub trait OfflineHandler {
fn get_environment(&self) -> Environment;
}

pub struct LocalFileHandler {
environment: Environment,
}

impl LocalFileHandler {
pub fn new(environment_document_path: &str) -> Result<Self, std::io::Error> {
// Read the environment document from the specified path
let environment_document = fs::read(environment_document_path)?;

// Deserialize the JSON into EnvironmentModel
let environment: Environment = serde_json::from_slice(&environment_document)?;

// Create and initialize the LocalFileHandler
let handler = LocalFileHandler { environment };

Ok(handler)
}
}

impl OfflineHandler for LocalFileHandler {
fn get_environment(&self) -> Environment {
self.environment.clone()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_local_file_handler() {
let handler = LocalFileHandler::new("tests/fixtures/environment.json").unwrap();

let environment = handler.get_environment();
assert_eq!(environment.api_key, "B62qaMZNwfiqT76p38ggrQ");
}
}
58 changes: 58 additions & 0 deletions tests/fixtures/environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"api_key": "B62qaMZNwfiqT76p38ggrQ",
"project": {
"name": "Test project",
"organisation": {
"feature_analytics": false,
"name": "Test Org",
"id": 1,
"persist_trait_data": true,
"stop_serving_flags": false
},
"id": 1,
"hide_disabled_flags": false,
"segments": [
{
"id": 1,
"name": "Test Segment",
"feature_states":[],
"rules": [
{
"type": "ALL",
"conditions": [],
"rules": [
{
"type": "ALL",
"rules": [],
"conditions": [
{
"operator": "EQUAL",
"property_": "foo",
"value": "bar"
}
]
}
]
}
]
}
]
},
"segment_overrides": [],
"id": 1,
"feature_states": [
{
"multivariate_feature_state_values": [],
"feature_state_value": "some_value",
"id": 1,
"featurestate_uuid": "40eb539d-3713-4720-bbd4-829dbef10d51",
"feature": {
"name": "feature_1",
"type": "STANDARD",
"id": 1
},
"segment_id": null,
"enabled": true
}
]
}
116 changes: 116 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use flagsmith::flagsmith::offline_handler;
use flagsmith::{Flagsmith, FlagsmithOptions};
use flagsmith_flag_engine::identities::Trait;
use flagsmith_flag_engine::types::{FlagsmithValue, FlagsmithValueType};
Expand All @@ -15,6 +16,43 @@ use fixtures::local_eval_flagsmith;
use fixtures::mock_server;
use fixtures::ENVIRONMENT_KEY;

#[rstest]
#[should_panic(expected = "default_flag_handler cannot be used with offline_handler")]
fn test_flagsmith_panics_if_both_default_handler_and_offline_hanlder_are_set(
default_flag_handler: fn(&str) -> flagsmith::Flag,
) {
let handler =
offline_handler::LocalFileHandler::new("tests/fixtures/environment.json").unwrap();
let flagsmith_options = FlagsmithOptions {
default_flag_handler: Some(default_flag_handler),
offline_handler: Some(Box::new(handler)),
..Default::default()
};
Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
}

#[rstest]
#[should_panic(expected = "offline_handler must be set to use offline_mode")]
fn test_flagsmith_panics_if_offline_mode_is_used_without_offline_hanlder() {
let flagsmith_options = FlagsmithOptions {
offline_mode: true,
..Default::default()
};
Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
}

#[rstest]
#[should_panic(expected = "offline_handler cannot be used with local evaluation")]
fn test_flagsmith_should_panic_if_local_evaluation_mode_is_used_with_offline_handler() {
let handler =
offline_handler::LocalFileHandler::new("tests/fixtures/environment.json").unwrap();
let flagsmith_options = FlagsmithOptions {
enable_local_evaluation: true,
offline_handler: Some(Box::new(handler)),
..Default::default()
};
Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
}
#[rstest]
fn test_get_environment_flags_uses_local_environment_when_available(
mock_server: MockServer,
Expand Down Expand Up @@ -82,6 +120,84 @@ fn test_get_environment_flags_calls_api_when_no_local_environment(
);
api_mock.assert();
}

#[rstest]
fn test_offline_mode() {
// Given
let handler =
offline_handler::LocalFileHandler::new("tests/fixtures/environment.json").unwrap();
let flagsmith_options = FlagsmithOptions {
offline_handler: Some(Box::new(handler)),
..Default::default()
};

let flagsmith = Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);

// When
let env_flags = flagsmith.get_environment_flags().unwrap().all_flags();
let identity_flags = flagsmith
.get_identity_flags("test_identity", None)
.unwrap()
.all_flags();

// Then
assert_eq!(env_flags.len(), 1);
assert_eq!(env_flags[0].feature_name, fixtures::FEATURE_1_NAME);
assert_eq!(env_flags[0].feature_id, fixtures::FEATURE_1_ID);
assert_eq!(
env_flags[0].value_as_string().unwrap(),
fixtures::FEATURE_1_STR_VALUE
);

// And
assert_eq!(identity_flags.len(), 1);
assert_eq!(identity_flags[0].feature_name, fixtures::FEATURE_1_NAME);
assert_eq!(identity_flags[0].feature_id, fixtures::FEATURE_1_ID);
assert_eq!(
identity_flags[0].value_as_string().unwrap(),
fixtures::FEATURE_1_STR_VALUE
);
}

#[rstest]
fn test_offline_handler_is_used_if_request_fails(mock_server: MockServer) {
let url = mock_server.url("/api/v1/");
let handler =
offline_handler::LocalFileHandler::new("tests/fixtures/environment.json").unwrap();
let flagsmith_options = FlagsmithOptions {
api_url: url,
offline_handler: Some(Box::new(handler)),
..Default::default()
};

let flagsmith = Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);

// When
let env_flags = flagsmith.get_environment_flags().unwrap().all_flags();
let identity_flags = flagsmith
.get_identity_flags("test_identity", None)
.unwrap()
.all_flags();

// Then
assert_eq!(env_flags.len(), 1);
assert_eq!(env_flags[0].feature_name, fixtures::FEATURE_1_NAME);
assert_eq!(env_flags[0].feature_id, fixtures::FEATURE_1_ID);
assert_eq!(
env_flags[0].value_as_string().unwrap(),
fixtures::FEATURE_1_STR_VALUE
);

// And
assert_eq!(identity_flags.len(), 1);
assert_eq!(identity_flags[0].feature_name, fixtures::FEATURE_1_NAME);
assert_eq!(identity_flags[0].feature_id, fixtures::FEATURE_1_ID);
assert_eq!(
identity_flags[0].value_as_string().unwrap(),
fixtures::FEATURE_1_STR_VALUE
);
}

#[rstest]
fn test_get_identity_flags_uses_local_environment_when_available(
mock_server: MockServer,
Expand Down

0 comments on commit 793ca03

Please sign in to comment.