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, }
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}