growthbook_rust/
model_public.rs

1use std::fmt::{Display, Formatter};
2
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6
7use crate::error::{GrowthbookError, GrowthbookErrorCode};
8use crate::extensions::JsonHelper;
9
10#[derive(Clone, PartialEq, Debug)]
11pub struct GrowthBookAttribute {
12    pub key: String,
13    pub value: GrowthBookAttributeValue,
14}
15
16#[derive(Clone, PartialEq, Debug)]
17pub enum GrowthBookAttributeValue {
18    Empty,
19    String(String),
20    Int(i64),
21    Float(f64),
22    Bool(bool),
23    Array(Vec<GrowthBookAttributeValue>),
24    Object(Vec<GrowthBookAttribute>),
25}
26
27#[derive(Serialize, Clone, Debug)]
28#[serde(rename_all = "camelCase")]
29pub struct FeatureResult {
30    pub value: Value,
31    pub on: bool,
32    pub off: bool,
33    pub experiment: Option<Experiment>,
34    pub experiment_result: Option<ExperimentResult>,
35    pub source: String,
36}
37
38#[derive(Serialize, Clone, Debug)]
39#[serde(rename_all = "camelCase")]
40pub struct Experiment {
41    pub name: Option<String>,
42    pub seed: Option<String>,
43    pub hash_version: Option<i64>,
44    pub hash_attribute: Option<String>,
45    pub namespace: Option<Vec<Value>>,
46    pub coverage: Option<f32>,
47    pub ranges: Option<Vec<Vec<f32>>>,
48    pub meta: Option<Value>,
49    pub filters: Option<Value>,
50    pub variations: Vec<Value>,
51    pub weights: Option<Vec<f32>>,
52    pub condition: Option<Value>,
53}
54
55#[derive(Serialize, Clone, Debug)]
56#[serde(rename_all = "camelCase")]
57pub struct ExperimentResult {
58    pub feature_id: String,
59    pub value: Value,
60    pub variation_id: i64,
61    pub in_experiment: bool,
62    pub hash_used: bool,
63    pub hash_attribute: Option<String>,
64    pub hash_value: Option<Value>,
65    pub bucket: Option<f32>,
66    pub key: String,
67    pub sticky_bucket_used: bool,
68}
69
70impl GrowthBookAttribute {
71    pub fn new(
72        key: String,
73        value: GrowthBookAttributeValue,
74    ) -> Self {
75        GrowthBookAttribute { key, value }
76    }
77
78    pub fn from(value: Value) -> Result<Vec<Self>, GrowthbookError> {
79        if !value.is_object() {
80            return Err(GrowthbookError::new(
81                GrowthbookErrorCode::GrowthBookAttributeIsNotObject,
82                "GrowthBookAttribute must be an object with at leat one key value pair",
83            ));
84        }
85
86        let default_map = Map::new();
87        let map = value.as_object().unwrap_or(&default_map);
88        let mut attributes = Vec::new();
89        for (key, value) in map {
90            attributes.push(GrowthBookAttribute {
91                key: key.clone(),
92                value: GrowthBookAttributeValue::from(value.clone()),
93            });
94        }
95        Ok(attributes)
96    }
97}
98
99impl GrowthBookAttributeValue {
100    pub fn is_number(&self) -> bool {
101        if let Ok(regex) = Regex::new("\\d+") {
102            regex.is_match(&self.to_string().replace('.', ""))
103        } else {
104            false
105        }
106    }
107    pub fn as_f64(&self) -> Option<f64> {
108        self.to_string().replace('.', "").parse::<f64>().ok()
109    }
110
111    pub fn to_value(&self) -> Value {
112        match self {
113            GrowthBookAttributeValue::Empty => Value::Null,
114            GrowthBookAttributeValue::String(it) => Value::from(it.clone()),
115            GrowthBookAttributeValue::Int(it) => Value::from(*it),
116            GrowthBookAttributeValue::Float(it) => Value::from(*it),
117            GrowthBookAttributeValue::Bool(it) => Value::from(*it),
118            GrowthBookAttributeValue::Array(it) => Value::Array(it.iter().map(|item| item.to_value()).collect()),
119            GrowthBookAttributeValue::Object(it) => {
120                let mut map = Map::new();
121                for attr in it {
122                    map.insert(attr.key.clone(), attr.value.to_value());
123                }
124                Value::Object(map)
125            },
126        }
127    }
128}
129
130impl From<Value> for GrowthBookAttributeValue {
131    fn from(value: Value) -> Self {
132        if value.is_string() {
133            GrowthBookAttributeValue::String(value.as_str().unwrap_or_default().to_string())
134        } else if value.is_boolean() {
135            GrowthBookAttributeValue::Bool(value.as_bool().unwrap_or_default())
136        } else if value.is_i64() {
137            GrowthBookAttributeValue::Int(value.as_i64().unwrap_or_default())
138        } else if value.is_f64() {
139            GrowthBookAttributeValue::Float(value.as_f64().unwrap_or_default())
140        } else if value.is_array() {
141            let vec: Vec<GrowthBookAttributeValue> = value.as_array().unwrap_or(&vec![]).iter().map(|item| GrowthBookAttributeValue::from(item.clone())).collect();
142            GrowthBookAttributeValue::Array(vec)
143        } else {
144            let objects: Vec<_> = value
145                .as_object()
146                .unwrap_or(&Map::new())
147                .iter()
148                .map(|(k, v)| GrowthBookAttribute::new(k.clone(), GrowthBookAttributeValue::from(v.clone())))
149                .collect();
150
151            if objects.is_empty() {
152                GrowthBookAttributeValue::Empty
153            } else {
154                GrowthBookAttributeValue::Object(objects)
155            }
156        }
157    }
158}
159
160impl Display for GrowthBookAttributeValue {
161    fn fmt(
162        &self,
163        f: &mut Formatter<'_>,
164    ) -> std::fmt::Result {
165        let message = match self {
166            GrowthBookAttributeValue::Empty => String::new(),
167            GrowthBookAttributeValue::Array(it) => it.iter().fold(String::new(), |acc, value| format!("{acc}{}", value)),
168            GrowthBookAttributeValue::Object(it) => it.iter().fold(String::new(), |acc, att| format!("{acc}{}", att.value)),
169            GrowthBookAttributeValue::String(it) => it.clone(),
170            GrowthBookAttributeValue::Int(it) => it.to_string(),
171            GrowthBookAttributeValue::Float(it) => it.to_string(),
172            GrowthBookAttributeValue::Bool(it) => it.to_string(),
173        };
174
175        write!(f, "{}", message)
176    }
177}
178
179impl FeatureResult {
180    pub fn value_as<T>(&self) -> Result<T, GrowthbookError>
181    where
182        for<'a> T: Deserialize<'a>,
183    {
184        serde_json::from_value(self.value.clone()).map_err(GrowthbookError::from)
185    }
186
187    pub fn new(
188        value: Value,
189        on: bool,
190        source: String,
191    ) -> Self {
192        FeatureResult {
193            value,
194            on,
195            off: !on,
196            experiment: None,
197            experiment_result: None,
198            source,
199        }
200    }
201
202    pub fn force(value: Value) -> Self {
203        let is_on = is_on(&value);
204        FeatureResult {
205            value,
206            on: is_on,
207            off: !is_on,
208            experiment: None,
209            experiment_result: None,
210            source: String::from("force"),
211        }
212    }
213
214    pub fn experiment(
215        value: Value,
216        experiment: Experiment,
217        experiment_result: ExperimentResult,
218    ) -> Self {
219        let is_on = is_on(&value);
220        FeatureResult {
221            value,
222            on: is_on,
223            off: !is_on,
224            experiment: Some(experiment),
225            experiment_result: Some(experiment_result),
226            source: String::from("experiment"),
227        }
228    }
229
230    pub fn from_default_value(option_value: Option<Value>) -> Self {
231        let value = option_value.unwrap_or(Value::Null);
232        let is_on = is_on(&value);
233        Self {
234            value,
235            on: is_on,
236            off: !is_on,
237            experiment: None,
238            experiment_result: None,
239            source: String::from("defaultValue"),
240        }
241    }
242
243    pub fn prerequisite() -> Self {
244        Self {
245            value: Value::Null,
246            on: false,
247            off: true,
248            experiment: None,
249            experiment_result: None,
250            source: String::from("prerequisite"),
251        }
252    }
253
254    pub fn cyclic_prerequisite() -> Self {
255        Self {
256            value: Value::Null,
257            on: false,
258            off: true,
259            experiment: None,
260            experiment_result: None,
261            source: String::from("cyclicPrerequisite"),
262        }
263    }
264
265    pub fn unknown_feature() -> Self {
266        Self {
267            value: Value::Null,
268            on: false,
269            off: true,
270            experiment: None,
271            experiment_result: None,
272            source: String::from("unknownFeature"),
273        }
274    }
275}
276
277fn is_on(value: &Value) -> bool {
278    let is_on = if value.is_null() {
279        false
280    } else if (value.is_number() && value.force_f64(-1.0) != 0.0) || (value.is_string() && value.force_string("any") != "") {
281        true
282    } else if value.is_boolean() {
283        value.as_bool().unwrap_or(false)
284    } else if value.is_object() {
285        value.as_object().map(|it| !it.is_empty()).unwrap_or(false)
286    } else if value.is_array() {
287        value.as_array().map(|it| !it.is_empty()).unwrap_or(false)
288    } else {
289        false
290    };
291    is_on
292}