Skip to content
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
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ url = "2.1"
chrono = { version = "0.4" }
log = "0.4"
flume = "0.10.14"

flagsmith-flag-engine = "0.4.0"
flagsmith-flag-engine = "0.5.0"

[dev-dependencies]
httpmock = "0.6"
Expand Down
7 changes: 5 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::convert::From;
use std::error;
use std::fmt;

/// Wraps several types of errors.
Expand All @@ -25,12 +26,14 @@ impl Error{
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.kind {
ErrorKind::FlagsmithClientError => write!(f, "Flagsmith API error: {}", &self.msg),
ErrorKind::FlagsmithAPIError => write!(f, "Flagsmith client error: {}", &self.msg),
ErrorKind::FlagsmithClientError => write!(f, "Flagsmith client error: {}", &self.msg),
ErrorKind::FlagsmithAPIError => write!(f, "Flagsmith API error: {}", &self.msg),
}
}
}

impl error::Error for Error {}

impl From<url::ParseError> for Error {
fn from(e: url::ParseError) -> Self {
Error::new(ErrorKind::FlagsmithClientError, e.to_string())
Expand Down
144 changes: 82 additions & 62 deletions src/flagsmith/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use self::analytics::AnalyticsProcessor;
use self::models::{Flag, Flags};
use super::error;
use flagsmith_flag_engine::engine;
use flagsmith_flag_engine::engine::get_evaluation_result;
use flagsmith_flag_engine::engine_eval::{
add_identity_to_context, environment_to_context, EngineEvaluationContext, SegmentSource,
};
use flagsmith_flag_engine::environments::builders::build_environment_struct;
use flagsmith_flag_engine::environments::Environment;
use flagsmith_flag_engine::identities::{Identity, Trait};
use flagsmith_flag_engine::segments::evaluator::get_identity_segments;
use flagsmith_flag_engine::identities::Trait;
use flagsmith_flag_engine::segments::Segment;
use log::debug;
use models::SDKTrait;
Expand Down Expand Up @@ -70,7 +72,7 @@ pub struct Flagsmith {

struct DataStore {
environment: Option<Environment>,
identities_with_overrides_by_identifier: HashMap<String, Identity>,
evaluation_context: Option<EngineEvaluationContext>,
}

impl Flagsmith {
Expand Down Expand Up @@ -108,6 +110,9 @@ impl Flagsmith {
{
panic!("offline_handler cannot be used with local evaluation")
}
if flagsmith_options.enable_local_evaluation && !environment_key.starts_with("ser.") {
panic!("In order to use local evaluation, please use a server-side environment key (starts with 'ser.')")
}

// Initialize analytics processor
let analytics_processor = match flagsmith_options.enable_analytics {
Expand All @@ -122,7 +127,10 @@ impl Flagsmith {

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

let flagsmith = Flagsmith {
Expand All @@ -138,14 +146,18 @@ impl Flagsmith {

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(),
)
let environment = flagsmith
.options
.offline_handler
.as_ref()
.unwrap()
.get_environment();

// Create evaluation context from offline environment
let eval_context = environment_to_context(environment.clone());
data.evaluation_context = Some(eval_context);

data.environment = Some(environment);
}

// Create a thread to update environment document
Expand All @@ -155,8 +167,10 @@ impl Flagsmith {

if flagsmith.options.enable_local_evaluation {
// Update environment once...
update_environment(&client, &ds, &environment_url).unwrap();

if let Err(e) = update_environment(&client, &ds, &environment_url) {
log::warn!("Failed to fetch environment on initialization: {}. Will retry in background.", e);
}

// ...and continue updating in the background
let ds = Arc::clone(&ds);
thread::spawn(move || loop {
Expand All @@ -168,17 +182,19 @@ impl Flagsmith {
Err(TryRecvError::Empty) => {}
}
thread::sleep(Duration::from_millis(environment_refresh_interval_mills));
update_environment(&client, &ds, &environment_url).unwrap();
if let Err(e) = update_environment(&client, &ds, &environment_url) {
log::warn!("Failed to update environment: {}. Will retry on next interval.", e);
}
});
}
return flagsmith;
}
//Returns `Flags` struct holding all the flags for the current environment.
pub fn get_environment_flags(&self) -> Result<models::Flags, error::Error> {
let data = self.datastore.lock().unwrap();
if data.environment.is_some() {
let environment = data.environment.as_ref().unwrap();
return Ok(self.get_environment_flags_from_document(environment));
if data.evaluation_context.is_some() {
let eval_context = data.evaluation_context.as_ref().unwrap();
return Ok(self.get_environment_flags_from_document(eval_context));
}
return self.default_handler_if_err(self.get_environment_flags_from_api());
}
Expand Down Expand Up @@ -211,12 +227,11 @@ impl Flagsmith {
) -> Result<Flags, error::Error> {
let data = self.datastore.lock().unwrap();
let traits = traits.unwrap_or(vec![]);
if data.environment.is_some() {
let environment = data.environment.as_ref().unwrap();
if data.evaluation_context.is_some() {
let eval_context = data.evaluation_context.as_ref().unwrap();
let engine_traits: Vec<Trait> = traits.into_iter().map(|t| t.into()).collect();
return self.get_identity_flags_from_document(
environment,
&data.identities_with_overrides_by_identifier,
eval_context,
identifier,
engine_traits,
);
Expand All @@ -234,17 +249,33 @@ impl Flagsmith {
traits: Option<Vec<Trait>>,
) -> Result<Vec<Segment>, error::Error> {
let data = self.datastore.lock().unwrap();
if data.environment.is_none() {
if data.evaluation_context.is_none() {
return Err(error::Error::new(
error::ErrorKind::FlagsmithClientError,
"Local evaluation required to obtain identity segments.".to_string(),
));
}
let environment = data.environment.as_ref().unwrap();
let identities_with_overrides_by_identifier = &data.identities_with_overrides_by_identifier;
let identity_model =
self.get_identity_model(&environment, &identities_with_overrides_by_identifier, identifier, traits.clone().unwrap_or(vec![]))?;
let segments = get_identity_segments(environment, &identity_model, traits.as_ref());
let eval_context = data.evaluation_context.as_ref().unwrap();
let traits = traits.unwrap_or(vec![]);

let context_with_identity = add_identity_to_context(eval_context, identifier, &traits);

let result = get_evaluation_result(&context_with_identity);

let segments: Vec<Segment> = result
.segments
.iter()
.filter(|seg_result| {
seg_result.metadata.source == SegmentSource::Api
})
.map(|seg_result| Segment {
id: seg_result.metadata.segment_id.unwrap_or(0) as u32,
name: seg_result.name.clone(),
rules: vec![],
feature_states: vec![],
})
.collect();

return Ok(segments);
}

Expand All @@ -268,12 +299,19 @@ impl Flagsmith {
}
}
}
fn get_environment_flags_from_document(&self, environment: &Environment) -> models::Flags {
return models::Flags::from_feature_states(
&environment.feature_states,
fn get_environment_flags_from_document(&self, eval_context: &EngineEvaluationContext) -> models::Flags {
// Clear segments and identity for environment evaluation
let environment_eval_ctx = EngineEvaluationContext {
environment: eval_context.environment.clone(),
features: eval_context.features.clone(),
segments: HashMap::new(),
identity: None,
};
let result = get_evaluation_result(&environment_eval_ctx);
return models::Flags::from_evaluation_result(
&result,
self.analytics_processor.clone(),
self.options.default_flag_handler,
None,
);
}
pub fn update_environment(&mut self) -> Result<(), error::Error> {
Expand All @@ -282,41 +320,22 @@ impl Flagsmith {

fn get_identity_flags_from_document(
&self,
environment: &Environment,
identities_with_overrides_by_identifier: &HashMap<String, Identity>,
eval_context: &EngineEvaluationContext,
identifier: &str,
traits: Vec<Trait>,
) -> Result<Flags, error::Error> {
let identity = self.get_identity_model(environment, identities_with_overrides_by_identifier, identifier, traits.clone())?;
let feature_states =
engine::get_identity_feature_states(environment, &identity, Some(traits.as_ref()));
let flags = Flags::from_feature_states(
&feature_states,
let context_with_identity = add_identity_to_context(eval_context, identifier, &traits);

let result = get_evaluation_result(&context_with_identity);

let flags = Flags::from_evaluation_result(
&result,
self.analytics_processor.clone(),
self.options.default_flag_handler,
Some(&identity.composite_key()),
);
return Ok(flags);
}

fn get_identity_model(
&self,
environment: &Environment,
identities_with_overrides_by_identifier: &HashMap<String, Identity>,
identifier: &str,
traits: Vec<Trait>,
) -> Result<Identity, error::Error> {
let mut identity: Identity;

if identities_with_overrides_by_identifier.contains_key(identifier) {
identity = identities_with_overrides_by_identifier.get(identifier).unwrap().clone();
} else {
identity = Identity::new(identifier.to_string(), environment.api_key.clone());
}

identity.identity_traits = traits;
return Ok(identity.to_owned())
}
fn get_identity_flags_from_api(
&self,
identifier: &str,
Expand Down Expand Up @@ -396,9 +415,10 @@ fn update_environment(
&client,
environment_url.clone(),
)?);
for identity in &environment.as_ref().unwrap().identity_overrides {
data.identities_with_overrides_by_identifier.insert(identity.identifier.clone(), identity.clone());
}

let eval_context = environment_to_context(environment.as_ref().unwrap().clone());
data.evaluation_context = Some(eval_context);

data.environment = environment;
return Ok(());
}
Expand Down
26 changes: 25 additions & 1 deletion src/flagsmith/models.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::flagsmith::analytics::AnalyticsProcessor;
use core::f64;
use flagsmith_flag_engine::engine_eval::EvaluationResult;
use flagsmith_flag_engine::features::FeatureState;
use flagsmith_flag_engine::identities::Trait;
use flagsmith_flag_engine::types::{FlagsmithValue, FlagsmithValueType};
Expand Down Expand Up @@ -115,6 +116,29 @@ impl Flags {
});
}

pub fn from_evaluation_result(
result: &EvaluationResult,
analytics_processor: Option<AnalyticsProcessor>,
default_flag_handler: Option<fn(&str) -> Flag>,
) -> Flags {
let mut flags: HashMap<String, Flag> = HashMap::new();
for (feature_name, flag_result) in &result.flags {
let flag = Flag {
feature_name: flag_result.name.clone(),
is_default: false,
enabled: flag_result.enabled,
value: flag_result.value.clone(),
feature_id: flag_result.metadata.feature_id,
};
flags.insert(feature_name.clone(), flag);
}
return Flags {
flags,
analytics_processor,
default_flag_handler,
};
}

// Returns a vector of all `Flag` structs
pub fn all_flags(&self) -> Vec<Flag> {
return self.flags.clone().into_values().collect();
Expand Down Expand Up @@ -224,7 +248,7 @@ mod tests {
fn can_create_flag_from_feature_state() {
// Given
let feature_state: FeatureState =
serde_json::from_str(FEATURE_STATE_JSON_STRING.clone()).unwrap();
serde_json::from_str(FEATURE_STATE_JSON_STRING).unwrap();
// When
let flag = Flag::from_feature_state(feature_state.clone(), None);
// Then
Expand Down
25 changes: 25 additions & 0 deletions tests/fixtures/environment.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,30 @@
"segment_id": null,
"enabled": true
}
],
"identity_overrides": [
{
"identifier": "overridden-id",
"identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
"created_date": "2019-08-27T14:53:45.698555Z",
"updated_at": "2023-07-14 16:12:00.000000",
"environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
"identity_features": [
{
"id": 1,
"feature": {
"id": 1,
"name": "some_feature",
"type": "STANDARD"
},
"featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
"feature_state_value": "some-overridden-value",
"enabled": false,
"environment": 1,
"identity": null,
"feature_segment": null
}
]
}
]
}
Loading