From 793ca03e95186ebacb0aa68802acc91c9e49698c Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Tue, 30 Jan 2024 08:15:26 +0530 Subject: [PATCH] feat(offline-mode): Add support for offline handler (#16) * feat(offline-mode): Add support for offline handler * bump minor version --- Cargo.toml | 2 +- src/flagsmith/analytics.rs | 4 +- src/flagsmith/mod.rs | 46 ++++++++++-- src/flagsmith/offline_handler.rs | 44 ++++++++++++ tests/fixtures/environment.json | 58 ++++++++++++++++ tests/integration_test.rs | 116 +++++++++++++++++++++++++++++++ 6 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 src/flagsmith/offline_handler.rs create mode 100644 tests/fixtures/environment.json diff --git a/Cargo.toml b/Cargo.toml index c66d13a..b3743c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flagsmith" -version = "1.3.0" +version = "1.4.0" authors = ["Gagan Trivedi "] edition = "2021" license = "BSD-3-Clause" diff --git a/src/flagsmith/analytics.rs b/src/flagsmith/analytics.rs index 6338e03..6ea4488 100644 --- a/src/flagsmith/analytics.rs +++ b/src/flagsmith/analytics.rs @@ -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)] diff --git a/src/flagsmith/mod.rs b/src/flagsmith/mod.rs index 19999e9..95fab3f 100644 --- a/src/flagsmith/mod.rs +++ b/src/flagsmith/mod.rs @@ -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; @@ -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/"; @@ -26,6 +29,8 @@ pub struct FlagsmithOptions { pub environment_refresh_interval_mills: u64, pub enable_analytics: bool, pub default_flag_handler: Option Flag>, + pub offline_handler: Option>, + pub offline_mode: bool, } impl Default for FlagsmithOptions { @@ -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, } } } @@ -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( @@ -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::(1); + let flagsmith = Flagsmith { client: client.clone(), environment_flags_url, @@ -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 { @@ -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() {} // Then diff --git a/src/flagsmith/offline_handler.rs b/src/flagsmith/offline_handler.rs new file mode 100644 index 0000000..1ce7eb1 --- /dev/null +++ b/src/flagsmith/offline_handler.rs @@ -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 { + // 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"); + } +} diff --git a/tests/fixtures/environment.json b/tests/fixtures/environment.json new file mode 100644 index 0000000..321e40f --- /dev/null +++ b/tests/fixtures/environment.json @@ -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 + } + ] + } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index e911d58..187e47a 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -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}; @@ -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, @@ -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,