1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
use std::collections::HashMap;

use crate::EvaluationContextFieldValue;

/// The evaluation context provides ambient information for the purposes of flag evaluation.
/// Contextual data may be used as the basis for targeting, including rule-based evaluation,
/// overrides for specific subjects, or fractional flag evaluation.
///
/// The context might contain information about the end-user, the application, the host, or any
/// other ambient data that might be useful in flag evaluation. For example, a flag system might
/// define rules that return a specific value based on the user's email address, locale, or the
/// time of day. The context provides this information. The context can be optionally provided at
/// evaluation, and mutated in before hooks.
#[derive(Clone, Default, PartialEq, Debug)]
pub struct EvaluationContext {
    /// The targeting key uniquely identifies the subject (end-user, or client service) of a flag
    /// evaluation. Providers may require this field for fractional flag evaluation, rules, or
    /// overrides targeting specific users. Such providers may behave unpredictably if a targeting
    /// key is not specified at flag resolution.
    pub targeting_key: Option<String>,

    /// The evaluation context MUST support the inclusion of custom fields, having keys of type
    /// string, and values of type boolean | string | number | datetime | structure.
    pub custom_fields: HashMap<String, EvaluationContextFieldValue>,
}

impl EvaluationContext {
    /// Set the `targeting_key` of the evaluation context.
    #[must_use]
    pub fn with_targeting_key(mut self, targeting_key: impl Into<String>) -> Self {
        self.targeting_key = Some(targeting_key.into());
        self
    }

    /// Add `key` and `value` to the custom field of evaluation context.
    #[must_use]
    pub fn with_custom_field(
        mut self,
        key: impl Into<String>,
        value: impl Into<EvaluationContextFieldValue>,
    ) -> Self {
        self.add_custom_field(key, value);
        self
    }

    /// Add `key` and `value` to the custom field of evaluation context.
    pub fn add_custom_field(
        &mut self,
        key: impl Into<String>,
        value: impl Into<EvaluationContextFieldValue>,
    ) {
        self.custom_fields.insert(key.into(), value.into());
    }

    /// Merge `other` into `self` if corresponding field is not set.
    /// Meaning values set into `self` has higher precedence.
    pub fn merge_missing(&mut self, other: &Self) {
        if self.targeting_key.is_none() {
            if let Some(targeting_key) = &other.targeting_key {
                self.targeting_key = Some(targeting_key.clone());
            }
        }

        other.custom_fields.iter().for_each(|(key, value)| {
            if !self.custom_fields.contains_key(key) {
                self.custom_fields.insert(key.clone(), value.clone());
            }
        });
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use spec::spec;
    use time::OffsetDateTime;

    use super::*;

    #[test]
    fn merge_missig_given_empty() {
        let mut context = EvaluationContext::default()
            .with_targeting_key("Targeting Key")
            .with_custom_field("Some", "Value");

        let expected = context.clone();

        context.merge_missing(&EvaluationContext::default());

        assert_eq!(context, expected);
    }

    #[test]
    fn merge_missing_given_targeting_key() {
        let mut context = EvaluationContext::default()
            .with_targeting_key("Targeting Key")
            .to_owned();

        let expected = context.clone();

        context.merge_missing(&EvaluationContext::default().with_targeting_key("Another Key"));

        assert_eq!(context, expected);
    }

    #[test]
    fn merge_missing_given_custom_fields() {
        let mut context = EvaluationContext::default()
            .with_targeting_key("Targeting Key")
            .with_custom_field("Key", "Value");

        context.merge_missing(
            &EvaluationContext::default()
                .with_custom_field("Key", "Another Value")
                .with_custom_field("Another Key", "Value"),
        );

        assert_eq!(
            context,
            EvaluationContext::default()
                .with_targeting_key("Targeting Key")
                .with_custom_field("Key", "Value")
                .with_custom_field("Another Key", "Value")
        )
    }

    #[test]
    fn merge_missing_given_full() {
        let mut context = EvaluationContext::default();

        let other = EvaluationContext::default()
            .with_targeting_key("Targeting Key")
            .with_custom_field("Key", "Value");

        context.merge_missing(&other);

        assert_eq!(context, other);
    }

    #[derive(Clone, PartialEq, Eq, Debug)]
    pub struct DummyStruct {
        pub id: i64,
        pub name: String,
    }

    #[spec(
        number = "3.1.1",
        text = "The evaluation context structure MUST define an optional targeting key field of type string, identifying the subject of the flag evaluation."
    )]
    #[spec(
        number = "3.1.2",
        text = "The evaluation context MUST support the inclusion of custom fields, having keys of type string, and values of type boolean | string | number | datetime | structure."
    )]
    #[spec(
        number = "3.1.3",
        text = "The evaluation context MUST support fetching the custom fields by key and also fetching all key value pairs."
    )]
    #[spec(
        number = "3.1.4",
        text = "The evaluation context fields MUST have an unique key."
    )]
    #[test]
    fn fields_access() {
        let now_time = OffsetDateTime::now_utc();
        let struct_value = DummyStruct {
            id: 200,
            name: "Bob".to_string(),
        };

        let context = EvaluationContext::default()
            .with_targeting_key("Key")
            .with_custom_field("Bool", true)
            .with_custom_field("Int", 100)
            .with_custom_field("Float", 3.14)
            .with_custom_field("String", "Hello")
            .with_custom_field("Datetime", now_time)
            .with_custom_field(
                "Struct",
                EvaluationContextFieldValue::Struct(Arc::new(struct_value.clone())),
            );

        assert_eq!(context.targeting_key, Some("Key".to_string()));
        assert_eq!(
            context.custom_fields.get("Int"),
            Some(&EvaluationContextFieldValue::Int(100))
        );
        assert_eq!(
            context.custom_fields.get("Float"),
            Some(&EvaluationContextFieldValue::Float(3.14))
        );
        assert_eq!(
            context.custom_fields.get("String"),
            Some(&EvaluationContextFieldValue::String("Hello".to_string()))
        );
        assert_eq!(
            context.custom_fields.get("Datetime"),
            Some(&EvaluationContextFieldValue::DateTime(now_time))
        );
        assert_eq!(
            *context
                .custom_fields
                .get("Struct")
                .unwrap()
                .as_struct()
                .unwrap()
                .downcast::<DummyStruct>()
                .unwrap(),
            struct_value
        );
    }
}