Skip to main content

launchdarkly_server_sdk_evaluation/
test_data.rs

1//! Test data source builders for creating flags and segments for testing purposes.
2//!
3//! This module provides builder APIs for constructing feature flags with various configurations
4//! without needing to connect to LaunchDarkly services. These builders are intended for use in
5//! test scenarios and local development.
6
7use crate::{
8    contexts::context::Kind,
9    flag::{ClientVisibility, Target},
10    flag_value::FlagValue,
11    rule::{Clause, FlagRule, Op},
12    variation::VariationOrRollout,
13    AttributeValue, Flag, Reference,
14};
15
16/// Builder for constructing test flags with various configurations.
17///
18/// The flag builder provides a fluent API for creating flags with targeting rules,
19/// variations, and other configuration options. This is the primary way to create
20/// test flags for use in testing and development scenarios.
21#[derive(Clone)]
22pub struct FlagBuilder {
23    key: String,
24    on: bool,
25    variations: Vec<FlagValue>,
26    fallthrough_variation: usize,
27    off_variation: usize,
28    targets: Vec<Target>,
29    rules: Vec<FlagRule>,
30    sampling_ratio: Option<u32>,
31    exclude_from_summaries: bool,
32}
33
34impl FlagBuilder {
35    /// Returns the flag key for this builder.
36    pub fn key(&self) -> &str {
37        &self.key
38    }
39
40    /// Creates a new flag builder for the given flag key.
41    ///
42    /// If creating a new flag, it will be initialized as a boolean flag with:
43    /// - Variations: [true, false]
44    /// - Targeting enabled (on: true)
45    /// - Fallthrough variation: 0 (true)
46    /// - Off variation: 1 (false)
47    pub fn new(key: impl Into<String>) -> Self {
48        Self {
49            key: key.into(),
50            on: true,
51            variations: vec![FlagValue::Bool(true), FlagValue::Bool(false)],
52            fallthrough_variation: 0,
53            off_variation: 1,
54            targets: vec![],
55            rules: vec![],
56            sampling_ratio: None,
57            exclude_from_summaries: false,
58        }
59    }
60
61    /// Configures the flag as a boolean type with variations [true, false].
62    ///
63    /// Sets:
64    /// - Variations: [true, false]
65    /// - Fallthrough variation: 0 (true)
66    /// - Off variation: 1 (false)
67    pub fn boolean_flag(mut self) -> Self {
68        self.variations = vec![FlagValue::Bool(true), FlagValue::Bool(false)];
69        self.fallthrough_variation = 0;
70        self.off_variation = 1;
71        self
72    }
73
74    /// Sets the variations for this flag.
75    pub fn variations<I>(mut self, variations: I) -> Self
76    where
77        I: IntoIterator<Item = FlagValue>,
78    {
79        self.variations = variations.into_iter().collect();
80        self
81    }
82
83    /// Sets whether targeting is enabled for the flag.
84    ///
85    /// When targeting is off (false), the flag returns the off variation regardless
86    /// of other configuration.
87    pub fn on(mut self, on: bool) -> Self {
88        self.on = on;
89        self
90    }
91
92    /// Sets the fallthrough variation for boolean flags.
93    ///
94    /// This is a convenience method equivalent to calling `fallthrough_variation_index`
95    /// with 0 for true or 1 for false.
96    pub fn fallthrough_variation(self, value: bool) -> Self {
97        self.fallthrough_variation_index(if value { 0 } else { 1 })
98    }
99
100    /// Sets the fallthrough variation by index.
101    ///
102    /// The fallthrough variation is returned when targeting is on but no targets or rules match.
103    pub fn fallthrough_variation_index(mut self, index: usize) -> Self {
104        self.fallthrough_variation = index;
105        self
106    }
107
108    /// Sets the off variation for boolean flags.
109    ///
110    /// This is a convenience method equivalent to calling `off_variation_index`
111    /// with 0 for true or 1 for false.
112    pub fn off_variation(self, value: bool) -> Self {
113        self.off_variation_index(if value { 0 } else { 1 })
114    }
115
116    /// Sets the off variation by index.
117    ///
118    /// The off variation is returned when targeting is disabled (on: false).
119    pub fn off_variation_index(mut self, index: usize) -> Self {
120        self.off_variation = index;
121        self
122    }
123
124    /// Configures the flag to always return the specified boolean value for everyone.
125    ///
126    /// This is a convenience method that:
127    /// - Enables targeting (on: true)
128    /// - Removes all targets and rules
129    /// - Sets the fallthrough variation to the specified value
130    pub fn variation_for_all(mut self, value: bool) -> Self {
131        self.on = true;
132        self.targets.clear();
133        self.rules.clear();
134        self.fallthrough_variation = if value { 0 } else { 1 };
135        self
136    }
137
138    /// Configures the flag to always return the specified variation index for everyone.
139    ///
140    /// This is a convenience method that:
141    /// - Enables targeting (on: true)
142    /// - Removes all targets and rules
143    /// - Sets the fallthrough variation to the specified index
144    pub fn variation_for_all_index(mut self, index: usize) -> Self {
145        self.on = true;
146        self.targets.clear();
147        self.rules.clear();
148        self.fallthrough_variation = index;
149        self
150    }
151
152    /// Configures the flag to always return the specified value.
153    ///
154    /// This is a convenience method that:
155    /// - Sets a single variation equal to the specified value
156    /// - Enables targeting (on: true)
157    /// - Removes all targets and rules
158    /// - Sets both fallthrough and off variation to index 0
159    pub fn value_for_all(mut self, value: FlagValue) -> Self {
160        self.variations = vec![value];
161        self.on = true;
162        self.targets.clear();
163        self.rules.clear();
164        self.fallthrough_variation = 0;
165        self.off_variation = 0;
166        self
167    }
168
169    /// Configures the flag to return a specific boolean value for a user context.
170    ///
171    /// This is a convenience method for targeting contexts with kind: "user".
172    pub fn variation_for_user(self, user_key: impl Into<String>, variation: bool) -> Self {
173        self.variation_index_for_key(Kind::user(), user_key, if variation { 0 } else { 1 })
174    }
175
176    /// Configures the flag to return a specific boolean value for a context of any kind.
177    pub fn variation_for_key(
178        self,
179        context_kind: Kind,
180        key: impl Into<String>,
181        variation: bool,
182    ) -> Self {
183        self.variation_index_for_key(context_kind, key, if variation { 0 } else { 1 })
184    }
185
186    /// Configures the flag to return a specific variation index for a user context.
187    ///
188    /// This is a convenience method for targeting contexts with kind: "user".
189    pub fn variation_index_for_user(self, user_key: impl Into<String>, variation: usize) -> Self {
190        self.variation_index_for_key(Kind::user(), user_key, variation)
191    }
192
193    /// Configures the flag to return a specific variation index for a context of any kind.
194    ///
195    /// When a context key is targeted, that key is automatically removed from targeting
196    /// for any other variation of the same flag (a key can only be targeted for one
197    /// variation at a time).
198    pub fn variation_index_for_key(
199        mut self,
200        context_kind: Kind,
201        key: impl Into<String>,
202        variation: usize,
203    ) -> Self {
204        let key = key.into();
205
206        // Remove the key from all existing targets for this context kind,
207        // then prune any targets left with empty values lists.
208        for target in &mut self.targets {
209            if target.context_kind == context_kind {
210                target.values.retain(|k| k != &key);
211            }
212        }
213        self.targets.retain(|t| !t.values.is_empty());
214
215        // Find or create target for this variation and context kind
216        let target = self
217            .targets
218            .iter_mut()
219            .find(|t| t.variation == variation as isize && t.context_kind == context_kind);
220
221        if let Some(target) = target {
222            if !target.values.contains(&key) {
223                target.values.push(key);
224            }
225        } else {
226            self.targets.push(Target {
227                context_kind,
228                values: vec![key],
229                variation: variation as isize,
230            });
231        }
232
233        self
234    }
235
236    /// Removes all individual context targets from the flag.
237    pub fn clear_targets(mut self) -> Self {
238        self.targets.clear();
239        self
240    }
241
242    /// Creates a rule that matches when the specified user attribute equals any of the provided values.
243    ///
244    /// This is a convenience method for creating rules that target contexts with kind: "user".
245    /// Returns a RuleBuilder that can be used to add more conditions or complete the rule.
246    pub fn if_match<I>(self, attribute: impl Into<String>, values: I) -> RuleBuilder
247    where
248        I: IntoIterator<Item = AttributeValue>,
249    {
250        self.if_match_context(Kind::user(), attribute, values)
251    }
252
253    /// Creates a rule that matches when the specified attribute equals any of the provided values
254    /// for a context of the specified kind.
255    ///
256    /// Returns a RuleBuilder that can be used to add more conditions or complete the rule.
257    pub fn if_match_context<I>(
258        self,
259        context_kind: Kind,
260        attribute: impl Into<String>,
261        values: I,
262    ) -> RuleBuilder
263    where
264        I: IntoIterator<Item = AttributeValue>,
265    {
266        RuleBuilder::new(self, context_kind, attribute, values, false)
267    }
268
269    /// Creates a rule that matches when the specified user attribute does NOT equal any of the provided values.
270    ///
271    /// This is identical to `if_match` except it uses negated logic.
272    pub fn if_not_match<I>(self, attribute: impl Into<String>, values: I) -> RuleBuilder
273    where
274        I: IntoIterator<Item = AttributeValue>,
275    {
276        self.if_not_match_context(Kind::user(), attribute, values)
277    }
278
279    /// Creates a rule that matches when the specified attribute does NOT equal any of the provided values
280    /// for a context of the specified kind.
281    pub fn if_not_match_context<I>(
282        self,
283        context_kind: Kind,
284        attribute: impl Into<String>,
285        values: I,
286    ) -> RuleBuilder
287    where
288        I: IntoIterator<Item = AttributeValue>,
289    {
290        RuleBuilder::new(self, context_kind, attribute, values, true)
291    }
292
293    /// Removes all rules from the flag.
294    pub fn clear_rules(mut self) -> Self {
295        self.rules.clear();
296        self
297    }
298
299    /// Sets the event sampling ratio for the flag.
300    pub fn sampling_ratio(mut self, ratio: u32) -> Self {
301        self.sampling_ratio = Some(ratio);
302        self
303    }
304
305    /// Sets whether the flag should be excluded from summary event counts.
306    pub fn exclude_from_summaries(mut self, exclude: bool) -> Self {
307        self.exclude_from_summaries = exclude;
308        self
309    }
310
311    /// Builds the final Flag instance.
312    ///
313    /// This method creates a complete Flag with all configured settings.
314    pub fn build(self) -> Flag {
315        Flag {
316            key: self.key,
317            version: 1,
318            on: self.on,
319            targets: self.targets,
320            context_targets: vec![],
321            rules: self.rules,
322            prerequisites: vec![],
323            fallthrough: VariationOrRollout::Variation {
324                variation: self.fallthrough_variation as isize,
325            },
326            off_variation: Some(self.off_variation as isize),
327            variations: self.variations,
328            client_visibility: ClientVisibility::default(),
329            salt: String::new(),
330            track_events: false,
331            track_events_fallthrough: false,
332            debug_events_until_date: None,
333            migration_settings: None,
334            sampling_ratio: self.sampling_ratio,
335            exclude_from_summaries: self.exclude_from_summaries,
336        }
337    }
338}
339
340/// Builder for constructing flag rules with multiple clauses.
341///
342/// Rules are evaluated in the order they were added to the flag. The first matching rule wins.
343/// Rules are evaluated after individual context targets but before the fallthrough variation.
344pub struct RuleBuilder {
345    flag_builder: FlagBuilder,
346    clauses: Vec<Clause>,
347    rule_id: Option<String>,
348}
349
350impl RuleBuilder {
351    fn new<I>(
352        flag_builder: FlagBuilder,
353        context_kind: Kind,
354        attribute: impl Into<String>,
355        values: I,
356        negate: bool,
357    ) -> Self
358    where
359        I: IntoIterator<Item = AttributeValue>,
360    {
361        Self {
362            flag_builder,
363            clauses: vec![Self::make_clause(context_kind, attribute, values, negate)],
364            rule_id: None,
365        }
366    }
367
368    fn make_clause<I>(
369        context_kind: Kind,
370        attribute: impl Into<String>,
371        values: I,
372        negate: bool,
373    ) -> Clause
374    where
375        I: IntoIterator<Item = AttributeValue>,
376    {
377        Clause {
378            context_kind,
379            attribute: Reference::from(attribute.into()),
380            negate,
381            op: Op::In,
382            values: values.into_iter().collect(),
383        }
384    }
385
386    fn add_clause<I>(
387        mut self,
388        context_kind: Kind,
389        attribute: impl Into<String>,
390        values: I,
391        negate: bool,
392    ) -> Self
393    where
394        I: IntoIterator<Item = AttributeValue>,
395    {
396        self.clauses
397            .push(Self::make_clause(context_kind, attribute, values, negate));
398        self
399    }
400
401    /// Adds another clause to the current rule for user contexts.
402    ///
403    /// Multiple clauses in a rule have AND semantics - all must match for the rule to match.
404    pub fn and_match<I>(self, attribute: impl Into<String>, values: I) -> Self
405    where
406        I: IntoIterator<Item = AttributeValue>,
407    {
408        self.add_clause(Kind::user(), attribute, values, false)
409    }
410
411    /// Adds another clause to the current rule for a context of the specified kind.
412    ///
413    /// Multiple clauses in a rule have AND semantics - all must match for the rule to match.
414    pub fn and_match_context<I>(
415        self,
416        context_kind: Kind,
417        attribute: impl Into<String>,
418        values: I,
419    ) -> Self
420    where
421        I: IntoIterator<Item = AttributeValue>,
422    {
423        self.add_clause(context_kind, attribute, values, false)
424    }
425
426    /// Adds a negated clause to the current rule for user contexts.
427    ///
428    /// The clause must NOT match any of the values for the rule to match.
429    pub fn and_not_match<I>(self, attribute: impl Into<String>, values: I) -> Self
430    where
431        I: IntoIterator<Item = AttributeValue>,
432    {
433        self.add_clause(Kind::user(), attribute, values, true)
434    }
435
436    /// Adds a negated clause to the current rule for a context of the specified kind.
437    ///
438    /// The clause must NOT match any of the values for the rule to match.
439    pub fn and_not_match_context<I>(
440        self,
441        context_kind: Kind,
442        attribute: impl Into<String>,
443        values: I,
444    ) -> Self
445    where
446        I: IntoIterator<Item = AttributeValue>,
447    {
448        self.add_clause(context_kind, attribute, values, true)
449    }
450
451    /// Sets a custom rule ID for this rule.
452    ///
453    /// By default, rules are assigned auto-generated IDs like "rule0", "rule1", etc.
454    /// Use this method to override with a custom ID.
455    pub fn with_id(mut self, rule_id: impl Into<String>) -> Self {
456        self.rule_id = Some(rule_id.into());
457        self
458    }
459
460    /// Completes the rule configuration for a boolean flag.
461    ///
462    /// This method adds the completed rule to the flag and returns control to the flag builder.
463    pub fn then_return(self, variation: bool) -> FlagBuilder {
464        self.then_return_index(if variation { 0 } else { 1 })
465    }
466
467    /// Completes the rule configuration with a variation index.
468    ///
469    /// This method adds the completed rule to the flag and returns control to the flag builder.
470    pub fn then_return_index(self, variation: usize) -> FlagBuilder {
471        let rule_id = self
472            .rule_id
473            .unwrap_or_else(|| format!("rule{}", self.flag_builder.rules.len()));
474
475        let mut flag_builder = self.flag_builder;
476        flag_builder.rules.push(FlagRule {
477            id: rule_id,
478            clauses: self.clauses,
479            variation_or_rollout: VariationOrRollout::Variation {
480                variation: variation as isize,
481            },
482            track_events: false,
483        });
484        flag_builder
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use crate::{eval::evaluate, variation::VariationOrRollout, ContextBuilder, Store};
492
493    // Simple in-memory store for testing
494    struct TestStore {
495        flag: Option<Flag>,
496    }
497
498    impl Store for TestStore {
499        fn flag(&self, _flag_key: &str) -> Option<Flag> {
500            self.flag.clone()
501        }
502
503        fn segment(&self, _segment_key: &str) -> Option<crate::Segment> {
504            None
505        }
506    }
507
508    #[test]
509    fn new_flag_has_boolean_defaults() {
510        let flag = FlagBuilder::new("test-flag").build();
511
512        assert_eq!(flag.key, "test-flag");
513        assert_eq!(flag.on, true);
514        assert_eq!(flag.off_variation, Some(1));
515
516        // Test by evaluating - should return true (fallthrough variation 0)
517        let store = TestStore {
518            flag: Some(flag.clone()),
519        };
520        let context = ContextBuilder::new("user-123").build().unwrap();
521        let flag_from_store = store.flag("test-flag").unwrap();
522        let result = evaluate(&store, &flag_from_store, &context, None);
523        assert_eq!(result.value, Some(&FlagValue::Bool(true)));
524    }
525
526    #[test]
527    fn boolean_flag_resets_to_boolean_config() {
528        let flag = FlagBuilder::new("test-flag")
529            .variations(vec![
530                FlagValue::Str("red".to_string()),
531                FlagValue::Str("blue".to_string()),
532            ])
533            .boolean_flag()
534            .build();
535
536        // Use evaluation to verify the boolean configuration
537        let store = TestStore {
538            flag: Some(flag.clone()),
539        };
540        let context = ContextBuilder::new("user-123").build().unwrap();
541        let result = evaluate(&store, &flag, &context, None);
542
543        // Should evaluate to boolean true (fallthrough variation 0)
544        assert_eq!(result.value, Some(&FlagValue::Bool(true)));
545        assert_eq!(flag.off_variation, Some(1));
546    }
547
548    #[test]
549    fn variations_sets_custom_variations() {
550        let flag = FlagBuilder::new("test-flag")
551            .variations(vec![
552                FlagValue::Str("red".to_string()),
553                FlagValue::Str("green".to_string()),
554                FlagValue::Str("blue".to_string()),
555            ])
556            .fallthrough_variation_index(0)
557            .build();
558
559        // Test by evaluating
560        let store = TestStore {
561            flag: Some(flag.clone()),
562        };
563        let context = ContextBuilder::new("user-123").build().unwrap();
564        let result = evaluate(&store, &flag, &context, None);
565
566        assert_eq!(result.value, Some(&FlagValue::Str("red".to_string())));
567    }
568
569    #[test]
570    fn on_method_sets_targeting_state() {
571        let flag_on = FlagBuilder::new("test-flag").on(true).build();
572        assert_eq!(flag_on.on, true);
573
574        let flag_off = FlagBuilder::new("test-flag").on(false).build();
575        assert_eq!(flag_off.on, false);
576    }
577
578    #[test]
579    fn fallthrough_variation_sets_boolean_fallthrough() {
580        let flag_true = FlagBuilder::new("test-flag")
581            .fallthrough_variation(true)
582            .build();
583        assert_eq!(
584            flag_true.fallthrough,
585            VariationOrRollout::Variation { variation: 0 }
586        );
587
588        let flag_false = FlagBuilder::new("test-flag")
589            .fallthrough_variation(false)
590            .build();
591        assert_eq!(
592            flag_false.fallthrough,
593            VariationOrRollout::Variation { variation: 1 }
594        );
595    }
596
597    #[test]
598    fn fallthrough_variation_index_sets_index() {
599        let flag = FlagBuilder::new("test-flag")
600            .fallthrough_variation_index(2)
601            .build();
602        assert_eq!(
603            flag.fallthrough,
604            VariationOrRollout::Variation { variation: 2 }
605        );
606    }
607
608    #[test]
609    fn off_variation_sets_boolean_off() {
610        let flag_true = FlagBuilder::new("test-flag").off_variation(true).build();
611        assert_eq!(flag_true.off_variation, Some(0));
612
613        let flag_false = FlagBuilder::new("test-flag").off_variation(false).build();
614        assert_eq!(flag_false.off_variation, Some(1));
615    }
616
617    #[test]
618    fn off_variation_index_sets_index() {
619        let flag = FlagBuilder::new("test-flag").off_variation_index(2).build();
620        assert_eq!(flag.off_variation, Some(2));
621    }
622
623    #[test]
624    fn variation_for_all_configures_for_everyone() {
625        let flag = FlagBuilder::new("test-flag")
626            .variation_for_user("user1", false)
627            .if_match("country", vec![AttributeValue::String("us".to_string())])
628            .then_return(false)
629            .variation_for_all(true)
630            .build();
631
632        assert_eq!(flag.on, true);
633        assert_eq!(flag.targets.len(), 0);
634        assert_eq!(flag.rules.len(), 0);
635        assert_eq!(
636            flag.fallthrough,
637            VariationOrRollout::Variation { variation: 0 }
638        );
639    }
640
641    #[test]
642    fn variation_for_all_index_configures_with_index() {
643        let flag = FlagBuilder::new("test-flag")
644            .variations(vec![
645                FlagValue::Str("red".to_string()),
646                FlagValue::Str("green".to_string()),
647                FlagValue::Str("blue".to_string()),
648            ])
649            .variation_for_all_index(2)
650            .build();
651
652        assert_eq!(flag.on, true);
653        assert_eq!(flag.targets.len(), 0);
654        assert_eq!(flag.rules.len(), 0);
655        assert_eq!(
656            flag.fallthrough,
657            VariationOrRollout::Variation { variation: 2 }
658        );
659    }
660
661    #[test]
662    fn value_for_all_sets_single_value() {
663        let flag = FlagBuilder::new("test-flag")
664            .value_for_all(FlagValue::Str("constant".to_string()))
665            .build();
666
667        // Test by evaluating with targeting on and off
668        let store = TestStore {
669            flag: Some(flag.clone()),
670        };
671        let context = ContextBuilder::new("user-123").build().unwrap();
672        let result = evaluate(&store, &flag, &context, None);
673
674        assert_eq!(result.value, Some(&FlagValue::Str("constant".to_string())));
675        assert_eq!(flag.on, true);
676        assert_eq!(flag.off_variation, Some(0));
677    }
678
679    #[test]
680    fn variation_for_user_targets_user_context() {
681        let flag = FlagBuilder::new("test-flag")
682            .variation_for_user("user-123", true)
683            .build();
684
685        let store = TestStore { flag: Some(flag) };
686        let context = ContextBuilder::new("user-123").build().unwrap();
687        let flag = store.flag("test-flag").unwrap();
688        let result = evaluate(&store, &flag, &context, None);
689
690        assert_eq!(result.value, Some(&FlagValue::Bool(true)));
691    }
692
693    #[test]
694    fn variation_for_key_targets_any_context_kind() {
695        let flag = FlagBuilder::new("test-flag")
696            .variation_for_key(Kind::from("organization"), "org-456", false)
697            .build();
698
699        let store = TestStore { flag: Some(flag) };
700        let context = ContextBuilder::new("org-456")
701            .kind("organization")
702            .build()
703            .unwrap();
704        let flag = store.flag("test-flag").unwrap();
705        let result = evaluate(&store, &flag, &context, None);
706
707        assert_eq!(result.value, Some(&FlagValue::Bool(false)));
708    }
709
710    #[test]
711    fn variation_index_for_user_works_with_indices() {
712        let flag = FlagBuilder::new("test-flag")
713            .variations(vec![
714                FlagValue::Str("red".to_string()),
715                FlagValue::Str("green".to_string()),
716                FlagValue::Str("blue".to_string()),
717            ])
718            .variation_index_for_user("user-123", 2)
719            .build();
720
721        let store = TestStore { flag: Some(flag) };
722        let context = ContextBuilder::new("user-123").build().unwrap();
723        let flag = store.flag("test-flag").unwrap();
724        let result = evaluate(&store, &flag, &context, None);
725
726        assert_eq!(result.value, Some(&FlagValue::Str("blue".to_string())));
727    }
728
729    #[test]
730    fn variation_index_for_key_works_with_any_kind() {
731        let flag = FlagBuilder::new("test-flag")
732            .variations(vec![
733                FlagValue::Number(0.0),
734                FlagValue::Number(1.0),
735                FlagValue::Number(2.0),
736            ])
737            .variation_index_for_key(Kind::from("device"), "device-789", 1)
738            .build();
739
740        let store = TestStore { flag: Some(flag) };
741        let context = ContextBuilder::new("device-789")
742            .kind("device")
743            .build()
744            .unwrap();
745        let flag = store.flag("test-flag").unwrap();
746        let result = evaluate(&store, &flag, &context, None);
747
748        assert_eq!(result.value, Some(&FlagValue::Number(1.0)));
749    }
750
751    #[test]
752    fn context_targeting_takes_precedence_over_rules() {
753        let flag = FlagBuilder::new("test-flag")
754            .variation_for_user("user-123", true)
755            .if_match("key", vec![AttributeValue::String("user-123".to_string())])
756            .then_return(false)
757            .build();
758
759        let store = TestStore { flag: Some(flag) };
760        let context = ContextBuilder::new("user-123").build().unwrap();
761        let flag = store.flag("test-flag").unwrap();
762        let result = evaluate(&store, &flag, &context, None);
763
764        assert_eq!(result.value, Some(&FlagValue::Bool(true)));
765    }
766
767    #[test]
768    fn targeting_key_removes_from_other_variations() {
769        let flag = FlagBuilder::new("test-flag")
770            .variation_for_user("user-123", true)
771            .variation_for_user("user-123", false)
772            .build();
773
774        // Should only be in the false target now
775        let false_targets: Vec<_> = flag
776            .targets
777            .iter()
778            .filter(|t| t.variation == 1)
779            .flat_map(|t| &t.values)
780            .collect();
781        assert!(false_targets.contains(&&"user-123".to_string()));
782
783        let true_targets: Vec<_> = flag
784            .targets
785            .iter()
786            .filter(|t| t.variation == 0)
787            .flat_map(|t| &t.values)
788            .collect();
789        assert!(!true_targets.contains(&&"user-123".to_string()));
790    }
791
792    #[test]
793    fn clear_targets_removes_all_targets() {
794        let flag = FlagBuilder::new("test-flag")
795            .variation_for_user("user-123", true)
796            .variation_for_user("user-456", false)
797            .clear_targets()
798            .build();
799
800        assert_eq!(flag.targets.len(), 0);
801    }
802
803    #[test]
804    fn if_match_creates_rule_for_user_contexts() {
805        let flag = FlagBuilder::new("test-flag")
806            .if_match(
807                "country",
808                vec![
809                    AttributeValue::String("us".to_string()),
810                    AttributeValue::String("ca".to_string()),
811                ],
812            )
813            .then_return(true)
814            .build();
815
816        let store = TestStore { flag: Some(flag) };
817        let context = ContextBuilder::new("user-123")
818            .set_value("country", AttributeValue::String("us".to_string()))
819            .build()
820            .unwrap();
821        let flag = store.flag("test-flag").unwrap();
822        let result = evaluate(&store, &flag, &context, None);
823
824        assert_eq!(result.value, Some(&FlagValue::Bool(true)));
825    }
826
827    #[test]
828    fn if_match_context_creates_rule_for_any_kind() {
829        let flag = FlagBuilder::new("test-flag")
830            .if_match_context(
831                Kind::from("organization"),
832                "industry",
833                vec![AttributeValue::String("tech".to_string())],
834            )
835            .then_return(true)
836            .build();
837
838        let store = TestStore { flag: Some(flag) };
839        let context = ContextBuilder::new("org-123")
840            .kind("organization")
841            .set_value("industry", AttributeValue::String("tech".to_string()))
842            .build()
843            .unwrap();
844        let flag = store.flag("test-flag").unwrap();
845        let result = evaluate(&store, &flag, &context, None);
846
847        assert_eq!(result.value, Some(&FlagValue::Bool(true)));
848    }
849
850    #[test]
851    fn if_not_match_creates_negated_rule() {
852        let flag = FlagBuilder::new("test-flag")
853            .fallthrough_variation(false)
854            .if_not_match("country", vec![AttributeValue::String("us".to_string())])
855            .then_return(true)
856            .build();
857
858        let store = TestStore {
859            flag: Some(flag.clone()),
860        };
861
862        // US context should NOT match negated rule, gets fallthrough (false)
863        let us_context = ContextBuilder::new("user-123")
864            .set_value("country", AttributeValue::String("us".to_string()))
865            .build()
866            .unwrap();
867        let us_result = evaluate(&store, &flag, &us_context, None);
868        assert_eq!(us_result.value, Some(&FlagValue::Bool(false)));
869
870        // CA context should match negated rule, gets rule value (true)
871        let ca_context = ContextBuilder::new("user-456")
872            .set_value("country", AttributeValue::String("ca".to_string()))
873            .build()
874            .unwrap();
875        let ca_result = evaluate(&store, &flag, &ca_context, None);
876        assert_eq!(ca_result.value, Some(&FlagValue::Bool(true)));
877    }
878
879    #[test]
880    fn if_not_match_context_creates_negated_rule_for_any_kind() {
881        let flag = FlagBuilder::new("test-flag")
882            .fallthrough_variation(false)
883            .if_not_match_context(
884                Kind::from("organization"),
885                "tier",
886                vec![AttributeValue::String("enterprise".to_string())],
887            )
888            .then_return(true)
889            .build();
890
891        let store = TestStore {
892            flag: Some(flag.clone()),
893        };
894
895        // Should match non-enterprise
896        let basic_context = ContextBuilder::new("org-123")
897            .kind("organization")
898            .set_value("tier", AttributeValue::String("basic".to_string()))
899            .build()
900            .unwrap();
901        let basic_result = evaluate(&store, &flag, &basic_context, None);
902        assert_eq!(basic_result.value, Some(&FlagValue::Bool(true)));
903    }
904
905    #[test]
906    fn and_match_adds_multiple_clauses() {
907        let flag = FlagBuilder::new("test-flag")
908            .fallthrough_variation(false)
909            .if_match("country", vec![AttributeValue::String("us".to_string())])
910            .and_match("state", vec![AttributeValue::String("ca".to_string())])
911            .then_return(true)
912            .build();
913
914        let store = TestStore {
915            flag: Some(flag.clone()),
916        };
917
918        // Both conditions match - gets rule value (true)
919        let matching_context = ContextBuilder::new("user-123")
920            .set_value("country", AttributeValue::String("us".to_string()))
921            .set_value("state", AttributeValue::String("ca".to_string()))
922            .build()
923            .unwrap();
924        let matching_result = evaluate(&store, &flag, &matching_context, None);
925        assert_eq!(matching_result.value, Some(&FlagValue::Bool(true)));
926
927        // Only one condition matches - gets fallthrough (false)
928        let partial_context = ContextBuilder::new("user-456")
929            .set_value("country", AttributeValue::String("us".to_string()))
930            .set_value("state", AttributeValue::String("ny".to_string()))
931            .build()
932            .unwrap();
933        let partial_result = evaluate(&store, &flag, &partial_context, None);
934        assert_eq!(partial_result.value, Some(&FlagValue::Bool(false)));
935    }
936
937    #[test]
938    fn and_not_match_adds_negated_clauses() {
939        let flag = FlagBuilder::new("test-flag")
940            .fallthrough_variation(false)
941            .if_match("country", vec![AttributeValue::String("us".to_string())])
942            .and_not_match("state", vec![AttributeValue::String("ca".to_string())])
943            .then_return(true)
944            .build();
945
946        let store = TestStore {
947            flag: Some(flag.clone()),
948        };
949
950        // Matches US but not CA
951        let matching_context = ContextBuilder::new("user-123")
952            .set_value("country", AttributeValue::String("us".to_string()))
953            .set_value("state", AttributeValue::String("ny".to_string()))
954            .build()
955            .unwrap();
956        let matching_result = evaluate(&store, &flag, &matching_context, None);
957        assert_eq!(matching_result.value, Some(&FlagValue::Bool(true)));
958
959        // Matches US and CA (should not match rule)
960        let ca_context = ContextBuilder::new("user-456")
961            .set_value("country", AttributeValue::String("us".to_string()))
962            .set_value("state", AttributeValue::String("ca".to_string()))
963            .build()
964            .unwrap();
965        let ca_result = evaluate(&store, &flag, &ca_context, None);
966        assert_eq!(ca_result.value, Some(&FlagValue::Bool(false))); // fallthrough
967    }
968
969    #[test]
970    fn then_return_completes_rule() {
971        let flag = FlagBuilder::new("test-flag")
972            .if_match("beta", vec![AttributeValue::Bool(true)])
973            .then_return(true)
974            .build();
975
976        assert_eq!(flag.rules.len(), 1);
977        assert_eq!(
978            flag.rules[0].variation_or_rollout,
979            VariationOrRollout::Variation { variation: 0 }
980        );
981    }
982
983    #[test]
984    fn then_return_index_completes_rule_with_index() {
985        let flag = FlagBuilder::new("test-flag")
986            .variations(vec![
987                FlagValue::Str("red".to_string()),
988                FlagValue::Str("green".to_string()),
989                FlagValue::Str("blue".to_string()),
990            ])
991            .if_match("color", vec![AttributeValue::String("primary".to_string())])
992            .then_return_index(2)
993            .build();
994
995        assert_eq!(flag.rules.len(), 1);
996        assert_eq!(
997            flag.rules[0].variation_or_rollout,
998            VariationOrRollout::Variation { variation: 2 }
999        );
1000    }
1001
1002    #[test]
1003    fn rules_evaluated_in_order() {
1004        let flag = FlagBuilder::new("test-flag")
1005            .if_match("key", vec![AttributeValue::String("user-123".to_string())])
1006            .then_return(true)
1007            .if_match("key", vec![AttributeValue::String("user-123".to_string())])
1008            .then_return(false)
1009            .build();
1010
1011        let store = TestStore { flag: Some(flag) };
1012        let context = ContextBuilder::new("user-123").build().unwrap();
1013        let flag = store.flag("test-flag").unwrap();
1014        let result = evaluate(&store, &flag, &context, None);
1015
1016        // First rule should win
1017        assert_eq!(result.value, Some(&FlagValue::Bool(true)));
1018    }
1019
1020    #[test]
1021    fn rules_evaluated_after_targets_before_fallthrough() {
1022        let flag = FlagBuilder::new("test-flag")
1023            .fallthrough_variation(false)
1024            .variation_for_user("user-targeted", true)
1025            .if_match("beta", vec![AttributeValue::Bool(true)])
1026            .then_return(true)
1027            .build();
1028
1029        let store = TestStore {
1030            flag: Some(flag.clone()),
1031        };
1032
1033        // Targeted user gets target value
1034        let targeted_context = ContextBuilder::new("user-targeted")
1035            .set_value("beta", AttributeValue::Bool(true))
1036            .build()
1037            .unwrap();
1038        let targeted_result = evaluate(&store, &flag, &targeted_context, None);
1039        assert_eq!(targeted_result.value, Some(&FlagValue::Bool(true)));
1040
1041        // Non-targeted user with matching rule gets rule value
1042        let rule_context = ContextBuilder::new("user-beta")
1043            .set_value("beta", AttributeValue::Bool(true))
1044            .build()
1045            .unwrap();
1046        let rule_result = evaluate(&store, &flag, &rule_context, None);
1047        assert_eq!(rule_result.value, Some(&FlagValue::Bool(true)));
1048
1049        // Non-targeted user without matching rule gets fallthrough
1050        let fallthrough_context = ContextBuilder::new("user-other")
1051            .set_value("beta", AttributeValue::Bool(false))
1052            .build()
1053            .unwrap();
1054        let fallthrough_result = evaluate(&store, &flag, &fallthrough_context, None);
1055        assert_eq!(fallthrough_result.value, Some(&FlagValue::Bool(false)));
1056    }
1057
1058    #[test]
1059    fn clear_rules_removes_all_rules() {
1060        let flag = FlagBuilder::new("test-flag")
1061            .if_match("country", vec![AttributeValue::String("us".to_string())])
1062            .then_return(true)
1063            .if_match("state", vec![AttributeValue::String("ca".to_string())])
1064            .then_return(false)
1065            .clear_rules()
1066            .build();
1067
1068        assert_eq!(flag.rules.len(), 0);
1069    }
1070
1071    #[test]
1072    fn only_in_operator_used_in_rules() {
1073        let flag = FlagBuilder::new("test-flag")
1074            .fallthrough_variation(false)
1075            .if_match("country", vec![AttributeValue::String("us".to_string())])
1076            .then_return(true)
1077            .build();
1078
1079        assert_eq!(flag.rules.len(), 1);
1080
1081        // Verify the rule works as expected with "in" semantics
1082        let store = TestStore {
1083            flag: Some(flag.clone()),
1084        };
1085
1086        // US matches rule - gets rule value (true)
1087        let us_context = ContextBuilder::new("user-123")
1088            .set_value("country", AttributeValue::String("us".to_string()))
1089            .build()
1090            .unwrap();
1091        let us_result = evaluate(&store, &flag, &us_context, None);
1092        assert_eq!(us_result.value, Some(&FlagValue::Bool(true)));
1093
1094        // CA does not match rule - gets fallthrough (false)
1095        let ca_context = ContextBuilder::new("user-456")
1096            .set_value("country", AttributeValue::String("ca".to_string()))
1097            .build()
1098            .unwrap();
1099        let ca_result = evaluate(&store, &flag, &ca_context, None);
1100        assert_eq!(ca_result.value, Some(&FlagValue::Bool(false)));
1101    }
1102
1103    #[test]
1104    fn sampling_ratio_sets_ratio() {
1105        let flag = FlagBuilder::new("test-flag").sampling_ratio(10000).build();
1106        assert_eq!(flag.sampling_ratio, Some(10000));
1107    }
1108
1109    #[test]
1110    fn sampling_ratio_defaults_to_none() {
1111        let flag = FlagBuilder::new("test-flag").build();
1112        assert_eq!(flag.sampling_ratio, None);
1113    }
1114
1115    #[test]
1116    fn exclude_from_summaries_sets_exclusion() {
1117        let flag = FlagBuilder::new("test-flag")
1118            .exclude_from_summaries(true)
1119            .build();
1120        assert!(flag.exclude_from_summaries);
1121    }
1122
1123    #[test]
1124    fn exclude_from_summaries_defaults_to_false() {
1125        let flag = FlagBuilder::new("test-flag").build();
1126        assert!(!flag.exclude_from_summaries);
1127    }
1128}