Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(offline-mode): Add support for offline handler #16

Merged
merged 2 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading