1use 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#[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 pub fn key(&self) -> &str {
37 &self.key
38 }
39
40 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 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 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 pub fn on(mut self, on: bool) -> Self {
88 self.on = on;
89 self
90 }
91
92 pub fn fallthrough_variation(self, value: bool) -> Self {
97 self.fallthrough_variation_index(if value { 0 } else { 1 })
98 }
99
100 pub fn fallthrough_variation_index(mut self, index: usize) -> Self {
104 self.fallthrough_variation = index;
105 self
106 }
107
108 pub fn off_variation(self, value: bool) -> Self {
113 self.off_variation_index(if value { 0 } else { 1 })
114 }
115
116 pub fn off_variation_index(mut self, index: usize) -> Self {
120 self.off_variation = index;
121 self
122 }
123
124 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 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 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 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 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 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 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 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 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 pub fn clear_targets(mut self) -> Self {
238 self.targets.clear();
239 self
240 }
241
242 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 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 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 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 pub fn clear_rules(mut self) -> Self {
295 self.rules.clear();
296 self
297 }
298
299 pub fn sampling_ratio(mut self, ratio: u32) -> Self {
301 self.sampling_ratio = Some(ratio);
302 self
303 }
304
305 pub fn exclude_from_summaries(mut self, exclude: bool) -> Self {
307 self.exclude_from_summaries = exclude;
308 self
309 }
310
311 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
340pub 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 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 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 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 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 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 pub fn then_return(self, variation: bool) -> FlagBuilder {
464 self.then_return_index(if variation { 0 } else { 1 })
465 }
466
467 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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))); }
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 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 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 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 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 let store = TestStore {
1083 flag: Some(flag.clone()),
1084 };
1085
1086 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 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}