1use std::collections::HashSet;
2
3use crate::flag::Flag;
4use crate::flag_value::FlagValue;
5use crate::store::Store;
6use crate::variation::VariationIndex;
7use crate::{BucketResult, Context, Target};
8use log::warn;
9use serde::Serialize;
10
11pub struct PrerequisiteEvent {
13 pub target_flag_key: String,
15 pub context: Context,
17 pub prerequisite_flag: Flag,
19 pub prerequisite_result: Detail<FlagValue>,
21}
22
23pub trait PrerequisiteEventRecorder {
25 fn record(&self, event: PrerequisiteEvent);
27}
28
29const PREALLOCATED_PREREQUISITE_CHAIN_SIZE: usize = 20;
30const PREALLOCATED_SEGMENT_CHAIN_SIZE: usize = 20;
31
32pub(crate) struct EvaluationStack {
33 pub(crate) prerequisite_flag_chain: HashSet<String>,
34 pub(crate) segment_chain: HashSet<String>,
35}
36
37impl EvaluationStack {
38 fn new() -> Self {
39 Self {
43 prerequisite_flag_chain: HashSet::with_capacity(PREALLOCATED_PREREQUISITE_CHAIN_SIZE),
44 segment_chain: HashSet::with_capacity(PREALLOCATED_SEGMENT_CHAIN_SIZE),
45 }
46 }
47}
48
49impl Default for EvaluationStack {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55pub fn evaluate<'a>(
62 store: &'a dyn Store,
63 flag: &'a Flag,
64 context: &'a Context,
65 prerequisite_event_recorder: Option<&dyn PrerequisiteEventRecorder>,
66) -> Detail<&'a FlagValue> {
67 let mut evaluation_stack = EvaluationStack::default();
68 evaluate_internal(
69 store,
70 flag,
71 context,
72 prerequisite_event_recorder,
73 &mut evaluation_stack,
74 )
75}
76
77fn evaluate_internal<'a>(
78 store: &'a dyn Store,
79 flag: &'a Flag,
80 context: &'a Context,
81 prerequisite_event_recorder: Option<&dyn PrerequisiteEventRecorder>,
82 evaluation_stack: &mut EvaluationStack,
83) -> Detail<&'a FlagValue> {
84 if !flag.on {
85 return flag.off_value(Reason::Off);
86 }
87
88 if evaluation_stack.prerequisite_flag_chain.contains(&flag.key) {
89 warn!("prerequisite relationship to {} caused a circular reference; this is probably a temporary condition due to an incomplete update", flag.key);
90 return flag.off_value(Reason::Error {
91 error: Error::MalformedFlag,
92 });
93 }
94
95 evaluation_stack
96 .prerequisite_flag_chain
97 .insert(flag.key.clone());
98
99 for prereq in &flag.prerequisites {
100 if let Some(prereq_flag) = store.flag(&prereq.key) {
101 if evaluation_stack
102 .prerequisite_flag_chain
103 .contains(&prereq_flag.key)
104 {
105 return Detail::err(Error::MalformedFlag);
106 }
107
108 let prerequisite_result = evaluate_internal(
109 store,
110 &prereq_flag,
111 context,
112 prerequisite_event_recorder,
113 evaluation_stack,
114 );
115
116 if let Detail {
117 reason: Reason::Error { .. },
118 ..
119 } = prerequisite_result
120 {
121 return Detail::err(Error::MalformedFlag);
122 }
123
124 let variation_index = prerequisite_result.variation_index;
125
126 if let Some(recorder) = prerequisite_event_recorder {
127 recorder.record(PrerequisiteEvent {
128 target_flag_key: flag.key.clone(),
129 context: context.clone(),
130 prerequisite_flag: prereq_flag.clone(),
131 prerequisite_result: prerequisite_result.map(|v| v.clone()),
132 });
133 }
134
135 if !prereq_flag.on || variation_index != Some(prereq.variation) {
136 return flag.off_value(Reason::PrerequisiteFailed {
137 prerequisite_key: prereq.key.to_string(),
138 });
139 }
140 } else {
141 return flag.off_value(Reason::PrerequisiteFailed {
142 prerequisite_key: prereq.key.to_string(),
143 });
144 }
145 }
146
147 evaluation_stack.prerequisite_flag_chain.remove(&flag.key);
148
149 if let Some(variation_index) = any_target_match_variation(context, flag) {
150 return flag.variation(variation_index, Reason::TargetMatch);
151 }
152
153 for (rule_index, rule) in flag.rules.iter().enumerate() {
154 match rule.matches(context, store, evaluation_stack) {
155 Err(e) => {
156 warn!("{:?}", e);
157 return Detail::err(Error::MalformedFlag);
158 }
159 Ok(matches) if matches => {
160 let result = flag.resolve_variation_or_rollout(&rule.variation_or_rollout, context);
161 return match result {
162 Ok(BucketResult {
163 variation_index,
164 in_experiment,
165 }) => {
166 let reason = Reason::RuleMatch {
167 rule_index,
168 rule_id: rule.id.clone(),
169 in_experiment,
170 };
171 flag.variation(variation_index, reason)
172 }
173 Err(e) => Detail::err(e),
174 };
175 }
176 _ => (),
177 }
178 }
179
180 let result = flag.resolve_variation_or_rollout(&flag.fallthrough, context);
181 match result {
182 Ok(BucketResult {
183 variation_index,
184 in_experiment,
185 }) => {
186 let reason = Reason::Fallthrough { in_experiment };
187 flag.variation(variation_index, reason)
188 }
189 Err(e) => Detail::err(e),
190 }
191}
192
193fn any_target_match_variation(context: &Context, flag: &Flag) -> Option<VariationIndex> {
194 if flag.context_targets.is_empty() {
195 for target in &flag.targets {
196 if let Some(index) = target_match_variation(context, target) {
197 return Some(index);
198 }
199 }
200 } else {
201 for context_target in &flag.context_targets {
202 if context_target.context_kind.is_user() && context_target.values.is_empty() {
203 for target in &flag.targets {
204 if target.variation == context_target.variation {
205 if let Some(index) = target_match_variation(context, target) {
206 return Some(index);
207 }
208 }
209 }
210 } else if let Some(index) = target_match_variation(context, context_target) {
211 return Some(index);
212 }
213 }
214 }
215
216 None
217}
218
219fn target_match_variation(context: &Context, target: &Target) -> Option<VariationIndex> {
220 if let Some(context) = context.as_kind(&target.context_kind) {
221 let key = context.key();
222 for value in &target.values {
223 if value == key {
224 return Some(target.variation);
225 }
226 }
227 }
228
229 None
230}
231
232#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
235#[serde(rename_all = "camelCase")]
236pub struct Detail<T> {
237 #[serde(skip_serializing_if = "Option::is_none")]
240 pub value: Option<T>,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
247 pub variation_index: Option<VariationIndex>,
248
249 pub reason: Reason,
251}
252
253impl<T> Detail<T> {
254 pub fn empty(reason: Reason) -> Detail<T> {
260 Detail {
261 value: None,
262 variation_index: None,
263 reason,
264 }
265 }
266
267 pub fn err_default(error: Error, default: T) -> Detail<T> {
274 Detail {
275 value: Some(default),
276 variation_index: None,
277 reason: Reason::Error { error },
278 }
279 }
280
281 pub fn err(error: Error) -> Detail<T> {
283 Detail::empty(Reason::Error { error })
284 }
285
286 pub fn map<U, F>(self, f: F) -> Detail<U>
289 where
290 F: FnOnce(T) -> U,
291 {
292 Detail {
293 value: self.value.map(f),
294 variation_index: self.variation_index,
295 reason: self.reason,
296 }
297 }
298
299 pub fn should_have_value(mut self, e: Error) -> Detail<T> {
302 if self.value.is_none() {
303 self.reason = Reason::Error { error: e };
304 }
305 self
306 }
307
308 pub fn try_map<U, F>(self, f: F, default: U, e: Error) -> Detail<U>
318 where
319 F: FnOnce(T) -> Option<U>,
320 {
321 if self.value.is_none() {
322 return Detail {
323 value: Some(default),
324 variation_index: self.variation_index,
325 reason: self.reason,
326 };
327 }
328 match f(self.value.unwrap()) {
329 Some(v) => Detail {
330 value: Some(v),
331 variation_index: self.variation_index,
332 reason: self.reason,
333 },
334 None => Detail::err_default(e, default),
335 }
336 }
337
338 pub fn or(mut self, default: T) -> Detail<T> {
343 if self.value.is_none() {
344 self.value = Some(default);
345 self.variation_index = None;
346 }
348 self
349 }
350
351 pub fn or_else<F>(mut self, default: F) -> Detail<T>
357 where
358 F: Fn() -> T,
359 {
360 if self.value.is_none() {
361 self.value = Some(default());
362 self.variation_index = None;
363 }
364 self
365 }
366}
367
368#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
370#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "kind")]
371pub enum Reason {
372 Off,
374 TargetMatch,
376 #[serde(rename_all = "camelCase")]
378 RuleMatch {
379 rule_index: usize,
381 #[serde(skip_serializing_if = "String::is_empty")]
382 rule_id: String,
384 #[serde(skip_serializing_if = "std::ops::Not::not")]
388 in_experiment: bool,
389 },
390 #[serde(rename_all = "camelCase")]
393 PrerequisiteFailed {
394 prerequisite_key: String,
396 },
397 #[serde(rename_all = "camelCase")]
400 Fallthrough {
401 #[serde(skip_serializing_if = "std::ops::Not::not")]
405 in_experiment: bool,
406 },
407 Error {
411 #[serde(rename = "errorKind")]
413 error: Error,
414 },
415}
416
417impl Reason {
418 pub fn is_in_experiment(&self) -> bool {
421 match self {
422 Reason::RuleMatch { in_experiment, .. } => *in_experiment,
423 Reason::Fallthrough { in_experiment } => *in_experiment,
424 _ => false,
425 }
426 }
427}
428
429#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
432#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
433pub enum Error {
434 ClientNotReady,
437 FlagNotFound,
440 MalformedFlag,
443 WrongType,
446 Exception,
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use crate::contexts::context::Kind;
455 use crate::flag_value::FlagValue::{Bool, Str};
456 use crate::test_common::{InMemoryPrerequisiteEventRecorder, TestStore};
457 use crate::variation::VariationOrRollout;
458 use crate::{AttributeValue, ContextBuilder, MultiContextBuilder};
459 use spectral::prelude::*;
460 use std::cell::RefCell;
461 use test_case::test_case;
462
463 #[test]
464 fn test_eval_flag_basic() {
465 let store = TestStore::new();
466 let alice = ContextBuilder::new("alice").build().unwrap(); let bob = ContextBuilder::new("bob").build().unwrap(); let mut flag = store.flag("flagWithTarget").unwrap();
469
470 assert!(!flag.on);
471 let detail = evaluate(&store, &flag, &alice, None);
472 assert_that!(detail.value).contains_value(&Bool(false));
473 assert_that!(detail.variation_index).contains_value(0);
474 assert_that!(detail.reason).is_equal_to(&Reason::Off);
475
476 assert_that!(evaluate(&store, &flag, &bob, None)).is_equal_to(&detail);
477
478 flag.off_variation = Some(1);
480 let detail = evaluate(&store, &flag, &alice, None);
481 assert_that!(detail.value).contains_value(&Bool(true));
482 assert_that!(detail.variation_index).contains_value(1);
483
484 flag.off_variation = None;
486 let detail = evaluate(&store, &flag, &alice, None);
487 assert_that!(detail.value).is_none();
488 assert_that!(detail.variation_index).is_none();
489 assert_that!(detail.reason).is_equal_to(&Reason::Off);
490
491 flag.on = true;
493 let detail = evaluate(&store, &flag, &alice, None);
494 assert_that!(detail.value).contains_value(&Bool(true));
495 assert_that!(detail.variation_index).contains_value(1);
496 assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
497 in_experiment: false,
498 });
499
500 let detail = evaluate(&store, &flag, &bob, None);
501 assert_that!(detail.value).contains_value(&Bool(false));
502 assert_that!(detail.variation_index).contains_value(0);
503 assert_that!(detail.reason).is_equal_to(&Reason::TargetMatch);
504
505 flag.fallthrough = VariationOrRollout::Variation { variation: 0 };
507 let detail = evaluate(&store, &flag, &alice, None);
508 assert_that!(detail.value).contains_value(&Bool(false));
509 assert_that!(detail.variation_index).contains_value(0);
510
511 let detail = evaluate(&store, &flag, &bob, None);
513 assert_that!(detail.value).contains_value(&Bool(false));
514 assert_that!(detail.variation_index).contains_value(0);
515 assert_that!(detail.reason).is_equal_to(&Reason::TargetMatch);
516 }
517
518 #[test]
519 fn test_eval_with_matches_op_groups() {
520 let store = TestStore::new();
521 let alice = ContextBuilder::new("alice").build().unwrap(); let mut bob_builder = ContextBuilder::new("bob");
523 bob_builder.try_set_value(
524 "groups",
525 AttributeValue::Array(vec![AttributeValue::String("my-group".into())]),
526 );
527 let bob = bob_builder.build().unwrap(); let flag = store.flag("flagWithMatchesOpOnGroups").unwrap();
529
530 let detail = evaluate(&store, &flag, &alice, None);
531 assert_that!(detail.value).contains_value(&Bool(true));
532 assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
533 in_experiment: false,
534 });
535
536 let detail = evaluate(&store, &flag, &bob, None);
537 assert_that!(detail.value).contains_value(&Bool(false));
538 assert_that!(detail.variation_index).contains_value(0);
539 assert_that!(detail.reason).is_equal_to(&Reason::RuleMatch {
540 rule_index: 0,
541 rule_id: "6a7755ac-e47a-40ea-9579-a09dd5f061bd".into(),
542 in_experiment: false,
543 });
544 }
545
546 #[test_case("flagWithMatchesOpOnKinds")]
547 #[test_case("flagWithMatchesOpOnKindsAttributeReference")]
550 #[test_case("flagWithMatchesOpOnKindsPlainAttributeReference")]
551 fn test_eval_with_matches_op_kinds(flag_key: &str) {
555 let store = TestStore::new();
556
557 let alice = ContextBuilder::new("alice").build().unwrap(); let mut bob_builder = ContextBuilder::new("bob");
560 let bob = bob_builder.kind("company").build().unwrap(); let flag = store.flag(flag_key).unwrap();
563
564 let detail = evaluate(&store, &flag, &alice, None);
565
566 assert_that!(detail.value).contains_value(&Bool(false));
567 assert_that!(detail.variation_index).contains_value(0);
568 assert_that!(detail.reason).is_equal_to(&Reason::RuleMatch {
569 rule_index: 0,
570 rule_id: "6a7755ac-e47a-40ea-9579-a09dd5f061bd".into(),
571 in_experiment: false,
572 });
573
574 let detail = evaluate(&store, &flag, &bob, None);
575 assert_that!(detail.value).contains_value(&Bool(true));
576 assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
577 in_experiment: false,
578 });
579
580 bob_builder.kind("org");
581 let new_bob = bob_builder.build().unwrap(); let detail = evaluate(&store, &flag, &new_bob, None);
583 assert_that!(detail.value).contains_value(&Bool(false));
584 assert_that!(detail.variation_index).contains_value(0);
585 assert_that!(detail.reason).is_equal_to(&Reason::RuleMatch {
586 rule_index: 0,
587 rule_id: "6a7755ac-e47a-40ea-9579-a09dd5f061bd".into(),
588 in_experiment: false,
589 });
590 }
591
592 #[test]
593 fn test_prerequisite_events_are_captured() {
594 let recorder = InMemoryPrerequisiteEventRecorder {
595 events: RefCell::new(Vec::new()),
596 };
597 let store = TestStore::new();
598 let alice = ContextBuilder::new("alice").build().unwrap();
599 let flag = store.flag("flagWithNestedPrereq").unwrap();
600
601 let _ = evaluate(&store, &flag, &alice, Some(&recorder));
602 assert_that!(*recorder.events.borrow()).has_length(2);
603
604 let event = &recorder.events.borrow()[0];
605 assert_eq!("flagWithSatisfiedPrereq", event.target_flag_key);
606 assert_eq!("prereq", event.prerequisite_flag.key);
607 assert!(event.prerequisite_flag.exclude_from_summaries);
608
609 let event = &recorder.events.borrow()[1];
610 assert_eq!("flagWithNestedPrereq", event.target_flag_key);
611 assert_eq!("flagWithSatisfiedPrereq", event.prerequisite_flag.key);
612 assert!(!event.prerequisite_flag.exclude_from_summaries);
613 }
614
615 #[test]
616 fn test_eval_flag_rules() {
617 let store = TestStore::new();
618 let alice = ContextBuilder::new("alice").build().unwrap();
619 let bob = ContextBuilder::new("bob")
620 .set_value("team", "Avengers".into())
621 .build()
622 .unwrap();
623
624 let mut flag = store.flag("flagWithInRule").unwrap();
625
626 assert!(!flag.on);
627 for context in &[&alice, &bob] {
628 let detail = evaluate(&store, &flag, context, None);
629 assert_that!(detail.value).contains_value(&Bool(false));
630 assert_that!(detail.variation_index).contains_value(0);
631 assert_that!(detail.reason).is_equal_to(&Reason::Off);
632 }
633
634 flag.on = true;
636 let detail = evaluate(&store, &flag, &alice, None);
637 assert_that!(detail.value).contains_value(&Bool(true));
638 assert_that!(detail.variation_index).contains_value(1);
639 assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
640 in_experiment: false,
641 });
642
643 let detail = evaluate(&store, &flag, &bob, None);
644 assert_that!(detail.value).contains_value(&Bool(false));
645 assert_that!(detail.variation_index).contains_value(0);
646 assert_that!(detail.reason).is_equal_to(&Reason::RuleMatch {
647 rule_id: "in-rule".to_string(),
648 rule_index: 0,
649 in_experiment: false,
650 });
651 }
652
653 #[test]
654 fn test_eval_flag_unsatisfied_prereq() {
655 let store = TestStore::new();
656 let flag = store.flag("flagWithMissingPrereq").unwrap();
657 assert!(flag.on);
658
659 let alice = ContextBuilder::new("alice").build().unwrap();
660 let bob = ContextBuilder::new("bob").build().unwrap();
661
662 for user in &[&alice, &bob] {
663 let detail = evaluate(&store, &flag, user, None);
664 assert_that!(detail.value).contains_value(&Bool(false));
665 assert_that!(detail.reason).is_equal_to(&Reason::PrerequisiteFailed {
666 prerequisite_key: "badPrereq".to_string(),
667 });
668 }
669 }
670
671 #[test]
672 fn test_eval_flag_off_prereq() {
673 let store = TestStore::new();
674 let flag = store.flag("flagWithOffPrereq").unwrap();
675 assert!(flag.on);
676
677 let alice = ContextBuilder::new("alice").build().unwrap();
678
679 let detail = evaluate(&store, &flag, &alice, None);
680 assert_that!(detail.value).contains_value(&Bool(false));
681 assert_that!(detail.reason).is_equal_to(&Reason::PrerequisiteFailed {
682 prerequisite_key: "offPrereq".to_string(),
683 });
684 }
685
686 #[test]
687 fn test_eval_flag_satisfied_prereq() {
688 let mut store = TestStore::new();
689 let flag = store.flag("flagWithSatisfiedPrereq").unwrap();
690
691 let alice = ContextBuilder::new("alice").build().unwrap();
692 let bob = ContextBuilder::new("bob").build().unwrap();
693
694 let detail = evaluate(&store, &flag, &alice, None);
695 asserting!("alice should pass prereq and see fallthrough")
696 .that(&detail.value)
697 .contains_value(&Bool(true));
698 let detail = evaluate(&store, &flag, &bob, None);
699 asserting!("bob should see prereq failed due to target")
700 .that(&detail.value)
701 .contains_value(&Bool(false));
702 assert_that!(detail.reason).is_equal_to(Reason::PrerequisiteFailed {
703 prerequisite_key: "prereq".to_string(),
704 });
705
706 store.update_flag("prereq", |flag| (flag.on = false));
708 for user in &[&alice, &bob] {
709 let detail = evaluate(&store, &flag, user, None);
710 assert_that!(detail.value).contains_value(&Bool(false));
711 assert_that!(detail.reason).is_equal_to(&Reason::PrerequisiteFailed {
712 prerequisite_key: "prereq".to_string(),
713 });
714 }
715 }
716
717 #[test]
718 fn test_flag_targets_different_context() {
719 let store = TestStore::new();
720 let alice_user_context = ContextBuilder::new("alice").build().unwrap();
721 let bob_user_context = ContextBuilder::new("bob").build().unwrap();
722 let org_context = ContextBuilder::new("LaunchDarkly")
723 .kind("org")
724 .build()
725 .unwrap();
726 let flag = store.flag("flagWithContextTarget").unwrap();
727
728 let detail = evaluate(&store, &flag, &org_context, None);
729 assert_that!(detail.value).contains_value(&Bool(true));
730 assert_that!(detail.variation_index).contains_value(1);
731 assert_that!(detail.reason).is_equal_to(&Reason::TargetMatch);
732
733 let detail = evaluate(&store, &flag, &alice_user_context, None);
734 assert_that!(detail.value).contains_value(&Bool(false));
735 assert_that!(detail.variation_index).contains_value(0);
736 assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
737 in_experiment: false,
738 });
739
740 let detail = evaluate(&store, &flag, &bob_user_context, None);
741 assert_that!(detail.value).contains_value(&Bool(true));
742 assert_that!(detail.variation_index).contains_value(1);
743 assert_that!(detail.reason).is_equal_to(&Reason::TargetMatch);
744 }
745
746 #[test]
747 fn test_eval_flag_single_user_segments() {
748 let store = TestStore::new();
749 let flag = store.flag("flagWithSegmentMatchRule").unwrap();
750
751 let alice = ContextBuilder::new("alice").build().unwrap();
752 let bob = ContextBuilder::new("bob").build().unwrap();
753
754 let detail = evaluate(&store, &flag, &alice, None);
755 asserting!("alice is in segment, should see false with RuleMatch")
756 .that(&detail.value)
757 .contains_value(&Bool(false));
758 assert_that!(detail.reason).is_equal_to(Reason::RuleMatch {
759 rule_id: "match-rule".to_string(),
760 rule_index: 0,
761 in_experiment: false,
762 });
763 let detail = evaluate(&store, &flag, &bob, None);
764 asserting!("bob is not in segment and should see fallthrough")
765 .that(&detail.value)
766 .contains_value(&Bool(true));
767 assert_that!(detail.reason).is_equal_to(Reason::Fallthrough {
768 in_experiment: false,
769 });
770 }
771
772 #[test]
773 fn test_eval_multi_context_user_segment() {
774 let store = TestStore::new();
775 let flag = store.flag("flagWithSegmentMatchRule").unwrap();
776 let alice = ContextBuilder::new("alice").build();
777 let taco = ContextBuilder::new("taco").kind("food").build();
778 let multi = MultiContextBuilder::new()
779 .add_context(taco.unwrap())
780 .add_context(alice.unwrap())
781 .build()
782 .unwrap();
783
784 let detail = evaluate(&store, &flag, &multi, None);
785
786 asserting!("alice is in segment, should see false with RuleMatch")
787 .that(&detail.value)
788 .contains_value(&Bool(false));
789 assert_that!(detail.reason).is_equal_to(Reason::RuleMatch {
790 rule_id: "match-rule".to_string(),
791 rule_index: 0,
792 in_experiment: false,
793 });
794 }
795
796 #[test]
797 fn test_eval_single_franchise_segment() {
798 let store = TestStore::new();
799 let flag = store.flag("flagWithContextSegmentMatchRule").unwrap();
800 let macdonwalds = ContextBuilder::new("macdonwalds")
801 .kind("franchise")
802 .build()
803 .unwrap();
804
805 let detail = evaluate(&store, &flag, &macdonwalds, None);
806
807 asserting!("macdonwalds is in segment, should see false with RuleMatch")
808 .that(&detail.value)
809 .contains_value(&Bool(false));
810 assert_that!(detail.reason).is_equal_to(Reason::RuleMatch {
811 rule_id: "match-rule".to_string(),
812 rule_index: 0,
813 in_experiment: false,
814 });
815 }
816
817 #[test]
818 fn test_eval_multi_franchise_segment() {
819 let store = TestStore::new();
820 let flag = store.flag("flagWithContextSegmentMatchRule").unwrap();
821 let macdonwalds = ContextBuilder::new("macdonwalds")
822 .kind("franchise")
823 .build()
824 .unwrap();
825
826 let alice = ContextBuilder::new("alice").build().unwrap();
827 let multi = MultiContextBuilder::new()
828 .add_context(alice)
829 .add_context(macdonwalds)
830 .build()
831 .unwrap();
832
833 let detail = evaluate(&store, &flag, &multi, None);
834
835 asserting!("macdonwalds is in segment, should see false with RuleMatch")
836 .that(&detail.value)
837 .contains_value(&Bool(false));
838 assert_that!(detail.reason).is_equal_to(Reason::RuleMatch {
839 rule_id: "match-rule".to_string(),
840 rule_index: 0,
841 in_experiment: false,
842 });
843 }
844
845 #[test]
846 fn test_eval_multi_franchise_segment_exclude() {
847 let store = TestStore::new();
848 let flag = store
849 .flag("flagWithContextExcludeSegmentMatchRule")
850 .unwrap();
851 let tacochime = ContextBuilder::new("tacochime")
852 .kind("franchise")
853 .build()
854 .unwrap();
855
856 let alice = ContextBuilder::new("alice").build().unwrap();
857 let multi = MultiContextBuilder::new()
858 .add_context(alice)
859 .add_context(tacochime)
860 .build()
861 .unwrap();
862
863 let detail = evaluate(&store, &flag, &multi, None);
864
865 asserting!("tacochime is not in the segment, should see false with Fallthrough")
866 .that(&detail.value)
867 .contains_value(&Bool(true));
868 assert_that!(detail.reason).is_equal_to(Reason::Fallthrough {
869 in_experiment: false,
870 });
871 }
872
873 #[test]
874 fn test_flag_has_prereq_which_duplicates_segment_rule() {
875 let store = TestStore::new();
876 let flag = store
877 .flag("flagWithPrereqWhichDuplicatesSegmentRuleCheck")
878 .unwrap();
879
880 let alice = ContextBuilder::new("alice").build().unwrap();
881
882 let mut evaluation_stack = EvaluationStack::default();
883 let detail = evaluate_internal(&store, &flag, &alice, None, &mut evaluation_stack);
884 asserting!("alice is in segment, should see false with RuleMatch")
885 .that(&detail.value)
886 .contains_value(&Bool(false));
887 assert_that!(detail.reason).is_equal_to(Reason::RuleMatch {
888 rule_id: "match-rule".to_string(),
889 rule_index: 0,
890 in_experiment: false,
891 });
892 assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
893 assert!(evaluation_stack.segment_chain.is_empty());
894 }
895
896 #[test]
900 fn test_simple_prereq_cycle() {
901 let flag_json = r#"{
902 "flagA": {
903 "key": "flagA",
904 "targets": [],
905 "rules": [],
906 "salt": "salty",
907 "prerequisites": [{
908 "key": "flagB",
909 "variation": 0
910 }],
911 "on": true,
912 "fallthrough": {"variation": 0},
913 "offVariation": 1,
914 "variations": [true, false]
915 },
916 "flagB": {
917 "key": "flagB",
918 "targets": [],
919 "rules": [],
920 "salt": "salty",
921 "prerequisites": [{
922 "key": "flagA",
923 "variation": 0
924 }],
925 "on": true,
926 "fallthrough": {"variation": 0},
927 "offVariation": 1,
928 "variations": [true, false]
929 }
930 }"#;
931 let store = TestStore::new_from_json_str(flag_json, "{}");
932 let flag = store.flag("flagA").unwrap();
933
934 let alice = ContextBuilder::new("alice").build().unwrap();
935
936 let detail = evaluate(&store, &flag, &alice, None);
937 assert_that!(detail.value).is_none();
938 assert_that!(detail.reason).is_equal_to(Reason::Error {
939 error: Error::MalformedFlag,
940 });
941 }
942
943 #[test]
948 fn test_eval_flag_with_first_prereq_as_prereq_of_second_prereq() {
949 let store = TestStore::new();
950 let flag = store
951 .flag("flagWithFirstPrereqAsPrereqToSecondPrereq")
952 .unwrap();
953
954 let alice = ContextBuilder::new("alice").build().unwrap();
955 let bob = ContextBuilder::new("bob").build().unwrap();
956
957 let mut evaluation_stack = EvaluationStack::default();
958 let detail = evaluate_internal(&store, &flag, &alice, None, &mut evaluation_stack);
959 asserting!("alice should pass prereq and see fallthrough")
960 .that(&detail.value)
961 .contains_value(&Bool(true));
962 assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
963 assert!(evaluation_stack.segment_chain.is_empty());
964
965 let detail = evaluate(&store, &flag, &bob, None);
966 asserting!("bob should see prereq failed due to target")
967 .that(&detail.value)
968 .contains_value(&Bool(false));
969 assert_that!(detail.reason).is_equal_to(Reason::PrerequisiteFailed {
970 prerequisite_key: "prereq".to_string(),
971 });
972 }
973
974 #[test]
980 fn test_prereq_cycle_across_three_flags() {
981 let flag_json = r#"{
982 "flagA": {
983 "key": "flagA",
984 "targets": [],
985 "rules": [],
986 "salt": "salty",
987 "prerequisites": [{
988 "key": "flagB",
989 "variation": 0
990 }],
991 "on": true,
992 "fallthrough": {"variation": 0},
993 "offVariation": 1,
994 "variations": [true, false]
995 },
996 "flagB": {
997 "key": "flagB",
998 "targets": [],
999 "rules": [],
1000 "salt": "salty",
1001 "prerequisites": [{
1002 "key": "flagC",
1003 "variation": 0
1004 }],
1005 "on": true,
1006 "fallthrough": {"variation": 0},
1007 "offVariation": 1,
1008 "variations": [true, false]
1009 },
1010 "flagC": {
1011 "key": "flagC",
1012 "targets": [],
1013 "rules": [],
1014 "salt": "salty",
1015 "prerequisites": [{
1016 "key": "flagA",
1017 "variation": 0
1018 }],
1019 "on": true,
1020 "fallthrough": {"variation": 0},
1021 "offVariation": 1,
1022 "variations": [true, false]
1023 }
1024 }"#;
1025 let store = TestStore::new_from_json_str(flag_json, "{}");
1026 let flag = store.flag("flagA").unwrap();
1027
1028 let alice = ContextBuilder::new("alice").build().unwrap();
1029
1030 let detail = evaluate(&store, &flag, &alice, None);
1031 assert_that!(detail.value).is_none();
1032 assert_that!(detail.reason).is_equal_to(Reason::Error {
1033 error: Error::MalformedFlag,
1034 });
1035 }
1036
1037 #[test]
1040 fn test_flag_and_prereq_share_segment_check() {
1041 let flag_json = r#"{
1042 "flagA": {
1043 "key": "flagA",
1044 "targets": [],
1045 "rules": [{
1046 "variation": 0,
1047 "id": "rule-1",
1048 "clauses": [
1049 {
1050 "contextKind": "user",
1051 "attribute": "key",
1052 "negate": false,
1053 "op": "segmentMatch",
1054 "values": ["segmentA"]
1055 }
1056 ],
1057 "trackEvents": true
1058 }],
1059 "salt": "salty",
1060 "prerequisites": [{
1061 "key": "flagB",
1062 "variation": 0
1063 }],
1064 "on": true,
1065 "fallthrough": {"variation": 0},
1066 "offVariation": 1,
1067 "variations": [true, false]
1068 },
1069 "flagB": {
1070 "key": "flagB",
1071 "targets": [],
1072 "rules": [{
1073 "variation": 0,
1074 "id": "rule-2",
1075 "clauses": [
1076 {
1077 "contextKind": "user",
1078 "attribute": "key",
1079 "negate": false,
1080 "op": "segmentMatch",
1081 "values": ["segmentA"]
1082 }
1083 ],
1084 "trackEvents": true
1085 }],
1086 "salt": "salty",
1087 "prerequisites": [],
1088 "on": true,
1089 "fallthrough": {"variation": 0},
1090 "offVariation": 1,
1091 "variations": [true, false]
1092 }
1093 }"#;
1094 let segment_json = r#"{
1095 "segmentA": {
1096 "key": "segmentA",
1097 "included": ["alice"],
1098 "includedContexts": [{
1099 "values": [],
1100 "contextKind": "user"
1101 }],
1102 "excluded": [],
1103 "rules": [],
1104 "salt": "salty",
1105 "version": 1
1106 }
1107 }"#;
1108 let store = TestStore::new_from_json_str(flag_json, segment_json);
1109 let flag = store.flag("flagA").unwrap();
1110
1111 let alice = ContextBuilder::new("alice").build().unwrap();
1112
1113 let mut evaluation_stack = EvaluationStack::default();
1114 let detail = evaluate_internal(&store, &flag, &alice, None, &mut evaluation_stack);
1115 assert_that!(detail.value).contains_value(&Bool(true));
1116 assert_that!(detail.reason).is_equal_to(Reason::RuleMatch {
1117 rule_index: 0,
1118 rule_id: "rule-1".into(),
1119 in_experiment: false,
1120 });
1121 assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
1122 assert!(evaluation_stack.segment_chain.is_empty());
1123 }
1124
1125 #[test]
1129 fn test_prereqs_can_share_segment_check() {
1130 let flag_json = r#"{
1131 "flagA": {
1132 "key": "flagA",
1133 "targets": [],
1134 "rules": [],
1135 "salt": "salty",
1136 "prerequisites": [
1137 {
1138 "key": "flagB",
1139 "variation": 0
1140 },
1141 {
1142 "key": "flagC",
1143 "variation": 0
1144 }
1145 ],
1146 "on": true,
1147 "fallthrough": {"variation": 0},
1148 "offVariation": 1,
1149 "variations": [true, false]
1150 },
1151 "flagB": {
1152 "key": "flagB",
1153 "targets": [],
1154 "rules": [{
1155 "variation": 0,
1156 "id": "rule-2",
1157 "clauses": [
1158 {
1159 "contextKind": "user",
1160 "attribute": "key",
1161 "negate": false,
1162 "op": "segmentMatch",
1163 "values": ["segmentA"]
1164 }
1165 ],
1166 "trackEvents": true
1167 }],
1168 "salt": "salty",
1169 "prerequisites": [],
1170 "on": true,
1171 "fallthrough": {"variation": 0},
1172 "offVariation": 1,
1173 "variations": [true, false]
1174 },
1175 "flagC": {
1176 "key": "flagC",
1177 "targets": [],
1178 "rules": [{
1179 "variation": 0,
1180 "id": "rule-2",
1181 "clauses": [
1182 {
1183 "contextKind": "user",
1184 "attribute": "key",
1185 "negate": false,
1186 "op": "segmentMatch",
1187 "values": ["segmentA"]
1188 }
1189 ],
1190 "trackEvents": true
1191 }],
1192 "salt": "salty",
1193 "prerequisites": [],
1194 "on": true,
1195 "fallthrough": {"variation": 0},
1196 "offVariation": 1,
1197 "variations": [true, false]
1198 }
1199 }"#;
1200 let segment_json = r#"{
1201 "segmentA": {
1202 "key": "segmentA",
1203 "included": ["alice"],
1204 "includedContexts": [{
1205 "values": [],
1206 "contextKind": "user"
1207 }],
1208 "excluded": [],
1209 "rules": [],
1210 "salt": "salty",
1211 "version": 1
1212 }
1213 }"#;
1214 let store = TestStore::new_from_json_str(flag_json, segment_json);
1215 let flag = store.flag("flagA").unwrap();
1216
1217 let alice = ContextBuilder::new("alice").build().unwrap();
1218
1219 let mut evaluation_stack = EvaluationStack::default();
1220 let detail = evaluate_internal(&store, &flag, &alice, None, &mut evaluation_stack);
1221 assert_that!(detail.value).contains_value(&Bool(true));
1222 assert_that!(detail.reason).is_equal_to(Reason::Fallthrough {
1223 in_experiment: false,
1224 });
1225 assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
1226 assert!(evaluation_stack.segment_chain.is_empty());
1227 }
1228
1229 #[test]
1234 fn test_segment_has_basic_recursive_condition() {
1235 let flag_json = r#"{
1236 "flagA": {
1237 "key": "flagA",
1238 "targets": [],
1239 "rules": [{
1240 "variation": 0,
1241 "id": "rule-1",
1242 "clauses": [
1243 {
1244 "contextKind": "user",
1245 "attribute": "key",
1246 "negate": false,
1247 "op": "segmentMatch",
1248 "values": ["segmentA"]
1249 }
1250 ],
1251 "trackEvents": true
1252 }],
1253 "salt": "salty",
1254 "prerequisites": [],
1255 "on": true,
1256 "fallthrough": {"variation": 0},
1257 "offVariation": 1,
1258 "variations": [true, false]
1259 }
1260 }"#;
1261 let segment_json = r#"{
1262 "segmentA": {
1263 "key": "segmentA",
1264 "included": [],
1265 "includedContexts": [],
1266 "excluded": [],
1267 "rules": [{
1268 "id": "rule-1",
1269 "clauses": [{
1270 "attribute": "key",
1271 "negate": false,
1272 "op": "segmentMatch",
1273 "values": ["segmentB"],
1274 "contextKind": "user"
1275 }]
1276 }],
1277 "salt": "salty",
1278 "version": 1
1279 },
1280 "segmentB": {
1281 "key": "segmentB",
1282 "included": [],
1283 "includedContexts": [],
1284 "excluded": [],
1285 "rules": [{
1286 "id": "rule-1",
1287 "clauses": [{
1288 "attribute": "key",
1289 "negate": false,
1290 "op": "segmentMatch",
1291 "values": ["segmentA"],
1292 "contextKind": "user"
1293 }]
1294 }],
1295 "salt": "salty",
1296 "version": 1
1297 }
1298 }"#;
1299 let store = TestStore::new_from_json_str(flag_json, segment_json);
1300 let flag = store.flag("flagA").unwrap();
1301
1302 let alice = ContextBuilder::new("alice").build().unwrap();
1303
1304 let detail = evaluate(&store, &flag, &alice, None);
1305 assert_that!(detail.value).is_none();
1306 assert_that!(detail.reason).is_equal_to(Reason::Error {
1307 error: Error::MalformedFlag,
1308 });
1309 }
1310
1311 #[test]
1315 fn test_segment_depends_on_self() {
1316 let flag_json = r#"{
1317 "flagA": {
1318 "key": "flagA",
1319 "targets": [],
1320 "rules": [{
1321 "variation": 0,
1322 "id": "rule-1",
1323 "clauses": [
1324 {
1325 "contextKind": "user",
1326 "attribute": "key",
1327 "negate": false,
1328 "op": "segmentMatch",
1329 "values": ["segmentA"]
1330 }
1331 ],
1332 "trackEvents": true
1333 }],
1334 "salt": "salty",
1335 "prerequisites": [],
1336 "on": true,
1337 "fallthrough": {"variation": 0},
1338 "offVariation": 1,
1339 "variations": [true, false]
1340 }
1341 }"#;
1342 let segment_json = r#"{
1343 "segmentA": {
1344 "key": "segmentA",
1345 "included": [],
1346 "includedContexts": [],
1347 "excluded": [],
1348 "rules": [{
1349 "id": "rule-1",
1350 "clauses": [{
1351 "attribute": "key",
1352 "negate": false,
1353 "op": "segmentMatch",
1354 "values": ["segmentA"],
1355 "contextKind": "user"
1356 }]
1357 }],
1358 "salt": "salty",
1359 "version": 1
1360 }
1361 }"#;
1362 let store = TestStore::new_from_json_str(flag_json, segment_json);
1363 let flag = store.flag("flagA").unwrap();
1364
1365 let alice = ContextBuilder::new("alice").build().unwrap();
1366
1367 let detail = evaluate(&store, &flag, &alice, None);
1368 assert_that!(detail.value).is_none();
1369 assert_that!(detail.reason).is_equal_to(Reason::Error {
1370 error: Error::MalformedFlag,
1371 });
1372 }
1373
1374 #[test]
1380 fn test_flag_has_segment_check_and_prereq_also_has_subset_of_segment_checks() {
1381 let flag_json = r#"{
1382 "flagA": {
1383 "key": "flagA",
1384 "targets": [],
1385 "rules": [{
1386 "variation": 0,
1387 "id": "rule-1",
1388 "clauses": [
1389 {
1390 "contextKind": "user",
1391 "attribute": "key",
1392 "negate": false,
1393 "op": "segmentMatch",
1394 "values": ["segmentA"]
1395 }
1396 ],
1397 "trackEvents": true
1398 }],
1399 "salt": "salty",
1400 "prerequisites": [{
1401 "key": "flagB",
1402 "variation": 0
1403 }],
1404 "on": true,
1405 "fallthrough": {"variation": 0},
1406 "offVariation": 1,
1407 "variations": [true, false]
1408 },
1409 "flagB": {
1410 "key": "flagB",
1411 "targets": [],
1412 "rules": [{
1413 "variation": 0,
1414 "id": "rule-2",
1415 "clauses": [
1416 {
1417 "contextKind": "user",
1418 "attribute": "key",
1419 "negate": false,
1420 "op": "segmentMatch",
1421 "values": ["segmentB"]
1422 }
1423 ],
1424 "trackEvents": true
1425 }],
1426 "salt": "salty",
1427 "prerequisites": [],
1428 "on": true,
1429 "fallthrough": {"variation": 0},
1430 "offVariation": 1,
1431 "variations": [true, false]
1432 }
1433 }"#;
1434 let segment_json = r#"{
1435 "segmentA": {
1436 "key": "segmentA",
1437 "included": [],
1438 "includedContexts": [],
1439 "excluded": [],
1440 "rules": [{
1441 "id": "rule-1",
1442 "clauses": [{
1443 "attribute": "key",
1444 "negate": false,
1445 "op": "segmentMatch",
1446 "values": ["segmentB"],
1447 "contextKind": "user"
1448 }]
1449 }],
1450 "salt": "salty",
1451 "version": 1
1452 },
1453 "segmentB": {
1454 "key": "segmentB",
1455 "included": [],
1456 "includedContexts": [],
1457 "excluded": [],
1458 "rules": [{
1459 "id": "rule-1",
1460 "clauses": [{
1461 "attribute": "key",
1462 "negate": false,
1463 "op": "segmentMatch",
1464 "values": ["segmentC"],
1465 "contextKind": "user"
1466 }]
1467 }],
1468 "salt": "salty",
1469 "version": 1
1470 },
1471 "segmentC": {
1472 "key": "segmentC",
1473 "included": ["alice"],
1474 "includedContexts": [],
1475 "excluded": ["bob"],
1476 "rules": [],
1477 "salt": "salty",
1478 "version": 1
1479 }
1480 }"#;
1481 let store = TestStore::new_from_json_str(flag_json, segment_json);
1482 let flag = store.flag("flagA").unwrap();
1483
1484 let alice = ContextBuilder::new("alice").build().unwrap();
1485 let bob = ContextBuilder::new("bob").build().unwrap();
1486
1487 let mut evaluation_stack = EvaluationStack::default();
1488 let detail = evaluate_internal(&store, &flag, &alice, None, &mut evaluation_stack);
1489 assert_that!(detail.value).contains_value(&Bool(true));
1490 assert_that!(detail.reason).is_equal_to(&Reason::RuleMatch {
1491 rule_index: 0,
1492 rule_id: "rule-1".to_string(),
1493 in_experiment: false,
1494 });
1495 assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
1496 assert!(evaluation_stack.segment_chain.is_empty());
1497
1498 let mut evaluation_stack = EvaluationStack::default();
1499 let detail = evaluate_internal(&store, &flag, &bob, None, &mut evaluation_stack);
1500 assert_that!(detail.value).contains_value(&Bool(true));
1501 assert_that!(detail.reason).is_equal_to(&Reason::Fallthrough {
1502 in_experiment: false,
1503 });
1504 assert!(evaluation_stack.prerequisite_flag_chain.is_empty());
1505 assert!(evaluation_stack.segment_chain.is_empty());
1506 }
1507
1508 #[test]
1509 fn test_rollout_flag() {
1510 let store = TestStore::new();
1511 let flag = store.flag("flagWithRolloutBucketBy").unwrap();
1512
1513 let alice = ContextBuilder::new("anonymous")
1514 .set_value("platform", "aem".into())
1515 .set_value("ld_quid", "d4ad12cb-392b-4fce-b214-843ad625d6f8".into())
1516 .build()
1517 .unwrap();
1518
1519 let detail = evaluate(&store, &flag, &alice, None);
1520 assert_that!(detail.value).contains_value(&Str("rollout1".to_string()));
1521 }
1522
1523 #[test]
1524 fn test_experiment_flag() {
1525 let store = TestStore::new();
1526 let flag = store.flag("flagWithExperiment").unwrap();
1527
1528 let user_a = ContextBuilder::new("userKeyA").build().unwrap();
1529 let detail = evaluate(&store, &flag, &user_a, None);
1530 assert_that!(detail.value).contains_value(&Bool(false));
1531 assert!(detail.reason.is_in_experiment());
1532
1533 let user_b = ContextBuilder::new("userKeyB").build().unwrap();
1534 let detail = evaluate(&store, &flag, &user_b, None);
1535 assert_that!(detail.value).contains_value(&Bool(true));
1536 assert!(detail.reason.is_in_experiment());
1537
1538 let user_c = ContextBuilder::new("userKeyC").build().unwrap();
1539 let detail = evaluate(&store, &flag, &user_c, None);
1540 assert_that!(detail.value).contains_value(&Bool(false));
1541 assert!(!detail.reason.is_in_experiment());
1542 }
1543
1544 #[test]
1545 fn test_experiment_flag_targeting_missing_context() {
1546 let store = TestStore::new();
1547 let flag = store.flag("flagWithExperimentTargetingContext").unwrap();
1548
1549 let user_a = ContextBuilder::new("userKeyA").build().unwrap();
1550 let detail = evaluate(&store, &flag, &user_a, None);
1551 assert_that!(detail.value).contains_value(&Bool(false));
1552 assert_that!(detail.reason).is_equal_to(Reason::Fallthrough {
1553 in_experiment: false,
1554 })
1555 }
1556
1557 #[test]
1558 fn test_malformed_rule() {
1559 let store = TestStore::new();
1560 let mut flag = store.flag("flagWithMalformedRule").unwrap();
1561
1562 let user_a = ContextBuilder::new("no").build().unwrap();
1563 let user_b = ContextBuilder::new("yes").build().unwrap();
1564
1565 let detail = evaluate(&store, &flag, &user_a, None);
1566 assert_that!(detail.value).contains_value(&Bool(false));
1567 assert_that!(detail.reason).is_equal_to(Reason::Off);
1568
1569 let detail = evaluate(&store, &flag, &user_b, None);
1570 assert_that!(detail.value).contains_value(&Bool(false));
1571 assert_that!(detail.reason).is_equal_to(Reason::Off);
1572
1573 flag.on = true;
1574
1575 let detail = evaluate(&store, &flag, &user_a, None);
1576 assert_that!(detail.value).contains_value(&Bool(true));
1577 assert_that!(detail.reason).is_equal_to(Reason::Fallthrough {
1578 in_experiment: false,
1579 });
1580
1581 let detail = evaluate(&store, &flag, &user_b, None);
1582 assert_that!(detail.value).is_none();
1583 assert_that!(detail.reason).is_equal_to(Reason::Error {
1584 error: Error::MalformedFlag,
1585 });
1586 }
1587
1588 #[test]
1589 fn reason_serialization() {
1590 struct Case<'a> {
1591 reason: Reason,
1592 json: &'a str,
1593 }
1594
1595 let cases = vec![
1596 Case {
1597 reason: Reason::Off,
1598 json: r#"{"kind":"OFF"}"#,
1599 },
1600 Case {
1601 reason: Reason::Fallthrough {
1602 in_experiment: false,
1603 },
1604 json: r#"{"kind":"FALLTHROUGH"}"#,
1605 },
1606 Case {
1607 reason: Reason::Fallthrough {
1608 in_experiment: true,
1609 },
1610 json: r#"{"kind":"FALLTHROUGH","inExperiment":true}"#,
1611 },
1612 Case {
1613 reason: Reason::TargetMatch {},
1614 json: r#"{"kind":"TARGET_MATCH"}"#,
1615 },
1616 Case {
1617 reason: Reason::RuleMatch {
1618 rule_index: 1,
1619 rule_id: "x".into(),
1620 in_experiment: false,
1621 },
1622 json: r#"{"kind":"RULE_MATCH","ruleIndex":1,"ruleId":"x"}"#,
1623 },
1624 Case {
1625 reason: Reason::RuleMatch {
1626 rule_index: 1,
1627 rule_id: "x".into(),
1628 in_experiment: true,
1629 },
1630 json: r#"{"kind":"RULE_MATCH","ruleIndex":1,"ruleId":"x","inExperiment":true}"#,
1631 },
1632 Case {
1633 reason: Reason::PrerequisiteFailed {
1634 prerequisite_key: "x".into(),
1635 },
1636 json: r#"{"kind":"PREREQUISITE_FAILED","prerequisiteKey":"x"}"#,
1637 },
1638 Case {
1639 reason: Reason::Error {
1640 error: Error::WrongType,
1641 },
1642 json: r#"{"kind":"ERROR","errorKind":"WRONG_TYPE"}"#,
1643 },
1644 ];
1645
1646 for Case {
1647 reason,
1648 json: expected_json,
1649 } in cases
1650 {
1651 let json = serde_json::to_string(&reason).unwrap();
1652 assert_eq!(
1653 expected_json, json,
1654 "unexpected serialization: {:?}",
1655 reason
1656 );
1657 }
1658 }
1659
1660 #[test]
1661 fn get_applicable_context_by_kind_returns_correct_context() {
1662 let org_kind = Kind::from("org");
1663 let user_kind = Kind::from("user");
1664 let company_kind = Kind::from("company");
1665
1666 let user_context = ContextBuilder::new("user").build().unwrap();
1667 let org_context = ContextBuilder::new("org").kind("org").build().unwrap();
1668
1669 assert!(user_context.as_kind(&user_kind).is_some());
1670 assert!(user_context.as_kind(&org_kind).is_none());
1671 assert!(org_context.as_kind(&org_kind).is_some());
1672
1673 let multi_context = MultiContextBuilder::new()
1674 .add_context(user_context)
1675 .add_context(org_context)
1676 .build()
1677 .unwrap();
1678
1679 assert_eq!(
1680 &user_kind,
1681 multi_context.as_kind(&user_kind).unwrap().kind()
1682 );
1683 assert_eq!(&org_kind, multi_context.as_kind(&org_kind).unwrap().kind());
1684 assert!(multi_context.as_kind(&company_kind).is_none());
1685 }
1686
1687 #[test]
1688 fn can_create_error_detail() {
1689 let detail = Detail::err_default(Error::MalformedFlag, true.into());
1690
1691 assert_eq!(Some(AttributeValue::Bool(true)), detail.value);
1692 assert!(detail.variation_index.is_none());
1693 assert_that!(detail.reason).is_equal_to(Reason::Error {
1694 error: Error::MalformedFlag,
1695 });
1696 }
1697
1698 #[test]
1699 fn can_force_error_if_value_is_none() {
1700 let detail: Detail<AttributeValue> = Detail {
1701 value: None,
1702 variation_index: None,
1703 reason: Reason::Off,
1704 };
1705
1706 let detail = detail.should_have_value(Error::MalformedFlag);
1707
1708 assert!(detail.value.is_none());
1709 assert!(detail.variation_index.is_none());
1710 assert_that!(detail.reason).is_equal_to(Reason::Error {
1711 error: Error::MalformedFlag,
1712 });
1713 }
1714
1715 #[test]
1716 fn can_map_detail_with_default_and_error() {
1717 let detail: Detail<AttributeValue> = Detail {
1718 value: None,
1719 variation_index: None,
1720 reason: Reason::Off,
1721 };
1722
1723 let mapped = detail.try_map(Some, false.into(), Error::MalformedFlag);
1724 assert_eq!(Some(AttributeValue::Bool(false)), mapped.value);
1725 assert!(mapped.variation_index.is_none());
1726 assert_that!(mapped.reason).is_equal_to(Reason::Off);
1727
1728 let detail: Detail<AttributeValue> = Detail {
1729 value: Some(true.into()),
1730 variation_index: Some(1),
1731 reason: Reason::Off,
1732 };
1733
1734 let mapped = detail.try_map(|_| Some(false.into()), false.into(), Error::MalformedFlag);
1735 assert_eq!(Some(AttributeValue::Bool(false)), mapped.value);
1736 assert_eq!(Some(1), mapped.variation_index);
1737 assert_that!(mapped.reason).is_equal_to(Reason::Off);
1738
1739 let detail: Detail<AttributeValue> = Detail {
1740 value: Some(true.into()),
1741 variation_index: Some(1),
1742 reason: Reason::Off,
1743 };
1744
1745 let mapped = detail.try_map(|_| None, false.into(), Error::MalformedFlag);
1746 assert_eq!(Some(AttributeValue::Bool(false)), mapped.value);
1747 assert!(mapped.variation_index.is_none());
1748 assert_that!(mapped.reason).is_equal_to(Reason::Error {
1749 error: Error::MalformedFlag,
1750 });
1751 }
1752
1753 #[test]
1754 fn can_set_value_to_default_if_does_not_exist() {
1755 let detail: Detail<AttributeValue> = Detail {
1756 value: Some(true.into()),
1757 variation_index: Some(1),
1758 reason: Reason::Off,
1759 };
1760
1761 let or_detail = detail.or(false.into());
1762 assert_eq!(Some(AttributeValue::Bool(true)), or_detail.value);
1763 assert_eq!(Some(1), or_detail.variation_index);
1764 assert_that!(or_detail.reason).is_equal_to(Reason::Off);
1765
1766 let detail: Detail<AttributeValue> = Detail {
1767 value: None,
1768 variation_index: Some(1),
1769 reason: Reason::Off,
1770 };
1771
1772 let or_detail = detail.or(false.into());
1773 assert_eq!(Some(AttributeValue::Bool(false)), or_detail.value);
1774 assert!(or_detail.variation_index.is_none());
1775 assert_that!(or_detail.reason).is_equal_to(Reason::Off);
1776 }
1777
1778 #[test]
1779 fn can_set_value_to_default_if_does_not_exist_through_callback() {
1780 let detail: Detail<AttributeValue> = Detail {
1781 value: Some(true.into()),
1782 variation_index: Some(1),
1783 reason: Reason::Off,
1784 };
1785
1786 let or_detail = detail.or_else(|| false.into());
1787 assert_eq!(Some(AttributeValue::Bool(true)), or_detail.value);
1788 assert_eq!(Some(1), or_detail.variation_index);
1789 assert_that!(or_detail.reason).is_equal_to(Reason::Off);
1790
1791 let detail: Detail<AttributeValue> = Detail {
1792 value: None,
1793 variation_index: Some(1),
1794 reason: Reason::Off,
1795 };
1796
1797 let or_detail = detail.or_else(|| false.into());
1798 assert_eq!(Some(AttributeValue::Bool(false)), or_detail.value);
1799 assert!(or_detail.variation_index.is_none());
1800 assert_that!(or_detail.reason).is_equal_to(Reason::Off);
1801 }
1802}