Skip to main content

statsig_rust/evaluation/
evaluation_types.rs

1use std::collections::HashMap;
2
3use super::dynamic_returnable::DynamicReturnable;
4use crate::{
5    evaluation::secondary_exposure_key::SecondaryExposureKey,
6    gcir::gcir_formatter::GCIRHashable,
7    hashing::{self, opt_bool_to_hashable},
8    interned_string::InternedString,
9    specs_response::explicit_params::ExplicitParameters,
10};
11
12use serde::{Deserialize, Serialize};
13
14pub fn is_false(v: &bool) -> bool {
15    !(*v)
16}
17
18#[derive(Serialize, Deserialize, Clone, Debug)]
19#[serde(rename_all = "camelCase")]
20pub struct SecondaryExposure {
21    pub gate: InternedString,
22    pub gate_value: InternedString,
23    #[serde(rename = "ruleID")]
24    pub rule_id: InternedString,
25}
26
27impl GCIRHashable for SecondaryExposure {
28    fn create_hash(&self, name: &InternedString) -> u64 {
29        hashing::hash_u64_slice(&[name.hash, self.gate_value.hash, self.rule_id.hash])
30    }
31}
32
33impl SecondaryExposure {
34    pub fn get_dedupe_key(&self) -> String {
35        let mut key = String::new();
36        key += &self.gate;
37        key += "|";
38        key += &self.gate_value;
39        key += "|";
40        key += self.rule_id.as_str();
41        key
42    }
43}
44
45impl From<&SecondaryExposure> for SecondaryExposureKey {
46    fn from(val: &SecondaryExposure) -> Self {
47        SecondaryExposureKey {
48            gate_name_hash: val.gate.hash,
49            rule_id_hash: val.rule_id.hash,
50            gate_value_hash: val.gate_value.hash,
51        }
52    }
53}
54
55#[derive(Serialize, Deserialize, Clone, Debug, Default)]
56pub struct ExtraExposureInfo {
57    pub sampling_rate: Option<u64>,
58    pub forward_all_exposures: Option<bool>,
59    pub has_seen_analytical_gates: Option<bool>,
60    pub override_config_name: Option<InternedString>,
61    pub version: Option<u32>,
62}
63
64#[derive(Serialize, Deserialize, Clone)]
65pub struct BaseEvaluation {
66    pub name: InternedString,
67    pub rule_id: InternedString,
68    pub secondary_exposures: Vec<SecondaryExposure>,
69    pub version: Option<u32>,
70
71    #[serde(skip_serializing)]
72    pub(crate) exposure_info: Option<ExtraExposureInfo>,
73}
74
75pub enum AnyEvaluation<'a> {
76    FeatureGate(&'a GateEvaluation),
77    DynamicConfig(&'a DynamicConfigEvaluation),
78    Experiment(&'a ExperimentEvaluation),
79    Layer(&'a LayerEvaluation),
80}
81
82impl AnyEvaluation<'_> {
83    pub fn get_base_result(&self) -> &BaseEvaluation {
84        match self {
85            AnyEvaluation::FeatureGate(gate) => &gate.base,
86            AnyEvaluation::DynamicConfig(config) => &config.base,
87            AnyEvaluation::Experiment(experiment) => &experiment.base,
88            AnyEvaluation::Layer(layer) => &layer.base,
89        }
90    }
91
92    pub fn get_gate_bool_value(&self) -> bool {
93        match self {
94            AnyEvaluation::FeatureGate(eval) => eval.value,
95            _ => false, // return false for all other types
96        }
97    }
98}
99
100impl<'a> From<&'a LayerEvaluation> for AnyEvaluation<'a> {
101    fn from(layer_eval: &'a LayerEvaluation) -> Self {
102        AnyEvaluation::Layer(layer_eval)
103    }
104}
105
106impl<'a> From<&'a GateEvaluation> for AnyEvaluation<'a> {
107    fn from(gate_eval: &'a GateEvaluation) -> Self {
108        AnyEvaluation::FeatureGate(gate_eval)
109    }
110}
111
112impl<'a> From<&'a ExperimentEvaluation> for AnyEvaluation<'a> {
113    fn from(experiment_eval: &'a ExperimentEvaluation) -> Self {
114        AnyEvaluation::Experiment(experiment_eval)
115    }
116}
117
118impl<'a> From<&'a DynamicConfigEvaluation> for AnyEvaluation<'a> {
119    fn from(dynamic_config_evalation: &'a DynamicConfigEvaluation) -> Self {
120        AnyEvaluation::DynamicConfig(dynamic_config_evalation)
121    }
122}
123
124#[derive(Serialize, Deserialize, Clone)]
125pub struct GateEvaluation {
126    #[serde(flatten)]
127    pub base: BaseEvaluation,
128
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub id_type: Option<InternedString>,
131    pub value: bool,
132}
133
134impl GCIRHashable for GateEvaluation {
135    fn create_hash(&self, name: &InternedString) -> u64 {
136        let version_hashes = optional_version_hash_values(&self.base.version);
137        let hash_array = [
138            name.hash,
139            self.value as u64,
140            self.base.rule_id.hash,
141            hash_secondary_exposures(&self.base.secondary_exposures),
142            version_hashes[0],
143            version_hashes[1],
144        ];
145        hashing::hash_u64_slice(&hash_array)
146    }
147}
148
149#[derive(Serialize, Deserialize, Clone)]
150pub struct DynamicConfigEvaluation {
151    #[serde(flatten)]
152    pub base: BaseEvaluation,
153
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub id_type: Option<InternedString>,
156    pub value: DynamicReturnable,
157
158    pub is_device_based: bool,
159
160    pub passed: bool,
161}
162
163impl GCIRHashable for DynamicConfigEvaluation {
164    fn create_hash(&self, name: &InternedString) -> u64 {
165        let version_hashes = optional_version_hash_values(&self.base.version);
166        let hash_array = [
167            name.hash,
168            self.value.get_stable_hash(),
169            self.base.rule_id.hash,
170            hash_secondary_exposures(&self.base.secondary_exposures),
171            self.passed as u64,
172            version_hashes[0],
173            version_hashes[1],
174        ];
175        hashing::hash_u64_slice(&hash_array)
176    }
177}
178
179#[derive(Serialize, Deserialize, Clone)]
180pub struct ExperimentEvaluation {
181    #[serde(flatten)]
182    pub base: BaseEvaluation,
183
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub id_type: Option<InternedString>,
186    pub value: DynamicReturnable,
187
188    pub is_device_based: bool,
189
190    #[serde(skip_serializing_if = "is_false")]
191    pub is_in_layer: bool,
192
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub explicit_parameters: Option<ExplicitParameters>,
195
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub group_name: Option<InternedString>,
198
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub is_experiment_active: Option<bool>,
201
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub is_user_in_experiment: Option<bool>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
206}
207
208impl GCIRHashable for ExperimentEvaluation {
209    fn create_hash(&self, name: &InternedString) -> u64 {
210        let version_hashes = optional_version_hash_values(&self.base.version);
211        let hash_array = [
212            name.hash,
213            self.value.get_stable_hash(),
214            self.base.rule_id.hash,
215            hash_secondary_exposures(&self.base.secondary_exposures),
216            self.is_in_layer as u64,
217            version_hashes[0],
218            version_hashes[1],
219            hash_explicit_parameters(self.explicit_parameters.as_ref()),
220            self.group_name.as_ref().map_or(0, |g| g.hash),
221            opt_bool_to_hashable(&self.is_experiment_active),
222            opt_bool_to_hashable(&self.is_user_in_experiment),
223        ];
224
225        hashing::hash_u64_slice(&hash_array)
226    }
227}
228
229#[derive(Serialize, Deserialize, Clone)]
230#[serde(untagged)]
231pub enum AnyConfigEvaluation {
232    DynamicConfig(DynamicConfigEvaluation),
233    Experiment(ExperimentEvaluation),
234}
235
236impl GCIRHashable for AnyConfigEvaluation {
237    fn create_hash(&self, name: &InternedString) -> u64 {
238        match self {
239            AnyConfigEvaluation::DynamicConfig(eval) => eval.create_hash(name),
240            AnyConfigEvaluation::Experiment(eval) => eval.create_hash(name),
241        }
242    }
243}
244
245#[derive(Serialize, Deserialize, Clone)]
246pub struct LayerEvaluation {
247    #[serde(flatten)]
248    pub base: BaseEvaluation,
249
250    pub value: DynamicReturnable,
251
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub id_type: Option<InternedString>,
254
255    pub is_device_based: bool,
256
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub group_name: Option<InternedString>,
259
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub is_experiment_active: Option<bool>,
262
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub is_user_in_experiment: Option<bool>,
265
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub allocated_experiment_name: Option<InternedString>,
268    pub explicit_parameters: ExplicitParameters,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
271
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub parameter_rule_ids: Option<HashMap<InternedString, InternedString>>,
274}
275
276impl GCIRHashable for LayerEvaluation {
277    fn create_hash(&self, name: &InternedString) -> u64 {
278        let version_hashes = optional_version_hash_values(&self.base.version);
279        let mut hash_array = hashing::U64HashBuilder::new();
280        hash_array.push(name.hash);
281        hash_array.push(self.value.get_stable_hash());
282        hash_array.push(self.base.rule_id.hash);
283        hash_array.push(hash_secondary_exposures(&self.base.secondary_exposures));
284        hash_array.push(self.group_name.as_ref().map_or(0, |g| g.hash));
285        hash_array.push(opt_bool_to_hashable(&self.is_experiment_active));
286        hash_array.push(opt_bool_to_hashable(&self.is_user_in_experiment));
287        hash_array.push(
288            self.allocated_experiment_name
289                .as_ref()
290                .map_or(0, |n| n.hash),
291        );
292        hash_array.push(version_hashes[0]);
293        hash_array.push(version_hashes[1]);
294        hash_array.push(hash_explicit_parameters(Some(&self.explicit_parameters)));
295        hash_array.push(hash_optional_secondary_exposures(
296            self.undelegated_secondary_exposures.as_ref(),
297        ));
298        if let Some(parameter_rule_ids) = &self.parameter_rule_ids {
299            hash_array.push(hash_parameter_rule_ids(parameter_rule_ids));
300        }
301
302        hash_array.finish()
303    }
304}
305
306fn hash_secondary_exposures(exposures: &Vec<SecondaryExposure>) -> u64 {
307    let mut secondary_exposure_hashes = hashing::U64HashBuilder::with_capacity(exposures.len());
308    for exposure in exposures {
309        secondary_exposure_hashes.push(exposure.create_hash(&exposure.gate));
310    }
311    secondary_exposure_hashes.finish()
312}
313
314fn hash_optional_secondary_exposures(exposures: Option<&Vec<SecondaryExposure>>) -> u64 {
315    let Some(exposures) = exposures else {
316        return hashing::hash_u64_slice(&[]);
317    };
318
319    hash_secondary_exposures(exposures)
320}
321
322fn hash_explicit_parameters(explicit_parameters: Option<&ExplicitParameters>) -> u64 {
323    let Some(explicit_parameters) = explicit_parameters else {
324        return hashing::hash_u64_slice(&[]);
325    };
326
327    let mut explicit_params_hashes =
328        hashing::U64HashBuilder::with_capacity(explicit_parameters.as_slice().len());
329    for value in explicit_parameters.as_slice() {
330        explicit_params_hashes.push(value.hash);
331    }
332    explicit_params_hashes.finish()
333}
334
335fn hash_parameter_rule_ids(parameter_rule_ids: &HashMap<InternedString, InternedString>) -> u64 {
336    hashing::hash_unordered(
337        parameter_rule_ids
338            .iter()
339            .map(|(param_name, rule_id)| hashing::hash_u64_slice(&[param_name.hash, rule_id.hash]))
340            .collect(),
341    )
342}
343
344fn optional_version_hash_values(version: &Option<u32>) -> [u64; 2] {
345    [version.is_some() as u64, version.map_or(0, u64::from)]
346}
347
348#[cfg(test)]
349mod tests {
350    use std::collections::HashMap;
351
352    use super::{hash_parameter_rule_ids, optional_version_hash_values};
353    use crate::interned_string::InternedString;
354
355    #[test]
356    fn optional_version_hash_values_distinguish_missing_from_zero() {
357        assert_eq!(optional_version_hash_values(&None), [0, 0]);
358        assert_eq!(optional_version_hash_values(&Some(0)), [1, 0]);
359        assert_eq!(optional_version_hash_values(&Some(1)), [1, 1]);
360    }
361
362    #[test]
363    fn parameter_rule_id_hash_ignores_map_order() {
364        let first = HashMap::from([
365            (
366                InternedString::from_str_ref("alpha"),
367                InternedString::from_str_ref("rule-a"),
368            ),
369            (
370                InternedString::from_str_ref("beta"),
371                InternedString::from_str_ref("rule-b"),
372            ),
373        ]);
374        let second = HashMap::from([
375            (
376                InternedString::from_str_ref("beta"),
377                InternedString::from_str_ref("rule-b"),
378            ),
379            (
380                InternedString::from_str_ref("alpha"),
381                InternedString::from_str_ref("rule-a"),
382            ),
383        ]);
384
385        assert_eq!(
386            hash_parameter_rule_ids(&first),
387            hash_parameter_rule_ids(&second)
388        );
389    }
390}