use crate::{
contexts::context::Kind,
flag::{ClientVisibility, Target},
flag_value::FlagValue,
rule::{Clause, FlagRule, Op},
variation::VariationOrRollout,
AttributeValue, Flag, Reference,
};
#[derive(Clone)]
pub struct FlagBuilder {
key: String,
on: bool,
variations: Vec<FlagValue>,
fallthrough_variation: usize,
off_variation: usize,
targets: Vec<Target>,
rules: Vec<FlagRule>,
sampling_ratio: Option<u32>,
exclude_from_summaries: bool,
}
impl FlagBuilder {
pub fn key(&self) -> &str {
&self.key
}
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
on: true,
variations: vec![FlagValue::Bool(true), FlagValue::Bool(false)],
fallthrough_variation: 0,
off_variation: 1,
targets: vec![],
rules: vec![],
sampling_ratio: None,
exclude_from_summaries: false,
}
}
pub fn boolean_flag(mut self) -> Self {
self.variations = vec![FlagValue::Bool(true), FlagValue::Bool(false)];
self.fallthrough_variation = 0;
self.off_variation = 1;
self
}
pub fn variations<I>(mut self, variations: I) -> Self
where
I: IntoIterator<Item = FlagValue>,
{
self.variations = variations.into_iter().collect();
self
}
pub fn on(mut self, on: bool) -> Self {
self.on = on;
self
}
pub fn fallthrough_variation(self, value: bool) -> Self {
self.fallthrough_variation_index(if value { 0 } else { 1 })
}
pub fn fallthrough_variation_index(mut self, index: usize) -> Self {
self.fallthrough_variation = index;
self
}
pub fn off_variation(self, value: bool) -> Self {
self.off_variation_index(if value { 0 } else { 1 })
}
pub fn off_variation_index(mut self, index: usize) -> Self {
self.off_variation = index;
self
}
pub fn variation_for_all(mut self, value: bool) -> Self {
self.on = true;
self.targets.clear();
self.rules.clear();
self.fallthrough_variation = if value { 0 } else { 1 };
self
}
pub fn variation_for_all_index(mut self, index: usize) -> Self {
self.on = true;
self.targets.clear();
self.rules.clear();
self.fallthrough_variation = index;
self
}
pub fn value_for_all(mut self, value: FlagValue) -> Self {
self.variations = vec![value];
self.on = true;
self.targets.clear();
self.rules.clear();
self.fallthrough_variation = 0;
self.off_variation = 0;
self
}
pub fn variation_for_user(self, user_key: impl Into<String>, variation: bool) -> Self {
self.variation_index_for_key(Kind::user(), user_key, if variation { 0 } else { 1 })
}
pub fn variation_for_key(
self,
context_kind: Kind,
key: impl Into<String>,
variation: bool,
) -> Self {
self.variation_index_for_key(context_kind, key, if variation { 0 } else { 1 })
}
pub fn variation_index_for_user(self, user_key: impl Into<String>, variation: usize) -> Self {
self.variation_index_for_key(Kind::user(), user_key, variation)
}
pub fn variation_index_for_key(
mut self,
context_kind: Kind,
key: impl Into<String>,
variation: usize,
) -> Self {
let key = key.into();
for target in &mut self.targets {
if target.context_kind == context_kind {
target.values.retain(|k| k != &key);
}
}
self.targets.retain(|t| !t.values.is_empty());
let target = self
.targets
.iter_mut()
.find(|t| t.variation == variation as isize && t.context_kind == context_kind);
if let Some(target) = target {
if !target.values.contains(&key) {
target.values.push(key);
}
} else {
self.targets.push(Target {
context_kind,
values: vec![key],
variation: variation as isize,
});
}
self
}
pub fn clear_targets(mut self) -> Self {
self.targets.clear();
self
}
pub fn if_match<I>(self, attribute: impl Into<String>, values: I) -> RuleBuilder
where
I: IntoIterator<Item = AttributeValue>,
{
self.if_match_context(Kind::user(), attribute, values)
}
pub fn if_match_context<I>(
self,
context_kind: Kind,
attribute: impl Into<String>,
values: I,
) -> RuleBuilder
where
I: IntoIterator<Item = AttributeValue>,
{
RuleBuilder::new(self, context_kind, attribute, values, false)
}
pub fn if_not_match<I>(self, attribute: impl Into<String>, values: I) -> RuleBuilder
where
I: IntoIterator<Item = AttributeValue>,
{
self.if_not_match_context(Kind::user(), attribute, values)
}
pub fn if_not_match_context<I>(
self,
context_kind: Kind,
attribute: impl Into<String>,
values: I,
) -> RuleBuilder
where
I: IntoIterator<Item = AttributeValue>,
{
RuleBuilder::new(self, context_kind, attribute, values, true)
}
pub fn clear_rules(mut self) -> Self {
self.rules.clear();
self
}
pub fn sampling_ratio(mut self, ratio: u32) -> Self {
self.sampling_ratio = Some(ratio);
self
}
pub fn exclude_from_summaries(mut self, exclude: bool) -> Self {
self.exclude_from_summaries = exclude;
self
}
pub fn build(self) -> Flag {
Flag {
key: self.key,
version: 1,
on: self.on,
targets: self.targets,
context_targets: vec![],
rules: self.rules,
prerequisites: vec![],
fallthrough: VariationOrRollout::Variation {
variation: self.fallthrough_variation as isize,
},
off_variation: Some(self.off_variation as isize),
variations: self.variations,
client_visibility: ClientVisibility::default(),
salt: String::new(),
track_events: false,
track_events_fallthrough: false,
debug_events_until_date: None,
migration_settings: None,
sampling_ratio: self.sampling_ratio,
exclude_from_summaries: self.exclude_from_summaries,
}
}
}
pub struct RuleBuilder {
flag_builder: FlagBuilder,
clauses: Vec<Clause>,
rule_id: Option<String>,
}
impl RuleBuilder {
fn new<I>(
flag_builder: FlagBuilder,
context_kind: Kind,
attribute: impl Into<String>,
values: I,
negate: bool,
) -> Self
where
I: IntoIterator<Item = AttributeValue>,
{
Self {
flag_builder,
clauses: vec![Self::make_clause(context_kind, attribute, values, negate)],
rule_id: None,
}
}
fn make_clause<I>(
context_kind: Kind,
attribute: impl Into<String>,
values: I,
negate: bool,
) -> Clause
where
I: IntoIterator<Item = AttributeValue>,
{
Clause {
context_kind,
attribute: Reference::from(attribute.into()),
negate,
op: Op::In,
values: values.into_iter().collect(),
}
}
fn add_clause<I>(
mut self,
context_kind: Kind,
attribute: impl Into<String>,
values: I,
negate: bool,
) -> Self
where
I: IntoIterator<Item = AttributeValue>,
{
self.clauses
.push(Self::make_clause(context_kind, attribute, values, negate));
self
}
pub fn and_match<I>(self, attribute: impl Into<String>, values: I) -> Self
where
I: IntoIterator<Item = AttributeValue>,
{
self.add_clause(Kind::user(), attribute, values, false)
}
pub fn and_match_context<I>(
self,
context_kind: Kind,
attribute: impl Into<String>,
values: I,
) -> Self
where
I: IntoIterator<Item = AttributeValue>,
{
self.add_clause(context_kind, attribute, values, false)
}
pub fn and_not_match<I>(self, attribute: impl Into<String>, values: I) -> Self
where
I: IntoIterator<Item = AttributeValue>,
{
self.add_clause(Kind::user(), attribute, values, true)
}
pub fn and_not_match_context<I>(
self,
context_kind: Kind,
attribute: impl Into<String>,
values: I,
) -> Self
where
I: IntoIterator<Item = AttributeValue>,
{
self.add_clause(context_kind, attribute, values, true)
}
pub fn with_id(mut self, rule_id: impl Into<String>) -> Self {
self.rule_id = Some(rule_id.into());
self
}
pub fn then_return(self, variation: bool) -> FlagBuilder {
self.then_return_index(if variation { 0 } else { 1 })
}
pub fn then_return_index(self, variation: usize) -> FlagBuilder {
let rule_id = self
.rule_id
.unwrap_or_else(|| format!("rule{}", self.flag_builder.rules.len()));
let mut flag_builder = self.flag_builder;
flag_builder.rules.push(FlagRule {
id: rule_id,
clauses: self.clauses,
variation_or_rollout: VariationOrRollout::Variation {
variation: variation as isize,
},
track_events: false,
});
flag_builder
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{eval::evaluate, variation::VariationOrRollout, ContextBuilder, Store};
struct TestStore {
flag: Option<Flag>,
}
impl Store for TestStore {
fn flag(&self, _flag_key: &str) -> Option<Flag> {
self.flag.clone()
}
fn segment(&self, _segment_key: &str) -> Option<crate::Segment> {
None
}
}
#[test]
fn new_flag_has_boolean_defaults() {
let flag = FlagBuilder::new("test-flag").build();
assert_eq!(flag.key, "test-flag");
assert_eq!(flag.on, true);
assert_eq!(flag.off_variation, Some(1));
let store = TestStore {
flag: Some(flag.clone()),
};
let context = ContextBuilder::new("user-123").build().unwrap();
let flag_from_store = store.flag("test-flag").unwrap();
let result = evaluate(&store, &flag_from_store, &context, None);
assert_eq!(result.value, Some(&FlagValue::Bool(true)));
}
#[test]
fn boolean_flag_resets_to_boolean_config() {
let flag = FlagBuilder::new("test-flag")
.variations(vec![
FlagValue::Str("red".to_string()),
FlagValue::Str("blue".to_string()),
])
.boolean_flag()
.build();
let store = TestStore {
flag: Some(flag.clone()),
};
let context = ContextBuilder::new("user-123").build().unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Bool(true)));
assert_eq!(flag.off_variation, Some(1));
}
#[test]
fn variations_sets_custom_variations() {
let flag = FlagBuilder::new("test-flag")
.variations(vec![
FlagValue::Str("red".to_string()),
FlagValue::Str("green".to_string()),
FlagValue::Str("blue".to_string()),
])
.fallthrough_variation_index(0)
.build();
let store = TestStore {
flag: Some(flag.clone()),
};
let context = ContextBuilder::new("user-123").build().unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Str("red".to_string())));
}
#[test]
fn on_method_sets_targeting_state() {
let flag_on = FlagBuilder::new("test-flag").on(true).build();
assert_eq!(flag_on.on, true);
let flag_off = FlagBuilder::new("test-flag").on(false).build();
assert_eq!(flag_off.on, false);
}
#[test]
fn fallthrough_variation_sets_boolean_fallthrough() {
let flag_true = FlagBuilder::new("test-flag")
.fallthrough_variation(true)
.build();
assert_eq!(
flag_true.fallthrough,
VariationOrRollout::Variation { variation: 0 }
);
let flag_false = FlagBuilder::new("test-flag")
.fallthrough_variation(false)
.build();
assert_eq!(
flag_false.fallthrough,
VariationOrRollout::Variation { variation: 1 }
);
}
#[test]
fn fallthrough_variation_index_sets_index() {
let flag = FlagBuilder::new("test-flag")
.fallthrough_variation_index(2)
.build();
assert_eq!(
flag.fallthrough,
VariationOrRollout::Variation { variation: 2 }
);
}
#[test]
fn off_variation_sets_boolean_off() {
let flag_true = FlagBuilder::new("test-flag").off_variation(true).build();
assert_eq!(flag_true.off_variation, Some(0));
let flag_false = FlagBuilder::new("test-flag").off_variation(false).build();
assert_eq!(flag_false.off_variation, Some(1));
}
#[test]
fn off_variation_index_sets_index() {
let flag = FlagBuilder::new("test-flag").off_variation_index(2).build();
assert_eq!(flag.off_variation, Some(2));
}
#[test]
fn variation_for_all_configures_for_everyone() {
let flag = FlagBuilder::new("test-flag")
.variation_for_user("user1", false)
.if_match("country", vec![AttributeValue::String("us".to_string())])
.then_return(false)
.variation_for_all(true)
.build();
assert_eq!(flag.on, true);
assert_eq!(flag.targets.len(), 0);
assert_eq!(flag.rules.len(), 0);
assert_eq!(
flag.fallthrough,
VariationOrRollout::Variation { variation: 0 }
);
}
#[test]
fn variation_for_all_index_configures_with_index() {
let flag = FlagBuilder::new("test-flag")
.variations(vec![
FlagValue::Str("red".to_string()),
FlagValue::Str("green".to_string()),
FlagValue::Str("blue".to_string()),
])
.variation_for_all_index(2)
.build();
assert_eq!(flag.on, true);
assert_eq!(flag.targets.len(), 0);
assert_eq!(flag.rules.len(), 0);
assert_eq!(
flag.fallthrough,
VariationOrRollout::Variation { variation: 2 }
);
}
#[test]
fn value_for_all_sets_single_value() {
let flag = FlagBuilder::new("test-flag")
.value_for_all(FlagValue::Str("constant".to_string()))
.build();
let store = TestStore {
flag: Some(flag.clone()),
};
let context = ContextBuilder::new("user-123").build().unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Str("constant".to_string())));
assert_eq!(flag.on, true);
assert_eq!(flag.off_variation, Some(0));
}
#[test]
fn variation_for_user_targets_user_context() {
let flag = FlagBuilder::new("test-flag")
.variation_for_user("user-123", true)
.build();
let store = TestStore { flag: Some(flag) };
let context = ContextBuilder::new("user-123").build().unwrap();
let flag = store.flag("test-flag").unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Bool(true)));
}
#[test]
fn variation_for_key_targets_any_context_kind() {
let flag = FlagBuilder::new("test-flag")
.variation_for_key(Kind::from("organization"), "org-456", false)
.build();
let store = TestStore { flag: Some(flag) };
let context = ContextBuilder::new("org-456")
.kind("organization")
.build()
.unwrap();
let flag = store.flag("test-flag").unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Bool(false)));
}
#[test]
fn variation_index_for_user_works_with_indices() {
let flag = FlagBuilder::new("test-flag")
.variations(vec![
FlagValue::Str("red".to_string()),
FlagValue::Str("green".to_string()),
FlagValue::Str("blue".to_string()),
])
.variation_index_for_user("user-123", 2)
.build();
let store = TestStore { flag: Some(flag) };
let context = ContextBuilder::new("user-123").build().unwrap();
let flag = store.flag("test-flag").unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Str("blue".to_string())));
}
#[test]
fn variation_index_for_key_works_with_any_kind() {
let flag = FlagBuilder::new("test-flag")
.variations(vec![
FlagValue::Number(0.0),
FlagValue::Number(1.0),
FlagValue::Number(2.0),
])
.variation_index_for_key(Kind::from("device"), "device-789", 1)
.build();
let store = TestStore { flag: Some(flag) };
let context = ContextBuilder::new("device-789")
.kind("device")
.build()
.unwrap();
let flag = store.flag("test-flag").unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Number(1.0)));
}
#[test]
fn context_targeting_takes_precedence_over_rules() {
let flag = FlagBuilder::new("test-flag")
.variation_for_user("user-123", true)
.if_match("key", vec![AttributeValue::String("user-123".to_string())])
.then_return(false)
.build();
let store = TestStore { flag: Some(flag) };
let context = ContextBuilder::new("user-123").build().unwrap();
let flag = store.flag("test-flag").unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Bool(true)));
}
#[test]
fn targeting_key_removes_from_other_variations() {
let flag = FlagBuilder::new("test-flag")
.variation_for_user("user-123", true)
.variation_for_user("user-123", false)
.build();
let false_targets: Vec<_> = flag
.targets
.iter()
.filter(|t| t.variation == 1)
.flat_map(|t| &t.values)
.collect();
assert!(false_targets.contains(&&"user-123".to_string()));
let true_targets: Vec<_> = flag
.targets
.iter()
.filter(|t| t.variation == 0)
.flat_map(|t| &t.values)
.collect();
assert!(!true_targets.contains(&&"user-123".to_string()));
}
#[test]
fn clear_targets_removes_all_targets() {
let flag = FlagBuilder::new("test-flag")
.variation_for_user("user-123", true)
.variation_for_user("user-456", false)
.clear_targets()
.build();
assert_eq!(flag.targets.len(), 0);
}
#[test]
fn if_match_creates_rule_for_user_contexts() {
let flag = FlagBuilder::new("test-flag")
.if_match(
"country",
vec![
AttributeValue::String("us".to_string()),
AttributeValue::String("ca".to_string()),
],
)
.then_return(true)
.build();
let store = TestStore { flag: Some(flag) };
let context = ContextBuilder::new("user-123")
.set_value("country", AttributeValue::String("us".to_string()))
.build()
.unwrap();
let flag = store.flag("test-flag").unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Bool(true)));
}
#[test]
fn if_match_context_creates_rule_for_any_kind() {
let flag = FlagBuilder::new("test-flag")
.if_match_context(
Kind::from("organization"),
"industry",
vec![AttributeValue::String("tech".to_string())],
)
.then_return(true)
.build();
let store = TestStore { flag: Some(flag) };
let context = ContextBuilder::new("org-123")
.kind("organization")
.set_value("industry", AttributeValue::String("tech".to_string()))
.build()
.unwrap();
let flag = store.flag("test-flag").unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Bool(true)));
}
#[test]
fn if_not_match_creates_negated_rule() {
let flag = FlagBuilder::new("test-flag")
.fallthrough_variation(false)
.if_not_match("country", vec![AttributeValue::String("us".to_string())])
.then_return(true)
.build();
let store = TestStore {
flag: Some(flag.clone()),
};
let us_context = ContextBuilder::new("user-123")
.set_value("country", AttributeValue::String("us".to_string()))
.build()
.unwrap();
let us_result = evaluate(&store, &flag, &us_context, None);
assert_eq!(us_result.value, Some(&FlagValue::Bool(false)));
let ca_context = ContextBuilder::new("user-456")
.set_value("country", AttributeValue::String("ca".to_string()))
.build()
.unwrap();
let ca_result = evaluate(&store, &flag, &ca_context, None);
assert_eq!(ca_result.value, Some(&FlagValue::Bool(true)));
}
#[test]
fn if_not_match_context_creates_negated_rule_for_any_kind() {
let flag = FlagBuilder::new("test-flag")
.fallthrough_variation(false)
.if_not_match_context(
Kind::from("organization"),
"tier",
vec![AttributeValue::String("enterprise".to_string())],
)
.then_return(true)
.build();
let store = TestStore {
flag: Some(flag.clone()),
};
let basic_context = ContextBuilder::new("org-123")
.kind("organization")
.set_value("tier", AttributeValue::String("basic".to_string()))
.build()
.unwrap();
let basic_result = evaluate(&store, &flag, &basic_context, None);
assert_eq!(basic_result.value, Some(&FlagValue::Bool(true)));
}
#[test]
fn and_match_adds_multiple_clauses() {
let flag = FlagBuilder::new("test-flag")
.fallthrough_variation(false)
.if_match("country", vec![AttributeValue::String("us".to_string())])
.and_match("state", vec![AttributeValue::String("ca".to_string())])
.then_return(true)
.build();
let store = TestStore {
flag: Some(flag.clone()),
};
let matching_context = ContextBuilder::new("user-123")
.set_value("country", AttributeValue::String("us".to_string()))
.set_value("state", AttributeValue::String("ca".to_string()))
.build()
.unwrap();
let matching_result = evaluate(&store, &flag, &matching_context, None);
assert_eq!(matching_result.value, Some(&FlagValue::Bool(true)));
let partial_context = ContextBuilder::new("user-456")
.set_value("country", AttributeValue::String("us".to_string()))
.set_value("state", AttributeValue::String("ny".to_string()))
.build()
.unwrap();
let partial_result = evaluate(&store, &flag, &partial_context, None);
assert_eq!(partial_result.value, Some(&FlagValue::Bool(false)));
}
#[test]
fn and_not_match_adds_negated_clauses() {
let flag = FlagBuilder::new("test-flag")
.fallthrough_variation(false)
.if_match("country", vec![AttributeValue::String("us".to_string())])
.and_not_match("state", vec![AttributeValue::String("ca".to_string())])
.then_return(true)
.build();
let store = TestStore {
flag: Some(flag.clone()),
};
let matching_context = ContextBuilder::new("user-123")
.set_value("country", AttributeValue::String("us".to_string()))
.set_value("state", AttributeValue::String("ny".to_string()))
.build()
.unwrap();
let matching_result = evaluate(&store, &flag, &matching_context, None);
assert_eq!(matching_result.value, Some(&FlagValue::Bool(true)));
let ca_context = ContextBuilder::new("user-456")
.set_value("country", AttributeValue::String("us".to_string()))
.set_value("state", AttributeValue::String("ca".to_string()))
.build()
.unwrap();
let ca_result = evaluate(&store, &flag, &ca_context, None);
assert_eq!(ca_result.value, Some(&FlagValue::Bool(false))); }
#[test]
fn then_return_completes_rule() {
let flag = FlagBuilder::new("test-flag")
.if_match("beta", vec![AttributeValue::Bool(true)])
.then_return(true)
.build();
assert_eq!(flag.rules.len(), 1);
assert_eq!(
flag.rules[0].variation_or_rollout,
VariationOrRollout::Variation { variation: 0 }
);
}
#[test]
fn then_return_index_completes_rule_with_index() {
let flag = FlagBuilder::new("test-flag")
.variations(vec![
FlagValue::Str("red".to_string()),
FlagValue::Str("green".to_string()),
FlagValue::Str("blue".to_string()),
])
.if_match("color", vec![AttributeValue::String("primary".to_string())])
.then_return_index(2)
.build();
assert_eq!(flag.rules.len(), 1);
assert_eq!(
flag.rules[0].variation_or_rollout,
VariationOrRollout::Variation { variation: 2 }
);
}
#[test]
fn rules_evaluated_in_order() {
let flag = FlagBuilder::new("test-flag")
.if_match("key", vec![AttributeValue::String("user-123".to_string())])
.then_return(true)
.if_match("key", vec![AttributeValue::String("user-123".to_string())])
.then_return(false)
.build();
let store = TestStore { flag: Some(flag) };
let context = ContextBuilder::new("user-123").build().unwrap();
let flag = store.flag("test-flag").unwrap();
let result = evaluate(&store, &flag, &context, None);
assert_eq!(result.value, Some(&FlagValue::Bool(true)));
}
#[test]
fn rules_evaluated_after_targets_before_fallthrough() {
let flag = FlagBuilder::new("test-flag")
.fallthrough_variation(false)
.variation_for_user("user-targeted", true)
.if_match("beta", vec![AttributeValue::Bool(true)])
.then_return(true)
.build();
let store = TestStore {
flag: Some(flag.clone()),
};
let targeted_context = ContextBuilder::new("user-targeted")
.set_value("beta", AttributeValue::Bool(true))
.build()
.unwrap();
let targeted_result = evaluate(&store, &flag, &targeted_context, None);
assert_eq!(targeted_result.value, Some(&FlagValue::Bool(true)));
let rule_context = ContextBuilder::new("user-beta")
.set_value("beta", AttributeValue::Bool(true))
.build()
.unwrap();
let rule_result = evaluate(&store, &flag, &rule_context, None);
assert_eq!(rule_result.value, Some(&FlagValue::Bool(true)));
let fallthrough_context = ContextBuilder::new("user-other")
.set_value("beta", AttributeValue::Bool(false))
.build()
.unwrap();
let fallthrough_result = evaluate(&store, &flag, &fallthrough_context, None);
assert_eq!(fallthrough_result.value, Some(&FlagValue::Bool(false)));
}
#[test]
fn clear_rules_removes_all_rules() {
let flag = FlagBuilder::new("test-flag")
.if_match("country", vec![AttributeValue::String("us".to_string())])
.then_return(true)
.if_match("state", vec![AttributeValue::String("ca".to_string())])
.then_return(false)
.clear_rules()
.build();
assert_eq!(flag.rules.len(), 0);
}
#[test]
fn only_in_operator_used_in_rules() {
let flag = FlagBuilder::new("test-flag")
.fallthrough_variation(false)
.if_match("country", vec![AttributeValue::String("us".to_string())])
.then_return(true)
.build();
assert_eq!(flag.rules.len(), 1);
let store = TestStore {
flag: Some(flag.clone()),
};
let us_context = ContextBuilder::new("user-123")
.set_value("country", AttributeValue::String("us".to_string()))
.build()
.unwrap();
let us_result = evaluate(&store, &flag, &us_context, None);
assert_eq!(us_result.value, Some(&FlagValue::Bool(true)));
let ca_context = ContextBuilder::new("user-456")
.set_value("country", AttributeValue::String("ca".to_string()))
.build()
.unwrap();
let ca_result = evaluate(&store, &flag, &ca_context, None);
assert_eq!(ca_result.value, Some(&FlagValue::Bool(false)));
}
#[test]
fn sampling_ratio_sets_ratio() {
let flag = FlagBuilder::new("test-flag").sampling_ratio(10000).build();
assert_eq!(flag.sampling_ratio, Some(10000));
}
#[test]
fn sampling_ratio_defaults_to_none() {
let flag = FlagBuilder::new("test-flag").build();
assert_eq!(flag.sampling_ratio, None);
}
#[test]
fn exclude_from_summaries_sets_exclusion() {
let flag = FlagBuilder::new("test-flag")
.exclude_from_summaries(true)
.build();
assert!(flag.exclude_from_summaries);
}
#[test]
fn exclude_from_summaries_defaults_to_false() {
let flag = FlagBuilder::new("test-flag").build();
assert!(!flag.exclude_from_summaries);
}
}