use std::collections::HashMap;
use super::dynamic_returnable::DynamicReturnable;
use crate::{
evaluation::secondary_exposure_key::SecondaryExposureKey,
gcir::gcir_formatter::GCIRHashable,
hashing::{self, opt_bool_to_hashable},
interned_string::InternedString,
specs_response::explicit_params::ExplicitParameters,
};
use serde::{Deserialize, Serialize};
pub fn is_false(v: &bool) -> bool {
!(*v)
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SecondaryExposure {
pub gate: InternedString,
pub gate_value: InternedString,
#[serde(rename = "ruleID")]
pub rule_id: InternedString,
}
impl GCIRHashable for SecondaryExposure {
fn create_hash(&self, name: &InternedString) -> u64 {
hashing::hash_u64_slice(&[name.hash, self.gate_value.hash, self.rule_id.hash])
}
}
impl SecondaryExposure {
pub fn get_dedupe_key(&self) -> String {
let mut key = String::new();
key += &self.gate;
key += "|";
key += &self.gate_value;
key += "|";
key += self.rule_id.as_str();
key
}
}
impl From<&SecondaryExposure> for SecondaryExposureKey {
fn from(val: &SecondaryExposure) -> Self {
SecondaryExposureKey {
gate_name_hash: val.gate.hash,
rule_id_hash: val.rule_id.hash,
gate_value_hash: val.gate_value.hash,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct ExtraExposureInfo {
pub sampling_rate: Option<u64>,
pub forward_all_exposures: Option<bool>,
pub has_seen_analytical_gates: Option<bool>,
pub override_config_name: Option<InternedString>,
pub version: Option<u32>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct BaseEvaluation {
pub name: InternedString,
pub rule_id: InternedString,
pub secondary_exposures: Vec<SecondaryExposure>,
pub version: Option<u32>,
#[serde(skip_serializing)]
pub(crate) exposure_info: Option<ExtraExposureInfo>,
}
pub enum AnyEvaluation<'a> {
FeatureGate(&'a GateEvaluation),
DynamicConfig(&'a DynamicConfigEvaluation),
Experiment(&'a ExperimentEvaluation),
Layer(&'a LayerEvaluation),
}
impl AnyEvaluation<'_> {
pub fn get_base_result(&self) -> &BaseEvaluation {
match self {
AnyEvaluation::FeatureGate(gate) => &gate.base,
AnyEvaluation::DynamicConfig(config) => &config.base,
AnyEvaluation::Experiment(experiment) => &experiment.base,
AnyEvaluation::Layer(layer) => &layer.base,
}
}
pub fn get_gate_bool_value(&self) -> bool {
match self {
AnyEvaluation::FeatureGate(eval) => eval.value,
_ => false, }
}
}
impl<'a> From<&'a LayerEvaluation> for AnyEvaluation<'a> {
fn from(layer_eval: &'a LayerEvaluation) -> Self {
AnyEvaluation::Layer(layer_eval)
}
}
impl<'a> From<&'a GateEvaluation> for AnyEvaluation<'a> {
fn from(gate_eval: &'a GateEvaluation) -> Self {
AnyEvaluation::FeatureGate(gate_eval)
}
}
impl<'a> From<&'a ExperimentEvaluation> for AnyEvaluation<'a> {
fn from(experiment_eval: &'a ExperimentEvaluation) -> Self {
AnyEvaluation::Experiment(experiment_eval)
}
}
impl<'a> From<&'a DynamicConfigEvaluation> for AnyEvaluation<'a> {
fn from(dynamic_config_evalation: &'a DynamicConfigEvaluation) -> Self {
AnyEvaluation::DynamicConfig(dynamic_config_evalation)
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct GateEvaluation {
#[serde(flatten)]
pub base: BaseEvaluation,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_type: Option<InternedString>,
pub value: bool,
}
impl GCIRHashable for GateEvaluation {
fn create_hash(&self, name: &InternedString) -> u64 {
let version_hashes = optional_version_hash_values(&self.base.version);
let hash_array = [
name.hash,
self.value as u64,
self.base.rule_id.hash,
hash_secondary_exposures(&self.base.secondary_exposures),
version_hashes[0],
version_hashes[1],
];
hashing::hash_u64_slice(&hash_array)
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct DynamicConfigEvaluation {
#[serde(flatten)]
pub base: BaseEvaluation,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_type: Option<InternedString>,
pub value: DynamicReturnable,
pub is_device_based: bool,
pub passed: bool,
}
impl GCIRHashable for DynamicConfigEvaluation {
fn create_hash(&self, name: &InternedString) -> u64 {
let version_hashes = optional_version_hash_values(&self.base.version);
let hash_array = [
name.hash,
self.value.get_stable_hash(),
self.base.rule_id.hash,
hash_secondary_exposures(&self.base.secondary_exposures),
self.passed as u64,
version_hashes[0],
version_hashes[1],
];
hashing::hash_u64_slice(&hash_array)
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ExperimentEvaluation {
#[serde(flatten)]
pub base: BaseEvaluation,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_type: Option<InternedString>,
pub value: DynamicReturnable,
pub is_device_based: bool,
#[serde(skip_serializing_if = "is_false")]
pub is_in_layer: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub explicit_parameters: Option<ExplicitParameters>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_name: Option<InternedString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_experiment_active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_user_in_experiment: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
}
impl GCIRHashable for ExperimentEvaluation {
fn create_hash(&self, name: &InternedString) -> u64 {
let version_hashes = optional_version_hash_values(&self.base.version);
let hash_array = [
name.hash,
self.value.get_stable_hash(),
self.base.rule_id.hash,
hash_secondary_exposures(&self.base.secondary_exposures),
self.is_in_layer as u64,
version_hashes[0],
version_hashes[1],
hash_explicit_parameters(self.explicit_parameters.as_ref()),
self.group_name.as_ref().map_or(0, |g| g.hash),
opt_bool_to_hashable(&self.is_experiment_active),
opt_bool_to_hashable(&self.is_user_in_experiment),
];
hashing::hash_u64_slice(&hash_array)
}
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum AnyConfigEvaluation {
DynamicConfig(DynamicConfigEvaluation),
Experiment(ExperimentEvaluation),
}
impl GCIRHashable for AnyConfigEvaluation {
fn create_hash(&self, name: &InternedString) -> u64 {
match self {
AnyConfigEvaluation::DynamicConfig(eval) => eval.create_hash(name),
AnyConfigEvaluation::Experiment(eval) => eval.create_hash(name),
}
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct LayerEvaluation {
#[serde(flatten)]
pub base: BaseEvaluation,
pub value: DynamicReturnable,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_type: Option<InternedString>,
pub is_device_based: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_name: Option<InternedString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_experiment_active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_user_in_experiment: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allocated_experiment_name: Option<InternedString>,
pub explicit_parameters: ExplicitParameters,
#[serde(skip_serializing_if = "Option::is_none")]
pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameter_rule_ids: Option<HashMap<InternedString, InternedString>>,
}
impl GCIRHashable for LayerEvaluation {
fn create_hash(&self, name: &InternedString) -> u64 {
let version_hashes = optional_version_hash_values(&self.base.version);
let mut hash_array = hashing::U64HashBuilder::new();
hash_array.push(name.hash);
hash_array.push(self.value.get_stable_hash());
hash_array.push(self.base.rule_id.hash);
hash_array.push(hash_secondary_exposures(&self.base.secondary_exposures));
hash_array.push(self.group_name.as_ref().map_or(0, |g| g.hash));
hash_array.push(opt_bool_to_hashable(&self.is_experiment_active));
hash_array.push(opt_bool_to_hashable(&self.is_user_in_experiment));
hash_array.push(
self.allocated_experiment_name
.as_ref()
.map_or(0, |n| n.hash),
);
hash_array.push(version_hashes[0]);
hash_array.push(version_hashes[1]);
hash_array.push(hash_explicit_parameters(Some(&self.explicit_parameters)));
hash_array.push(hash_optional_secondary_exposures(
self.undelegated_secondary_exposures.as_ref(),
));
if let Some(parameter_rule_ids) = &self.parameter_rule_ids {
hash_array.push(hash_parameter_rule_ids(parameter_rule_ids));
}
hash_array.finish()
}
}
fn hash_secondary_exposures(exposures: &Vec<SecondaryExposure>) -> u64 {
let mut secondary_exposure_hashes = hashing::U64HashBuilder::with_capacity(exposures.len());
for exposure in exposures {
secondary_exposure_hashes.push(exposure.create_hash(&exposure.gate));
}
secondary_exposure_hashes.finish()
}
fn hash_optional_secondary_exposures(exposures: Option<&Vec<SecondaryExposure>>) -> u64 {
let Some(exposures) = exposures else {
return hashing::hash_u64_slice(&[]);
};
hash_secondary_exposures(exposures)
}
fn hash_explicit_parameters(explicit_parameters: Option<&ExplicitParameters>) -> u64 {
let Some(explicit_parameters) = explicit_parameters else {
return hashing::hash_u64_slice(&[]);
};
let mut explicit_params_hashes =
hashing::U64HashBuilder::with_capacity(explicit_parameters.as_slice().len());
for value in explicit_parameters.as_slice() {
explicit_params_hashes.push(value.hash);
}
explicit_params_hashes.finish()
}
fn hash_parameter_rule_ids(parameter_rule_ids: &HashMap<InternedString, InternedString>) -> u64 {
hashing::hash_unordered(
parameter_rule_ids
.iter()
.map(|(param_name, rule_id)| hashing::hash_u64_slice(&[param_name.hash, rule_id.hash]))
.collect(),
)
}
fn optional_version_hash_values(version: &Option<u32>) -> [u64; 2] {
[version.is_some() as u64, version.map_or(0, u64::from)]
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::{hash_parameter_rule_ids, optional_version_hash_values};
use crate::interned_string::InternedString;
#[test]
fn optional_version_hash_values_distinguish_missing_from_zero() {
assert_eq!(optional_version_hash_values(&None), [0, 0]);
assert_eq!(optional_version_hash_values(&Some(0)), [1, 0]);
assert_eq!(optional_version_hash_values(&Some(1)), [1, 1]);
}
#[test]
fn parameter_rule_id_hash_ignores_map_order() {
let first = HashMap::from([
(
InternedString::from_str_ref("alpha"),
InternedString::from_str_ref("rule-a"),
),
(
InternedString::from_str_ref("beta"),
InternedString::from_str_ref("rule-b"),
),
]);
let second = HashMap::from([
(
InternedString::from_str_ref("beta"),
InternedString::from_str_ref("rule-b"),
),
(
InternedString::from_str_ref("alpha"),
InternedString::from_str_ref("rule-a"),
),
]);
assert_eq!(
hash_parameter_rule_ids(&first),
hash_parameter_rule_ids(&second)
);
}
}