1use std::collections::HashMap;
2
3use serde_json::Value;
4
5use crate::dto::GrowthBookFeatureRuleExperiment;
6use crate::extensions::{FindGrowthBookAttribute, JsonHelper};
7use crate::hash::{HashCode, HashCodeVersion};
8use crate::model_public::{ExperimentResult, FeatureResult, GrowthBookAttribute};
9use crate::namespace::use_case::Namespace;
10use crate::range::model::Range;
11
12impl GrowthBookFeatureRuleExperiment {
13 pub fn get_match_value(
14 &self,
15 feature_name: &str,
16 user_attributes: &Vec<GrowthBookAttribute>,
17 forced_variations: &Option<HashMap<String, i64>>,
18 ) -> Option<FeatureResult> {
19 if let Some(feature_attribute) = &self.hash_attribute {
20 self.check_experiment(&feature_name, user_attributes, forced_variations, feature_attribute)
21 } else {
22 let fallback_attribute = self.get_fallback_attribute();
23 self.check_experiment(&feature_name, user_attributes, forced_variations, &fallback_attribute)
24 }
25 }
26
27 fn check_experiment(
28 &self,
29 feature_name: &&str,
30 user_attributes: &Vec<GrowthBookAttribute>,
31 forced_variations: &Option<HashMap<String, i64>>,
32 feature_attribute: &str,
33 ) -> Option<FeatureResult> {
34 if let Some(user_value) = user_attributes.find_value(feature_attribute) {
35 if let Some((namespace, range)) = &self.namespace_range() {
36 if !Namespace::is_in(&user_value, namespace, range) {
37 return None;
38 }
39 }
40
41 if let Some(forced_variation) = self.forced_variation(feature_name, user_attributes, forced_variations) {
42 return Some(forced_variation);
43 }
44
45 let user_weight = HashCode::hash_code(&user_value.to_string(), &self.seed(feature_name), HashCodeVersion::from(self.hash_version)).unwrap_or(-1.0);
46 let ranges = self.ranges();
47 let index = choose_variation(user_weight, ranges);
48 if index >= 0 {
49 let usize_index = index as usize;
50 let value = self.variations[usize_index].clone();
51 let (meta_value, pass_through) = self.get_meta_value(usize_index);
52 if !pass_through {
53 return Some(FeatureResult::experiment(
54 value.clone(),
55 self.model_experiment(),
56 create_experiment_result(
57 feature_name,
58 value.clone(),
59 index,
60 true,
61 Some(feature_attribute.to_string()),
62 Some(user_value.to_value()),
63 Some(user_weight),
64 meta_value,
65 ),
66 ));
67 }
68 }
69 }
70
71 None
72 }
73
74 fn forced_variation(
75 &self,
76 feature_name: &str,
77 user_attributes: &Vec<GrowthBookAttribute>,
78 forced_variations: &Option<HashMap<String, i64>>,
79 ) -> Option<FeatureResult> {
80 if let Some(forced_variations) = forced_variations {
81 if let Some(found_forced_variation) = forced_variations.get(feature_name) {
82 let hash_attribute = self.hash_attribute.clone().unwrap_or(self.get_fallback_attribute());
83 if let Some(user_value) = user_attributes.find_value(&hash_attribute) {
84 let forced_variation_index = *found_forced_variation as usize;
85 let value = self.variations[forced_variation_index].clone();
86 let (meta_value, pass_through) = self.get_meta_value(forced_variation_index);
87 if !pass_through {
88 return Some(FeatureResult::experiment(
89 value.clone(),
90 self.model_experiment(),
91 create_experiment_result(
92 feature_name,
93 value.clone(),
94 *found_forced_variation,
95 true,
96 self.hash_attribute.clone(),
97 Some(user_value.to_value()),
98 None,
99 meta_value,
100 ),
101 ));
102 }
103 }
104 }
105 }
106 None
107 }
108
109 fn get_meta_value(
110 &self,
111 usize_index: usize,
112 ) -> (String, bool) {
113 match &self.meta {
114 None => (format!("{usize_index}"), false),
115 Some(it) => {
116 if let Some(meta_value) = it.force_array(vec![]).get(usize_index) {
117 let pass_through = if let Some(pass_through_value) = meta_value.get("passthrough") {
118 pass_through_value.force_bool(false)
119 } else {
120 false
121 };
122
123 if let Some(key) = meta_value.get("key") {
124 (key.force_string(""), pass_through)
125 } else {
126 (format!("{usize_index}"), pass_through)
127 }
128 } else {
129 (format!("{usize_index}"), false)
130 }
131 },
132 }
133 }
134
135 fn get_fallback_attribute(&self) -> String {
136 self.fallback_attribute.clone().unwrap_or(String::from("id"))
137 }
138}
139
140#[allow(clippy::too_many_arguments)]
141fn create_experiment_result(
142 feature_name: &str,
143 value: Value,
144 variation_id: i64,
145 hash_used: bool,
146 hash_attribute: Option<String>,
147 hash_value: Option<Value>,
148 bucket: Option<f32>,
149 key: String,
150) -> ExperimentResult {
151 ExperimentResult {
152 feature_id: String::from(feature_name),
153 value,
154 variation_id,
155 in_experiment: true,
156 hash_used,
157 hash_attribute,
158 hash_value,
159 bucket,
160 key,
161 sticky_bucket_used: false,
162 }
163}
164
165fn choose_variation(
166 user_weight: f32,
167 ranges: Vec<Range>,
168) -> i64 {
169 for (index, range) in ranges.iter().enumerate() {
170 if range.in_range(&user_weight) {
171 return index as i64;
172 }
173 }
174 -1
175}
176
177#[cfg(test)]
178mod test {
179 use std::fs;
180
181 use serde::Deserialize;
182 use serde_json::Value;
183
184 use crate::feature::feature_rule_experiment::choose_variation;
185 use crate::range::model::Range;
186
187 #[tokio::test]
188 async fn evaluate_choose_variation() -> Result<(), Box<dyn std::error::Error>> {
189 let cases = Cases::new();
190
191 for value in cases.choose_variation {
192 let eval_choose_variation = EvalChooseVariation::new(value);
193 let index = choose_variation(eval_choose_variation.weight, eval_choose_variation.ranges);
194 if eval_choose_variation.index != index {
195 panic!(
196 "EvalChooseVariation failed; name='{}' expected_index={} index={index}",
197 eval_choose_variation.name, eval_choose_variation.index
198 )
199 }
200 }
201
202 Ok(())
203 }
204
205 #[derive(Deserialize, Clone)]
206 #[serde(rename_all = "camelCase")]
207 struct Cases {
208 choose_variation: Vec<Value>,
209 }
210
211 pub struct EvalChooseVariation {
212 name: String,
213 weight: f32,
214 ranges: Vec<Range>,
215 index: i64,
216 }
217
218 impl EvalChooseVariation {
219 fn new(value: Value) -> Self {
220 let array = value.as_array().expect("Failed to convert to array");
221 Self {
222 name: array[0].as_str().expect("Failed to convert to str").to_string(),
223 weight: array[1].as_f64().expect("Failed to convert to f64") as f32,
224 ranges: array[2]
225 .as_array()
226 .expect("Failed to convert to array")
227 .iter()
228 .map(|it| {
229 let array = it.as_array().expect("Failed to convert to array [2]");
230 Range {
231 start: array[0].as_f64().expect("Failed to convert to f64") as f32,
232 end: array[1].as_f64().expect("Failed to convert to f64") as f32,
233 }
234 })
235 .collect(),
236 index: array[3].as_i64().expect("Failed to convert to i64"),
237 }
238 }
239 }
240
241 impl Cases {
242 pub fn new() -> Self {
243 let contents = fs::read_to_string("./tests/all_cases.json").expect("Should have been able to read the file");
244
245 serde_json::from_str(&contents).expect("Failed to create cases")
246 }
247 }
248}