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        let hash_array = vec![name.hash, self.gate_value.hash, self.rule_id.hash];
30        hashing::hash_one(hash_array)
31    }
32}
33
34impl SecondaryExposure {
35    pub fn get_dedupe_key(&self) -> String {
36        let mut key = String::new();
37        key += &self.gate;
38        key += "|";
39        key += &self.gate_value;
40        key += "|";
41        key += self.rule_id.as_str();
42        key
43    }
44}
45
46impl From<&SecondaryExposure> for SecondaryExposureKey {
47    fn from(val: &SecondaryExposure) -> Self {
48        SecondaryExposureKey {
49            gate_name_hash: val.gate.hash,
50            rule_id_hash: val.rule_id.hash,
51            gate_value_hash: val.gate_value.hash,
52        }
53    }
54}
55
56#[derive(Serialize, Deserialize, Clone, Debug, Default)]
57pub struct ExtraExposureInfo {
58    pub sampling_rate: Option<u64>,
59    pub forward_all_exposures: Option<bool>,
60    pub has_seen_analytical_gates: Option<bool>,
61    pub override_config_name: Option<InternedString>,
62    pub version: Option<u32>,
63}
64
65#[derive(Serialize, Deserialize, Clone)]
66pub struct BaseEvaluation {
67    pub name: InternedString,
68    pub rule_id: InternedString,
69    pub secondary_exposures: Vec<SecondaryExposure>,
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 hash_array = vec![
137            name.hash,
138            self.value as u64,
139            self.base.rule_id.hash,
140            hash_secondary_exposures(&self.base.secondary_exposures),
141        ];
142        hashing::hash_one(hash_array)
143    }
144}
145
146#[derive(Serialize, Deserialize, Clone)]
147pub struct DynamicConfigEvaluation {
148    #[serde(flatten)]
149    pub base: BaseEvaluation,
150
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub id_type: Option<InternedString>,
153    pub value: DynamicReturnable,
154
155    pub is_device_based: bool,
156
157    pub passed: bool,
158}
159
160impl GCIRHashable for DynamicConfigEvaluation {
161    fn create_hash(&self, name: &InternedString) -> u64 {
162        let hash_array = vec![
163            name.hash,
164            self.value.get_hash(),
165            self.base.rule_id.hash,
166            hash_secondary_exposures(&self.base.secondary_exposures),
167            self.passed as u64,
168        ];
169        hashing::hash_one(hash_array)
170    }
171}
172
173#[derive(Serialize, Deserialize, Clone)]
174pub struct ExperimentEvaluation {
175    #[serde(flatten)]
176    pub base: BaseEvaluation,
177
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub id_type: Option<InternedString>,
180    pub value: DynamicReturnable,
181
182    pub is_device_based: bool,
183
184    #[serde(skip_serializing_if = "is_false")]
185    pub is_in_layer: bool,
186
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub explicit_parameters: Option<ExplicitParameters>,
189
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub group_name: Option<InternedString>,
192
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub is_experiment_active: Option<bool>,
195
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub is_user_in_experiment: Option<bool>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
200}
201
202impl GCIRHashable for ExperimentEvaluation {
203    fn create_hash(&self, name: &InternedString) -> u64 {
204        let mut hash_array = vec![
205            name.hash,
206            self.value.get_hash(),
207            self.base.rule_id.hash,
208            hash_secondary_exposures(&self.base.secondary_exposures),
209            self.is_in_layer as u64,
210        ];
211        let mut explicit_params_hashes = Vec::new();
212        if let Some(explicit_parameters) = &self.explicit_parameters {
213            for value in explicit_parameters.to_vec_interned() {
214                explicit_params_hashes.push(value.hash);
215            }
216        }
217        hash_array.push(hashing::hash_one(explicit_params_hashes));
218        hash_array.push(self.group_name.as_ref().map_or(0, |g| g.hash));
219        hash_array.push(opt_bool_to_hashable(&self.is_experiment_active));
220        hash_array.push(opt_bool_to_hashable(&self.is_user_in_experiment));
221
222        hashing::hash_one(hash_array)
223    }
224}
225
226#[derive(Serialize, Deserialize, Clone)]
227#[serde(untagged)]
228pub enum AnyConfigEvaluation {
229    DynamicConfig(DynamicConfigEvaluation),
230    Experiment(ExperimentEvaluation),
231}
232
233impl GCIRHashable for AnyConfigEvaluation {
234    fn create_hash(&self, name: &InternedString) -> u64 {
235        match self {
236            AnyConfigEvaluation::DynamicConfig(eval) => eval.create_hash(name),
237            AnyConfigEvaluation::Experiment(eval) => eval.create_hash(name),
238        }
239    }
240}
241
242#[derive(Serialize, Deserialize, Clone)]
243pub struct LayerEvaluation {
244    #[serde(flatten)]
245    pub base: BaseEvaluation,
246
247    pub value: DynamicReturnable,
248
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub id_type: Option<InternedString>,
251
252    pub is_device_based: bool,
253
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub group_name: Option<InternedString>,
256
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub is_experiment_active: Option<bool>,
259
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub is_user_in_experiment: Option<bool>,
262
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub allocated_experiment_name: Option<InternedString>,
265    pub explicit_parameters: ExplicitParameters,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
268
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub parameter_rule_ids: Option<HashMap<InternedString, InternedString>>,
271}
272
273impl GCIRHashable for LayerEvaluation {
274    fn create_hash(&self, name: &InternedString) -> u64 {
275        let mut hash_array = vec![
276            name.hash,
277            self.value.get_hash(),
278            self.base.rule_id.hash,
279            hash_secondary_exposures(&self.base.secondary_exposures),
280            self.group_name.as_ref().map_or(0, |g| g.hash),
281            opt_bool_to_hashable(&self.is_experiment_active),
282            opt_bool_to_hashable(&self.is_user_in_experiment),
283            self.allocated_experiment_name
284                .as_ref()
285                .map_or(0, |n| n.hash),
286        ];
287        let mut explicit_params_hashes = Vec::new();
288        for value in self.explicit_parameters.to_vec_interned() {
289            explicit_params_hashes.push(value.hash);
290        }
291        hash_array.push(hashing::hash_one(explicit_params_hashes));
292        let mut undelegated_secondary_exposure_hashes = Vec::new();
293        if let Some(undelegated_secondary_exposures) = &self.undelegated_secondary_exposures {
294            for exposure in undelegated_secondary_exposures {
295                undelegated_secondary_exposure_hashes.push(exposure.create_hash(&exposure.gate));
296            }
297        }
298        hash_array.push(hashing::hash_one(undelegated_secondary_exposure_hashes));
299        if let Some(parameter_rule_ids) = &self.parameter_rule_ids {
300            let mut param_rule_ids_hash = Vec::new();
301            for (param_name, rule_id) in parameter_rule_ids {
302                param_rule_ids_hash.push(param_name.hash);
303                param_rule_ids_hash.push(rule_id.hash);
304            }
305            hash_array.push(hashing::hash_one(param_rule_ids_hash));
306        }
307
308        hashing::hash_one(hash_array)
309    }
310}
311
312fn hash_secondary_exposures(exposures: &Vec<SecondaryExposure>) -> u64 {
313    let mut secondary_exposure_hashes = Vec::new();
314    for exposure in exposures {
315        secondary_exposure_hashes.push(exposure.create_hash(&exposure.gate));
316    }
317    hashing::hash_one(secondary_exposure_hashes)
318}