use std::collections::HashSet;
use crate::flag::Flag;
use crate::flag_value::FlagValue;
use crate::store::Store;
use crate::variation::VariationIndex;
use crate::{BucketResult, Context, Target};
use log::warn;
use serde::Serialize;
pub struct PrerequisiteEvent {
pub target_flag_key: String,
pub context: Context,
pub prerequisite_flag: Flag,
pub prerequisite_result: Detail<FlagValue>,
}
pub trait PrerequisiteEventRecorder {
fn record(&self, event: PrerequisiteEvent);
}
const PREALLOCATED_PREREQUISITE_CHAIN_SIZE: usize = 20;
const PREALLOCATED_SEGMENT_CHAIN_SIZE: usize = 20;
pub(crate) struct EvaluationStack {
pub(crate) prerequisite_flag_chain: HashSet<String>,
pub(crate) segment_chain: HashSet<String>,
}
impl EvaluationStack {
fn new() -> Self {
Self {
prerequisite_flag_chain: HashSet::with_capacity(PREALLOCATED_PREREQUISITE_CHAIN_SIZE),
segment_chain: HashSet::with_capacity(PREALLOCATED_SEGMENT_CHAIN_SIZE),
}
}
}
impl Default for EvaluationStack {
fn default() -> Self {
Self::new()
}
}
pub fn evaluate<'a>(
store: &'a dyn Store,
flag: &'a Flag,
context: &'a Context,
prerequisite_event_recorder: Option<&dyn PrerequisiteEventRecorder>,
) -> Detail<&'a FlagValue> {
let mut evaluation_stack = EvaluationStack::default();
evaluate_internal(
store,
flag,
context,
prerequisite_event_recorder,
&mut evaluation_stack,
)
}
fn evaluate_internal<'a>(
store: &'a dyn Store,
flag: &'a Flag,
context: &'a Context,
prerequisite_event_recorder: Option<&dyn PrerequisiteEventRecorder>,
evaluation_stack: &mut EvaluationStack,
) -> Detail<&'a FlagValue> {
if !flag.on {
return flag.off_value(Reason::Off);
}
if evaluation_stack.prerequisite_flag_chain.contains(&flag.key) {
warn!("prerequisite relationship to {} caused a circular reference; this is probably a temporary condition due to an incomplete update", flag.key);
return flag.off_value(Reason::Error {
error: Error::MalformedFlag,
});
}
evaluation_stack
.prerequisite_flag_chain
.insert(flag.key.clone());
for prereq in &flag.prerequisites {
if let Some(prereq_flag) = store.flag(&prereq.key) {
if evaluation_stack
.prerequisite_flag_chain
.contains(&prereq_flag.key)
{
return Detail::err(Error::MalformedFlag);
}
let prerequisite_result = evaluate_internal(
store,
&prereq_flag,
context,
prerequisite_event_recorder,
evaluation_stack,
);
if let Detail {
reason: Reason::Error { .. },
..
} = prerequisite_result
{
return Detail::err(Error::MalformedFlag);
}
let variation_index = prerequisite_result.variation_index;
if let Some(recorder) = prerequisite_event_recorder {
recorder.record(PrerequisiteEvent {
target_flag_key: flag.key.clone(),
context: context.clone(),
prerequisite_flag: prereq_flag.clone(),
prerequisite_result: prerequisite_result.map(|v| v.clone()),
});
}
if !prereq_flag.on || variation_index != Some(prereq.variation) {
return flag.off_value(Reason::PrerequisiteFailed {
prerequisite_key: prereq.key.to_string(),
});
}
} else {
return flag.off_value(Reason::PrerequisiteFailed {
prerequisite_key: prereq.key.to_string(),
});
}
}
evaluation_stack.prerequisite_flag_chain.remove(&flag.key);
if let Some(variation_index) = any_target_match_variation(context, flag) {
return flag.variation(variation_index, Reason::TargetMatch);
}
for (rule_index, rule) in flag.rules.iter().enumerate() {
match rule.matches(context, store, evaluation_stack) {
Err(e) => {
warn!("{:?}", e);
return Detail::err(Error::MalformedFlag);
}
Ok(matches) if matches => {
let result = flag.resolve_variation_or_rollout(&rule.variation_or_rollout, context);
return match result {
Ok(BucketResult {
variation_index,
in_experiment,
}) => {
let reason = Reason::RuleMatch {
rule_index,
rule_id: rule.id.clone(),
in_experiment,
};
flag.variation(variation_index, reason)
}
Err(e) => Detail::err(e),
};
}
_ => (),
}
}
let result = flag.resolve_variation_or_rollout(&flag.fallthrough, context);
return match result {
Ok(BucketResult {
variation_index,
in_experiment,
}) => {
let reason = Reason::Fallthrough { in_experiment };
flag.variation(variation_index, reason)
}
Err(e) => Detail::err(e),
};
}
fn any_target_match_variation(context: &Context, flag: &Flag) -> Option<VariationIndex> {
if flag.context_targets.is_empty() {
for target in &flag.targets {
if let Some(index) = target_match_variation(context, target) {
return Some(index);
}
}
} else {
for context_target in &flag.context_targets {
if context_target.context_kind.is_user() && context_target.values.is_empty() {
for target in &flag.targets {
if target.variation == context_target.variation {
if let Some(index) = target_match_variation(context, target) {
return Some(index);
}
}
}
} else if let Some(index) = target_match_variation(context, context_target) {
return Some(index);
}
}
}
None
}
fn target_match_variation(context: &Context, target: &Target) -> Option<VariationIndex> {
if let Some(context) = context.as_kind(&target.context_kind) {
let key = context.key();
for value in &target.values {
if value == key {
return Some(target.variation);
}
}
}
None
}
#[derive(Clone, Debug, PartialEq)]
pub struct Detail<T> {
pub value: Option<T>,
pub variation_index: Option<VariationIndex>,
pub reason: Reason,
}
impl<T> Detail<T> {
pub fn empty(reason: Reason) -> Detail<T> {
Detail {
value: None,
variation_index: None,
reason,
}
}
pub fn err_default(error: Error, default: T) -> Detail<T> {
Detail {
value: Some(default),
variation_index: None,
reason: Reason::Error { error },
}
}
pub fn err(error: Error) -> Detail<T> {
Detail::empty(Reason::Error { error })
}
pub fn map<U, F>(self, f: F) -> Detail<U>
where
F: FnOnce(T) -> U,
{
Detail {
value: self.value.map(f),
variation_index: self.variation_index,
reason: self.reason,
}
}
pub fn should_have_value(mut self, e: Error) -> Detail<T> {
if self.value.is_none() {
self.reason = Reason::Error { error: e };
}
self
}
pub fn try_map<U, F>(self, f: F, default: U, e: Error) -> Detail<U>
where
F: FnOnce(T) -> Option<U>,
{
if self.value.is_none() {
return Detail {
value: Some(default),
variation_index: self.variation_index,
reason: self.reason,
};
}
match f(self.value.unwrap()) {
Some(v) => Detail {
value: Some(v),
variation_index: self.variation_index,
reason: self.reason,
},
None => Detail::err_default(e, default),
}
}
pub fn or(mut self, default: T) -> Detail<T> {
if self.value.is_none() {
self.value = Some(default);
self.variation_index = None;
}
self
}
pub fn or_else<F>(mut self, default: F) -> Detail<T>
where
F: Fn() -> T,
{
if self.value.is_none() {
self.value = Some(default());
self.variation_index = None;
}
self
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "kind")]
pub enum Reason {
Off,
TargetMatch,
#[serde(rename_all = "camelCase")]
RuleMatch {
rule_index: usize,
#[serde(skip_serializing_if = "String::is_empty")]
rule_id: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
in_experiment: bool,
},
#[serde(rename_all = "camelCase")]
PrerequisiteFailed {
prerequisite_key: String,
},
#[serde(rename_all = "camelCase")]
Fallthrough {
#[serde(skip_serializing_if = "std::ops::Not::not")]
in_experiment: bool,
},
Error {
#[serde(rename = "errorKind")]
error: Error,
},
}
impl Reason {
pub fn is_in_experiment(&self) -> bool {
match self {
Reason::RuleMatch { in_experiment, .. } => *in_experiment,
Reason::Fallthrough { in_experiment } => *in_experiment,
_ => false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Error {
ClientNotReady,
FlagNotFound,
MalformedFlag,
WrongType,
Exception,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contexts::context::Kind;
use crate::flag_value::FlagValue::{Bool, Str};
use crate::test_common::{InMemoryPrerequisiteEventRecorder, TestStore};
use crate::variation::VariationOrRollout;
use crate::{AttributeValue, ContextBuilder, MultiContextBuilder};
use spectral::prelude::*;
use std::cell::RefCell;
use test_case::test_case;
#[test]
fn test_eval_flag_basic() {
let store = TestStore::new();
let alice = ContextBuilder::new("alice").build().unwrap(); let bob = ContextBuilder::new("bob").build().unwrap(); let mut flag = store.flag("flagWithTarget").unwrap();
assert!(!flag.on);
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.variation_index).contains_value(0);
assert_that!(detail.reason).is_equal_to(&Reason::Off);
assert_that!(evaluate(&store, &flag, &bob, None)).is_equal_to(&detail);
flag.off_variation = Some(1);
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.variation_index).contains_value(1);
flag.off_variation = None;
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).is_none();
assert_that!(detail.variation_index).is_none();
assert_that!(detail.reason).is_equal_to(&Reason::Off);
flag.on = true;
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.variation_index).contains_value(1);
assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
in_experiment: false,
});
let detail = evaluate(&store, &flag, &bob, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.variation_index).contains_value(0);
assert_that!(detail.reason).is_equal_to(&Reason::TargetMatch);
flag.fallthrough = VariationOrRollout::Variation { variation: 0 };
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.variation_index).contains_value(0);
let detail = evaluate(&store, &flag, &bob, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.variation_index).contains_value(0);
assert_that!(detail.reason).is_equal_to(&Reason::TargetMatch);
}
#[test]
fn test_eval_with_matches_op_groups() {
let store = TestStore::new();
let alice = ContextBuilder::new("alice").build().unwrap(); let mut bob_builder = ContextBuilder::new("bob");
bob_builder.try_set_value(
"groups",
AttributeValue::Array(vec![AttributeValue::String("my-group".into())]),
);
let bob = bob_builder.build().unwrap(); let flag = store.flag("flagWithMatchesOpOnGroups").unwrap();
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
in_experiment: false,
});
let detail = evaluate(&store, &flag, &bob, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.variation_index).contains_value(0);
assert_that!(detail.reason).is_equal_to(&Reason::RuleMatch {
rule_index: 0,
rule_id: "6a7755ac-e47a-40ea-9579-a09dd5f061bd".into(),
in_experiment: false,
});
}
#[test_case("flagWithMatchesOpOnKinds")]
#[test_case("flagWithMatchesOpOnKindsAttributeReference")]
#[test_case("flagWithMatchesOpOnKindsPlainAttributeReference")]
fn test_eval_with_matches_op_kinds(flag_key: &str) {
let store = TestStore::new();
let alice = ContextBuilder::new("alice").build().unwrap(); let mut bob_builder = ContextBuilder::new("bob");
let bob = bob_builder.kind("company").build().unwrap(); let flag = store.flag(flag_key).unwrap();
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.variation_index).contains_value(0);
assert_that!(detail.reason).is_equal_to(&Reason::RuleMatch {
rule_index: 0,
rule_id: "6a7755ac-e47a-40ea-9579-a09dd5f061bd".into(),
in_experiment: false,
});
let detail = evaluate(&store, &flag, &bob, None);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
in_experiment: false,
});
bob_builder.kind("org");
let new_bob = bob_builder.build().unwrap(); let detail = evaluate(&store, &flag, &new_bob, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.variation_index).contains_value(0);
assert_that!(detail.reason).is_equal_to(&Reason::RuleMatch {
rule_index: 0,
rule_id: "6a7755ac-e47a-40ea-9579-a09dd5f061bd".into(),
in_experiment: false,
});
}
#[test]
fn test_prerequisite_events_are_captured() {
let recorder = InMemoryPrerequisiteEventRecorder {
events: RefCell::new(Vec::new()),
};
let store = TestStore::new();
let alice = ContextBuilder::new("alice").build().unwrap();
let flag = store.flag("flagWithNestedPrereq").unwrap();
let _ = evaluate(&store, &flag, &alice, Some(&recorder));
assert_that!(*recorder.events.borrow()).has_length(2);
let event = &recorder.events.borrow()[0];
assert_eq!("flagWithSatisfiedPrereq", event.target_flag_key);
assert_eq!("prereq", event.prerequisite_flag.key);
let event = &recorder.events.borrow()[1];
assert_eq!("flagWithNestedPrereq", event.target_flag_key);
assert_eq!("flagWithSatisfiedPrereq", event.prerequisite_flag.key);
}
#[test]
fn test_eval_flag_rules() {
let store = TestStore::new();
let alice = ContextBuilder::new("alice").build().unwrap();
let bob = ContextBuilder::new("bob")
.set_value("team", "Avengers".into())
.build()
.unwrap();
let mut flag = store.flag("flagWithInRule").unwrap();
assert!(!flag.on);
for context in &[&alice, &bob] {
let detail = evaluate(&store, &flag, context, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.variation_index).contains_value(0);
assert_that!(detail.reason).is_equal_to(&Reason::Off);
}
flag.on = true;
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.variation_index).contains_value(1);
assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
in_experiment: false,
});
let detail = evaluate(&store, &flag, &bob, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.variation_index).contains_value(0);
assert_that!(detail.reason).is_equal_to(&Reason::RuleMatch {
rule_id: "in-rule".to_string(),
rule_index: 0,
in_experiment: false,
});
}
#[test]
fn test_eval_flag_unsatisfied_prereq() {
let store = TestStore::new();
let flag = store.flag("flagWithMissingPrereq").unwrap();
assert!(flag.on);
let alice = ContextBuilder::new("alice").build().unwrap();
let bob = ContextBuilder::new("bob").build().unwrap();
for user in &[&alice, &bob] {
let detail = evaluate(&store, &flag, user, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.reason).is_equal_to(&Reason::PrerequisiteFailed {
prerequisite_key: "badPrereq".to_string(),
});
}
}
#[test]
fn test_eval_flag_off_prereq() {
let store = TestStore::new();
let flag = store.flag("flagWithOffPrereq").unwrap();
assert!(flag.on);
let alice = ContextBuilder::new("alice").build().unwrap();
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.reason).is_equal_to(&Reason::PrerequisiteFailed {
prerequisite_key: "offPrereq".to_string(),
});
}
#[test]
fn test_eval_flag_satisfied_prereq() {
let mut store = TestStore::new();
let flag = store.flag("flagWithSatisfiedPrereq").unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let bob = ContextBuilder::new("bob").build().unwrap();
let detail = evaluate(&store, &flag, &alice, None);
asserting!("alice should pass prereq and see fallthrough")
.that(&detail.value)
.contains_value(&Bool(true));
let detail = evaluate(&store, &flag, &bob, None);
asserting!("bob should see prereq failed due to target")
.that(&detail.value)
.contains_value(&Bool(false));
assert_that!(detail.reason).is_equal_to(Reason::PrerequisiteFailed {
prerequisite_key: "prereq".to_string(),
});
store.update_flag("prereq", |flag| (flag.on = false));
for user in &[&alice, &bob] {
let detail = evaluate(&store, &flag, user, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.reason).is_equal_to(&Reason::PrerequisiteFailed {
prerequisite_key: "prereq".to_string(),
});
}
}
#[test]
fn test_flag_targets_different_context() {
let store = TestStore::new();
let alice_user_context = ContextBuilder::new("alice").build().unwrap();
let bob_user_context = ContextBuilder::new("bob").build().unwrap();
let org_context = ContextBuilder::new("LaunchDarkly")
.kind("org")
.build()
.unwrap();
let flag = store.flag("flagWithContextTarget").unwrap();
let detail = evaluate(&store, &flag, &org_context, None);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.variation_index).contains_value(1);
assert_that!(detail.reason).is_equal_to(&Reason::TargetMatch);
let detail = evaluate(&store, &flag, &alice_user_context, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.variation_index).contains_value(0);
assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
in_experiment: false,
});
let detail = evaluate(&store, &flag, &bob_user_context, None);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.variation_index).contains_value(1);
assert_that!(detail.reason).is_equal_to(&Reason::TargetMatch);
}
#[test]
fn test_eval_flag_segments() {
let store = TestStore::new();
let flag = store.flag("flagWithSegmentMatchRule").unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let bob = ContextBuilder::new("bob").build().unwrap();
let detail = evaluate(&store, &flag, &alice, None);
asserting!("alice is in segment, should see false with RuleMatch")
.that(&detail.value)
.contains_value(&Bool(false));
assert_that!(detail.reason).is_equal_to(Reason::RuleMatch {
rule_id: "match-rule".to_string(),
rule_index: 0,
in_experiment: false,
});
let detail = evaluate(&store, &flag, &bob, None);
asserting!("bob is not in segment and should see fallthrough")
.that(&detail.value)
.contains_value(&Bool(true));
assert_that!(detail.reason).is_equal_to(Reason::Fallthrough {
in_experiment: false,
});
}
#[test]
fn test_flag_has_prereq_which_duplicates_segment_rule() {
let store = TestStore::new();
let flag = store
.flag("flagWithPrereqWhichDuplicatesSegmentRuleCheck")
.unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let mut evaluation_stack = EvaluationStack::default();
let detail = evaluate_internal(&store, &flag, &alice, None, &mut evaluation_stack);
asserting!("alice is in segment, should see false with RuleMatch")
.that(&detail.value)
.contains_value(&Bool(false));
assert_that!(detail.reason).is_equal_to(Reason::RuleMatch {
rule_id: "match-rule".to_string(),
rule_index: 0,
in_experiment: false,
});
assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
assert!(evaluation_stack.segment_chain.is_empty());
}
#[test]
fn test_simple_prereq_cycle() {
let flag_json = r#"{
"flagA": {
"key": "flagA",
"targets": [],
"rules": [],
"salt": "salty",
"prerequisites": [{
"key": "flagB",
"variation": 0
}],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
},
"flagB": {
"key": "flagB",
"targets": [],
"rules": [],
"salt": "salty",
"prerequisites": [{
"key": "flagA",
"variation": 0
}],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
}
}"#;
let store = TestStore::new_from_json_str(flag_json, "{}");
let flag = store.flag("flagA").unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).is_none();
assert_that!(detail.reason).is_equal_to(Reason::Error {
error: Error::MalformedFlag,
});
}
#[test]
fn test_eval_flag_with_first_prereq_as_prereq_of_second_prereq() {
let store = TestStore::new();
let flag = store
.flag("flagWithFirstPrereqAsPrereqToSecondPrereq")
.unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let bob = ContextBuilder::new("bob").build().unwrap();
let mut evaluation_stack = EvaluationStack::default();
let detail = evaluate_internal(&store, &flag, &alice, None, &mut evaluation_stack);
asserting!("alice should pass prereq and see fallthrough")
.that(&detail.value)
.contains_value(&Bool(true));
assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
assert!(evaluation_stack.segment_chain.is_empty());
let detail = evaluate(&store, &flag, &bob, None);
asserting!("bob should see prereq failed due to target")
.that(&detail.value)
.contains_value(&Bool(false));
assert_that!(detail.reason).is_equal_to(Reason::PrerequisiteFailed {
prerequisite_key: "prereq".to_string(),
});
}
#[test]
fn test_prereq_cycle_across_three_flags() {
let flag_json = r#"{
"flagA": {
"key": "flagA",
"targets": [],
"rules": [],
"salt": "salty",
"prerequisites": [{
"key": "flagB",
"variation": 0
}],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
},
"flagB": {
"key": "flagB",
"targets": [],
"rules": [],
"salt": "salty",
"prerequisites": [{
"key": "flagC",
"variation": 0
}],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
},
"flagC": {
"key": "flagC",
"targets": [],
"rules": [],
"salt": "salty",
"prerequisites": [{
"key": "flagA",
"variation": 0
}],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
}
}"#;
let store = TestStore::new_from_json_str(flag_json, "{}");
let flag = store.flag("flagA").unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).is_none();
assert_that!(detail.reason).is_equal_to(Reason::Error {
error: Error::MalformedFlag,
});
}
#[test]
fn test_flag_and_prereq_share_segment_check() {
let flag_json = r#"{
"flagA": {
"key": "flagA",
"targets": [],
"rules": [{
"variation": 0,
"id": "rule-1",
"clauses": [
{
"contextKind": "user",
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentA"]
}
],
"trackEvents": true
}],
"salt": "salty",
"prerequisites": [{
"key": "flagB",
"variation": 0
}],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
},
"flagB": {
"key": "flagB",
"targets": [],
"rules": [{
"variation": 0,
"id": "rule-2",
"clauses": [
{
"contextKind": "user",
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentA"]
}
],
"trackEvents": true
}],
"salt": "salty",
"prerequisites": [],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
}
}"#;
let segment_json = r#"{
"segmentA": {
"key": "segmentA",
"included": ["alice"],
"includedContexts": [{
"values": [],
"contextKind": "user"
}],
"excluded": [],
"rules": [],
"salt": "salty",
"version": 1
}
}"#;
let store = TestStore::new_from_json_str(flag_json, segment_json);
let flag = store.flag("flagA").unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let mut evaluation_stack = EvaluationStack::default();
let detail = evaluate_internal(&store, &flag, &alice, None, &mut evaluation_stack);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.reason).is_equal_to(Reason::RuleMatch {
rule_index: 0,
rule_id: "rule-1".into(),
in_experiment: false,
});
assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
assert!(evaluation_stack.segment_chain.is_empty());
}
#[test]
fn test_prereqs_can_share_segment_check() {
let flag_json = r#"{
"flagA": {
"key": "flagA",
"targets": [],
"rules": [],
"salt": "salty",
"prerequisites": [
{
"key": "flagB",
"variation": 0
},
{
"key": "flagC",
"variation": 0
}
],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
},
"flagB": {
"key": "flagB",
"targets": [],
"rules": [{
"variation": 0,
"id": "rule-2",
"clauses": [
{
"contextKind": "user",
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentA"]
}
],
"trackEvents": true
}],
"salt": "salty",
"prerequisites": [],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
},
"flagC": {
"key": "flagC",
"targets": [],
"rules": [{
"variation": 0,
"id": "rule-2",
"clauses": [
{
"contextKind": "user",
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentA"]
}
],
"trackEvents": true
}],
"salt": "salty",
"prerequisites": [],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
}
}"#;
let segment_json = r#"{
"segmentA": {
"key": "segmentA",
"included": ["alice"],
"includedContexts": [{
"values": [],
"contextKind": "user"
}],
"excluded": [],
"rules": [],
"salt": "salty",
"version": 1
}
}"#;
let store = TestStore::new_from_json_str(flag_json, segment_json);
let flag = store.flag("flagA").unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let mut evaluation_stack = EvaluationStack::default();
let detail = evaluate_internal(&store, &flag, &alice, None, &mut evaluation_stack);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.reason).is_equal_to(Reason::Fallthrough {
in_experiment: false,
});
assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
assert!(evaluation_stack.segment_chain.is_empty());
}
#[test]
fn test_segment_has_basic_recursive_condition() {
let flag_json = r#"{
"flagA": {
"key": "flagA",
"targets": [],
"rules": [{
"variation": 0,
"id": "rule-1",
"clauses": [
{
"contextKind": "user",
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentA"]
}
],
"trackEvents": true
}],
"salt": "salty",
"prerequisites": [],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
}
}"#;
let segment_json = r#"{
"segmentA": {
"key": "segmentA",
"included": [],
"includedContexts": [],
"excluded": [],
"rules": [{
"id": "rule-1",
"clauses": [{
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentB"],
"contextKind": "user"
}]
}],
"salt": "salty",
"version": 1
},
"segmentB": {
"key": "segmentB",
"included": [],
"includedContexts": [],
"excluded": [],
"rules": [{
"id": "rule-1",
"clauses": [{
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentA"],
"contextKind": "user"
}]
}],
"salt": "salty",
"version": 1
}
}"#;
let store = TestStore::new_from_json_str(flag_json, segment_json);
let flag = store.flag("flagA").unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).is_none();
assert_that!(detail.reason).is_equal_to(Reason::Error {
error: Error::MalformedFlag,
});
}
#[test]
fn test_segment_depends_on_self() {
let flag_json = r#"{
"flagA": {
"key": "flagA",
"targets": [],
"rules": [{
"variation": 0,
"id": "rule-1",
"clauses": [
{
"contextKind": "user",
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentA"]
}
],
"trackEvents": true
}],
"salt": "salty",
"prerequisites": [],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
}
}"#;
let segment_json = r#"{
"segmentA": {
"key": "segmentA",
"included": [],
"includedContexts": [],
"excluded": [],
"rules": [{
"id": "rule-1",
"clauses": [{
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentA"],
"contextKind": "user"
}]
}],
"salt": "salty",
"version": 1
}
}"#;
let store = TestStore::new_from_json_str(flag_json, segment_json);
let flag = store.flag("flagA").unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).is_none();
assert_that!(detail.reason).is_equal_to(Reason::Error {
error: Error::MalformedFlag,
});
}
#[test]
fn test_flag_has_segment_check_and_prereq_also_has_subset_of_segment_checks() {
let flag_json = r#"{
"flagA": {
"key": "flagA",
"targets": [],
"rules": [{
"variation": 0,
"id": "rule-1",
"clauses": [
{
"contextKind": "user",
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentA"]
}
],
"trackEvents": true
}],
"salt": "salty",
"prerequisites": [{
"key": "flagB",
"variation": 0
}],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
},
"flagB": {
"key": "flagB",
"targets": [],
"rules": [{
"variation": 0,
"id": "rule-2",
"clauses": [
{
"contextKind": "user",
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentB"]
}
],
"trackEvents": true
}],
"salt": "salty",
"prerequisites": [],
"on": true,
"fallthrough": {"variation": 0},
"offVariation": 1,
"variations": [true, false]
}
}"#;
let segment_json = r#"{
"segmentA": {
"key": "segmentA",
"included": [],
"includedContexts": [],
"excluded": [],
"rules": [{
"id": "rule-1",
"clauses": [{
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentB"],
"contextKind": "user"
}]
}],
"salt": "salty",
"version": 1
},
"segmentB": {
"key": "segmentB",
"included": [],
"includedContexts": [],
"excluded": [],
"rules": [{
"id": "rule-1",
"clauses": [{
"attribute": "key",
"negate": false,
"op": "segmentMatch",
"values": ["segmentC"],
"contextKind": "user"
}]
}],
"salt": "salty",
"version": 1
},
"segmentC": {
"key": "segmentC",
"included": ["alice"],
"includedContexts": [],
"excluded": ["bob"],
"rules": [],
"salt": "salty",
"version": 1
}
}"#;
let store = TestStore::new_from_json_str(flag_json, segment_json);
let flag = store.flag("flagA").unwrap();
let alice = ContextBuilder::new("alice").build().unwrap();
let bob = ContextBuilder::new("bob").build().unwrap();
let mut evaluation_stack = EvaluationStack::default();
let detail = evaluate_internal(&store, &flag, &alice, None, &mut evaluation_stack);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.reason).is_equal_to(&Reason::RuleMatch {
rule_index: 0,
rule_id: "rule-1".to_string(),
in_experiment: false,
});
assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
assert!(evaluation_stack.segment_chain.is_empty());
let mut evaluation_stack = EvaluationStack::default();
let detail = evaluate_internal(&store, &flag, &bob, None, &mut evaluation_stack);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
in_experiment: false,
});
assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
assert!(evaluation_stack.segment_chain.is_empty());
}
#[test]
fn test_rollout_flag() {
let store = TestStore::new();
let flag = store.flag("flagWithRolloutBucketBy").unwrap();
let alice = ContextBuilder::new("anonymous")
.set_value("platform", "aem".into())
.set_value("ld_quid", "d4ad12cb-392b-4fce-b214-843ad625d6f8".into())
.build()
.unwrap();
let detail = evaluate(&store, &flag, &alice, None);
assert_that!(detail.value).contains_value(&Str("rollout1".to_string()));
}
#[test]
fn test_experiment_flag() {
let store = TestStore::new();
let flag = store.flag("flagWithExperiment").unwrap();
let user_a = ContextBuilder::new("userKeyA").build().unwrap();
let detail = evaluate(&store, &flag, &user_a, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert!(detail.reason.is_in_experiment());
let user_b = ContextBuilder::new("userKeyB").build().unwrap();
let detail = evaluate(&store, &flag, &user_b, None);
assert_that!(detail.value).contains_value(&Bool(true));
assert!(detail.reason.is_in_experiment());
let user_c = ContextBuilder::new("userKeyC").build().unwrap();
let detail = evaluate(&store, &flag, &user_c, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert!(!detail.reason.is_in_experiment());
}
#[test]
fn test_experiment_flag_targeting_missing_context() {
let store = TestStore::new();
let flag = store.flag("flagWithExperimentTargetingContext").unwrap();
let user_a = ContextBuilder::new("userKeyA").build().unwrap();
let detail = evaluate(&store, &flag, &user_a, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.reason).is_equal_to(Reason::Fallthrough {
in_experiment: false,
})
}
#[test]
fn test_malformed_rule() {
let store = TestStore::new();
let mut flag = store.flag("flagWithMalformedRule").unwrap();
let user_a = ContextBuilder::new("no").build().unwrap();
let user_b = ContextBuilder::new("yes").build().unwrap();
let detail = evaluate(&store, &flag, &user_a, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.reason).is_equal_to(Reason::Off);
let detail = evaluate(&store, &flag, &user_b, None);
assert_that!(detail.value).contains_value(&Bool(false));
assert_that!(detail.reason).is_equal_to(Reason::Off);
flag.on = true;
let detail = evaluate(&store, &flag, &user_a, None);
assert_that!(detail.value).contains_value(&Bool(true));
assert_that!(detail.reason).is_equal_to(Reason::Fallthrough {
in_experiment: false,
});
let detail = evaluate(&store, &flag, &user_b, None);
assert_that!(detail.value).is_none();
assert_that!(detail.reason).is_equal_to(Reason::Error {
error: Error::MalformedFlag,
});
}
#[test]
fn reason_serialization() {
struct Case<'a> {
reason: Reason,
json: &'a str,
}
let cases = vec![
Case {
reason: Reason::Off,
json: r#"{"kind":"OFF"}"#,
},
Case {
reason: Reason::Fallthrough {
in_experiment: false,
},
json: r#"{"kind":"FALLTHROUGH"}"#,
},
Case {
reason: Reason::Fallthrough {
in_experiment: true,
},
json: r#"{"kind":"FALLTHROUGH","inExperiment":true}"#,
},
Case {
reason: Reason::TargetMatch {},
json: r#"{"kind":"TARGET_MATCH"}"#,
},
Case {
reason: Reason::RuleMatch {
rule_index: 1,
rule_id: "x".into(),
in_experiment: false,
},
json: r#"{"kind":"RULE_MATCH","ruleIndex":1,"ruleId":"x"}"#,
},
Case {
reason: Reason::RuleMatch {
rule_index: 1,
rule_id: "x".into(),
in_experiment: true,
},
json: r#"{"kind":"RULE_MATCH","ruleIndex":1,"ruleId":"x","inExperiment":true}"#,
},
Case {
reason: Reason::PrerequisiteFailed {
prerequisite_key: "x".into(),
},
json: r#"{"kind":"PREREQUISITE_FAILED","prerequisiteKey":"x"}"#,
},
Case {
reason: Reason::Error {
error: Error::WrongType,
},
json: r#"{"kind":"ERROR","errorKind":"WRONG_TYPE"}"#,
},
];
for Case {
reason,
json: expected_json,
} in cases
{
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(
expected_json, json,
"unexpected serialization: {:?}",
reason
);
}
}
#[test]
fn get_applicable_context_by_kind_returns_correct_context() {
let org_kind = Kind::from("org");
let user_kind = Kind::from("user");
let company_kind = Kind::from("company");
let user_context = ContextBuilder::new("user").build().unwrap();
let org_context = ContextBuilder::new("org").kind("org").build().unwrap();
assert!(user_context.as_kind(&user_kind).is_some());
assert!(user_context.as_kind(&org_kind).is_none());
assert!(org_context.as_kind(&org_kind).is_some());
let multi_context = MultiContextBuilder::new()
.add_context(user_context)
.add_context(org_context)
.build()
.unwrap();
assert_eq!(
&user_kind,
multi_context.as_kind(&user_kind).unwrap().kind()
);
assert_eq!(&org_kind, multi_context.as_kind(&org_kind).unwrap().kind());
assert!(multi_context.as_kind(&company_kind).is_none());
}
#[test]
fn can_create_error_detail() {
let detail = Detail::err_default(Error::MalformedFlag, true.into());
assert_eq!(Some(AttributeValue::Bool(true)), detail.value);
assert!(detail.variation_index.is_none());
assert_that!(detail.reason).is_equal_to(Reason::Error {
error: Error::MalformedFlag,
});
}
#[test]
fn can_force_error_if_value_is_none() {
let detail: Detail<AttributeValue> = Detail {
value: None,
variation_index: None,
reason: Reason::Off,
};
let detail = detail.should_have_value(Error::MalformedFlag);
assert!(detail.value.is_none());
assert!(detail.variation_index.is_none());
assert_that!(detail.reason).is_equal_to(Reason::Error {
error: Error::MalformedFlag,
});
}
#[test]
fn can_map_detail_with_default_and_error() {
let detail: Detail<AttributeValue> = Detail {
value: None,
variation_index: None,
reason: Reason::Off,
};
let mapped = detail.try_map(Some, false.into(), Error::MalformedFlag);
assert_eq!(Some(AttributeValue::Bool(false)), mapped.value);
assert!(mapped.variation_index.is_none());
assert_that!(mapped.reason).is_equal_to(Reason::Off);
let detail: Detail<AttributeValue> = Detail {
value: Some(true.into()),
variation_index: Some(1),
reason: Reason::Off,
};
let mapped = detail.try_map(|_| Some(false.into()), false.into(), Error::MalformedFlag);
assert_eq!(Some(AttributeValue::Bool(false)), mapped.value);
assert_eq!(Some(1), mapped.variation_index);
assert_that!(mapped.reason).is_equal_to(Reason::Off);
let detail: Detail<AttributeValue> = Detail {
value: Some(true.into()),
variation_index: Some(1),
reason: Reason::Off,
};
let mapped = detail.try_map(|_| None, false.into(), Error::MalformedFlag);
assert_eq!(Some(AttributeValue::Bool(false)), mapped.value);
assert!(mapped.variation_index.is_none());
assert_that!(mapped.reason).is_equal_to(Reason::Error {
error: Error::MalformedFlag,
});
}
#[test]
fn can_set_value_to_default_if_does_not_exist() {
let detail: Detail<AttributeValue> = Detail {
value: Some(true.into()),
variation_index: Some(1),
reason: Reason::Off,
};
let or_detail = detail.or(false.into());
assert_eq!(Some(AttributeValue::Bool(true)), or_detail.value);
assert_eq!(Some(1), or_detail.variation_index);
assert_that!(or_detail.reason).is_equal_to(Reason::Off);
let detail: Detail<AttributeValue> = Detail {
value: None,
variation_index: Some(1),
reason: Reason::Off,
};
let or_detail = detail.or(false.into());
assert_eq!(Some(AttributeValue::Bool(false)), or_detail.value);
assert!(or_detail.variation_index.is_none());
assert_that!(or_detail.reason).is_equal_to(Reason::Off);
}
#[test]
fn can_set_value_to_default_if_does_not_exist_through_callback() {
let detail: Detail<AttributeValue> = Detail {
value: Some(true.into()),
variation_index: Some(1),
reason: Reason::Off,
};
let or_detail = detail.or_else(|| false.into());
assert_eq!(Some(AttributeValue::Bool(true)), or_detail.value);
assert_eq!(Some(1), or_detail.variation_index);
assert_that!(or_detail.reason).is_equal_to(Reason::Off);
let detail: Detail<AttributeValue> = Detail {
value: None,
variation_index: Some(1),
reason: Reason::Off,
};
let or_detail = detail.or_else(|| false.into());
assert_eq!(Some(AttributeValue::Bool(false)), or_detail.value);
assert!(or_detail.variation_index.is_none());
assert_that!(or_detail.reason).is_equal_to(Reason::Off);
}
}