launchdarkly_server_sdk_evaluation/
eval.rs

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
11/// A struct representing the results of an evaluation on a prerequisite flag.
12pub struct PrerequisiteEvent {
13    /// String representing the [crate::Flag::key] of the original flag being evaluated.
14    pub target_flag_key: String,
15    /// The [crate::Context] provided during the evaluation process.
16    pub context: Context,
17    /// The prerequisite [crate::Flag] that was evaluated.
18    pub prerequisite_flag: Flag,
19    /// The result of calling [evaluate] on the [PrerequisiteEvent::prerequisite_flag].
20    pub prerequisite_result: Detail<FlagValue>,
21}
22
23/// Trait used by [evaluate] to record the result of prerequisite flag evaluations.
24pub trait PrerequisiteEventRecorder {
25    /// Record the results of a prerequisite flag evaluation.
26    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        // Preallocate some space for prerequisite_flag_chain and segment_chain on the stack. We
40        // can get up to that many levels of nested prerequisites or nested segments before
41        // appending to the slice will cause a heap allocation.
42        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
55/// Evaluate a feature flag for the specified [Context].
56///
57/// The evaluator does not know anything about analytics events; generating any appropriate
58/// analytics events is the responsibility of the caller. The caller can provide an optional
59/// [PrerequisiteEventRecorder] which will be notified if any additional evaluations were done due
60/// to prerequisites.
61pub 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/// A Detail instance is returned from [evaluate], combining the result of a flag evaluation with
233/// an explanation of how it was calculated.
234#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
235#[serde(rename_all = "camelCase")]
236pub struct Detail<T> {
237    /// The result of the flag evaluation. This will be either one of the flag's variations or None
238    /// if no appropriate fallback value was configured.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub value: Option<T>,
241
242    /// The index of the returned value within the flag's list of variations, e.g. 0 for the first
243    /// variation. This is an Option because it is possible for the value to be undefined (there is
244    /// no variation index if the application default value was returned due to an error in
245    /// evaluation) which is different from a value of 0.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub variation_index: Option<VariationIndex>,
248
249    /// A reason struct describing the main factor that influenced the flag evaluation value.
250    pub reason: Reason,
251}
252
253impl<T> Detail<T> {
254    /// Returns a detail with value and variation_index of None.
255    ///
256    /// If a flag does not have an appropriate fallback value, the [Detail::value] and
257    /// [Detail::variation_index] must be None. In each case, the [Detail::reason] will be set to
258    /// the reason provided to this method.
259    pub fn empty(reason: Reason) -> Detail<T> {
260        Detail {
261            value: None,
262            variation_index: None,
263            reason,
264        }
265    }
266
267    /// Returns a detail response using the provided default as the value and a variation_index
268    /// of None.
269    ///
270    /// If the SDK variation methods detect some error condition, it will fallback to the user-provided
271    /// default value. The provided error will be included as part of the [Detail::reason], and the
272    /// [Detail::variation_index] will be set to None.
273    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    /// Returns a detail response using the provided error as the [Detail::reason].
282    pub fn err(error: Error) -> Detail<T> {
283        Detail::empty(Reason::Error { error })
284    }
285
286    /// Returns a new instance of this detail with the provided function `f` applied to
287    /// [Detail::value].
288    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    /// Sets the [Detail::reason] to the provided error if the current detail instance does not
300    /// have a value set.
301    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    /// Returns a new instance of detail with the provided function `f` applied to
309    /// [Detail::value] if it exists.
310    ///
311    /// [Detail::value] may or may not be set. If it is not set, this method will return a new
312    /// [Detail] instance with the [Detail::reason] set to the provided [Error] `e`.
313    ///
314    /// If it is set, this method will apply the provided function `f` to the value. If the method
315    /// `f` returns None, this method will return an error [Detail]. See [Detail::err]. Otherwise,
316    /// a [Detail] instance will be returned with the result of the `f` application.
317    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    /// Set the [Detail::value] to `default` if it does not exist.
339    ///
340    /// The SDK always wants to return an evaluation result. This method helps ensure that if a
341    /// [Detail::value] is None, we can update it with the provided default.
342    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            // N.B. reason remains untouched: this is counterintuitive, but consistent with Go
347        }
348        self
349    }
350
351    /// Set the [Detail::value] to `default` if it does not exist.
352    ///
353    /// This method accomplishes the same thing as [Detail::or] but allows the default value to be
354    /// provided through the result of a callback. This helps reduce computation where an
355    /// evaluation default value might be costly to calculate and is likely infrequently used.
356    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/// Reason describes the reason that a flag evaluation produced a particular value.
369#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
370#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "kind")]
371pub enum Reason {
372    /// Off indicates that the flag was off and therefore returned its configured off value.
373    Off,
374    /// TargetMatch indicates that context key was specifically targeted for this flag.
375    TargetMatch,
376    /// RuleMatch indicates that the context matched one of the flag's rules.
377    #[serde(rename_all = "camelCase")]
378    RuleMatch {
379        /// Zero-based index of the [crate::FlagRule] that was matched.
380        rule_index: usize,
381        #[serde(skip_serializing_if = "String::is_empty")]
382        /// The id property of the [crate::FlagRule::id] that was matched.
383        rule_id: String,
384        /// This optional boolean property is true if the variation was determined by a [crate::Rollout]
385        /// whose kind was [crate::RolloutKind::Experiment] and if the selected [crate::WeightedVariation] did not have an
386        /// untracked property of true. It is false otherwise.
387        #[serde(skip_serializing_if = "std::ops::Not::not")]
388        in_experiment: bool,
389    },
390    /// PrerequisiteFailed indicates that the flag was considered off because it had at
391    /// least one prerequisite flag that either was off or did not return the desired variation.
392    #[serde(rename_all = "camelCase")]
393    PrerequisiteFailed {
394        /// The key of the prerequisite flag that failed.
395        prerequisite_key: String,
396    },
397    /// Fallthrough indicates that the flag was on but the context did not match any targets
398    /// or rules.
399    #[serde(rename_all = "camelCase")]
400    Fallthrough {
401        /// This optional boolean property is true if the variation was determined by a [crate::Rollout]
402        /// whose kind was [crate::RolloutKind::Experiment] and if the selected [crate::WeightedVariation] did not have an
403        /// untracked property of true. It is false otherwise.
404        #[serde(skip_serializing_if = "std::ops::Not::not")]
405        in_experiment: bool,
406    },
407    /// Error indicates that the flag could not be evaluated, e.g. because it does not
408    /// exist or due to an unexpected error. In this case the result value will be the default value
409    /// that the caller passed to the client.
410    Error {
411        /// An error representing the [Reason::Error].
412        #[serde(rename = "errorKind")]
413        error: Error,
414    },
415}
416
417impl Reason {
418    /// This method determines whether or not the provided [Reason] is considered to be part of an
419    /// ongoing experiment.
420    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/// Error is returned via a [Reason::Error] when the client could not evaluate a flag, and
430/// provides information about why the flag could not be evaluated.
431#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
432#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
433pub enum Error {
434    /// ClientNotReady indicates that the caller tried to evaluate a flag before the client
435    /// had successfully initialized.
436    ClientNotReady,
437    /// FlagNotFound indicates that the caller provided a flag key that did not match any
438    /// known flag.
439    FlagNotFound,
440    /// MalformedFlag indicates that there was an internal inconsistency in the flag data,
441    /// e.g. a rule specified a nonexistent variation.
442    MalformedFlag,
443    /// WrongType indicates that the result value was not of the requested type, e.g. you
444    /// called BoolVariationDetail but the value was an integer.
445    WrongType,
446    /// Exception indicates that an unexpected error stopped flag evaluation; check the
447    /// log for details.
448    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(); // not targeted
467        let bob = ContextBuilder::new("bob").build().unwrap(); // targeted
468        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        // flip off variation
479        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        // off variation unspecified
485        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        // flip targeting on
492        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        // flip default variation
506        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        // bob's reason should still be TargetMatch even though his value is now the default
512        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(); // not targeted
522        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(); // targeted
528        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    // Note that the following two cases should not be seen in real data, since
548    // rules that are meant to have special kind matching logic will omit contextKind altogether.
549    #[test_case("flagWithMatchesOpOnKindsAttributeReference")]
550    #[test_case("flagWithMatchesOpOnKindsPlainAttributeReference")]
551    // This test checks the special behavior of specifying an attribute reference that
552    // resolves to 'kind' in a clause. In this case, the clause is meant to apply to the
553    // context's kind(s), for example detecting if a (multi) context contains a certain kind.
554    fn test_eval_with_matches_op_kinds(flag_key: &str) {
555        let store = TestStore::new();
556
557        let alice = ContextBuilder::new("alice").build().unwrap(); // targeted
558
559        let mut bob_builder = ContextBuilder::new("bob");
560        let bob = bob_builder.kind("company").build().unwrap(); // not targeted
561
562        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(); // re-targeted
582        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        // flip targeting on
635        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        // prerequisite off
707        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    // Flag A
897    //   Flag B
898    //     Flag A
899    #[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    // Flag A
944    //   Flag B
945    //   Flag C
946    //     Flag B
947    #[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    // Flag A
975    //   Flag B
976    //     Flag C
977    // Flag C
978    //   Flag A
979    #[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    // Flag A Segment A
1038    // Flag B Segment A
1039    #[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    // Flag A
1126    //   Flag B Segment A
1127    //   Flag C Segment A
1128    #[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    // Flag A Segment A
1230    // Segment A
1231    //  Segment B
1232    //   Segment A
1233    #[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    // Flag A Segment A
1312    // Segment A
1313    //  Segment A
1314    #[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    // Flag A Segment A
1375    //   Flag B Segment B
1376    // Segment A
1377    //   Segment B
1378    //     Segment C
1379    #[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}