statsig_rust/
statsig_types.rs

1use crate::evaluation::evaluation_details::EvaluationDetails;
2use crate::evaluation::evaluation_types::{
3    DynamicConfigEvaluation, ExperimentEvaluation, GateEvaluation, LayerEvaluation,
4};
5use crate::event_logging::event_logger::{EventLogger, ExposureTrigger};
6use crate::event_logging::event_queue::queued_layer_param_expo::EnqueueLayerParamExpoOp;
7use crate::specs_response::param_store_types::Parameter;
8use crate::statsig_core_api_options::ParameterStoreEvaluationOptions;
9use crate::user::StatsigUserLoggable;
10use crate::Statsig;
11use crate::StatsigUser;
12
13use chrono::Utc;
14use serde::de::DeserializeOwned;
15use serde::{Deserialize, Serialize};
16use serde_json::{from_value, Value};
17use std::collections::HashMap;
18use std::sync::Weak;
19
20#[derive(Serialize, Deserialize, Clone)]
21pub struct FeatureGate {
22    pub name: String,
23    pub value: bool,
24    pub rule_id: String,
25    pub id_type: String,
26    pub details: EvaluationDetails,
27
28    pub(crate) __evaluation: Option<GateEvaluation>,
29}
30
31#[derive(Serialize, Deserialize, Clone)]
32pub struct DynamicConfig {
33    pub name: String,
34    pub value: HashMap<String, Value>,
35    pub rule_id: String,
36    pub id_type: String,
37    pub details: EvaluationDetails,
38
39    pub __evaluation: Option<DynamicConfigEvaluation>,
40}
41
42impl DynamicConfig {
43    #[must_use]
44    pub fn get_opt<T: DeserializeOwned>(&self, param_name: &str) -> Option<T> {
45        match self.value.get(param_name) {
46            Some(value) => from_value(value.clone()).ok(),
47            None => None,
48        }
49    }
50
51    pub fn get_typed_opt(&self, param_name: &str, fallback: Option<Value>) -> Option<Value> {
52        extract_matching_type(&self.value, param_name, &fallback).or(fallback)
53    }
54}
55
56#[derive(Serialize, Deserialize, Clone)]
57pub struct Experiment {
58    pub name: String,
59    pub value: HashMap<String, Value>,
60    pub rule_id: String,
61    pub id_type: String,
62    pub group_name: Option<String>,
63    pub details: EvaluationDetails,
64    pub is_experiment_active: bool,
65
66    pub __evaluation: Option<ExperimentEvaluation>,
67}
68
69impl Experiment {
70    #[must_use]
71    pub fn get_opt<T: DeserializeOwned>(&self, param_name: &str) -> Option<T> {
72        match self.value.get(param_name) {
73            Some(value) => from_value(value.clone()).ok(),
74            None => None,
75        }
76    }
77
78    pub fn get_typed_opt(&self, param_name: &str, fallback: Option<Value>) -> Option<Value> {
79        extract_matching_type(&self.value, param_name, &fallback).or(fallback)
80    }
81}
82
83#[derive(Serialize, Deserialize, Clone)]
84pub struct Layer {
85    pub name: String,
86    pub rule_id: String,
87    pub id_type: String,
88
89    pub group_name: Option<String>,
90    pub details: EvaluationDetails,
91    pub allocated_experiment_name: Option<String>,
92    pub is_experiment_active: bool,
93
94    pub __evaluation: Option<LayerEvaluation>,
95    pub __value: HashMap<String, Value>,
96    pub __user: StatsigUserLoggable,
97    pub __disable_exposure: bool,
98
99    pub __version: Option<u32>, // todo: rm when Java/PHP layer exposures are not a JSON round trip
100
101    #[serde(skip_serializing, skip_deserializing)]
102    pub __event_logger_ptr: Option<Weak<EventLogger>>,
103}
104
105impl Layer {
106    pub fn get_opt<T: DeserializeOwned>(&self, param_name: &str) -> Option<T> {
107        let value = match self.__value.get(param_name) {
108            Some(value) => value.clone(),
109            None => return None,
110        };
111
112        match from_value(value.clone()) {
113            Ok(value) => {
114                self.log_param_exposure(param_name);
115                Some(value)
116            }
117            Err(_) => None,
118        }
119    }
120
121    pub fn get_typed_opt(&self, param_name: &str, fallback: Option<Value>) -> Option<Value> {
122        match extract_matching_type(&self.__value, param_name, &fallback) {
123            Some(value) => {
124                self.log_param_exposure(param_name);
125                Some(value)
126            }
127            None => fallback,
128        }
129    }
130
131    pub fn get_raw_value(&self, param_name: &str) -> Option<Value> {
132        match self.__value.get(param_name) {
133            Some(value) => {
134                self.log_param_exposure(param_name);
135                Some(value.clone())
136            }
137            None => None,
138        }
139    }
140
141    fn log_param_exposure(&self, param_name: &str) -> Option<()> {
142        let logger = self.__event_logger_ptr.as_ref()?.upgrade()?;
143
144        if self.__disable_exposure {
145            logger.increment_non_exposure_checks(&self.name);
146            return None;
147        }
148
149        logger.enqueue(EnqueueLayerParamExpoOp::LayerRef(
150            Utc::now().timestamp_millis() as u64,
151            self,
152            param_name,
153            ExposureTrigger::Auto,
154        ));
155
156        None
157    }
158}
159
160macro_rules! impl_common_get_methods {
161    ($struct_name:ident) => {
162        impl $struct_name {
163            pub fn get<T: DeserializeOwned>(&self, param_name: &str, fallback: T) -> T {
164                self.get_opt(param_name).unwrap_or_else(|| fallback)
165            }
166
167            #[must_use]
168            pub fn get_bool(&self, param_name: &str, fallback: bool) -> bool {
169                self.get(param_name, fallback)
170            }
171
172            #[must_use]
173            pub fn get_f64(&self, param_name: &str, fallback: f64) -> f64 {
174                self.get(param_name, fallback)
175            }
176
177            #[must_use]
178            pub fn get_i64(&self, param_name: &str, fallback: i64) -> i64 {
179                self.get(param_name, fallback)
180            }
181
182            #[must_use]
183            pub fn get_string(&self, param_name: &str, fallback: String) -> String {
184                self.get(param_name, fallback)
185            }
186
187            #[must_use]
188            pub fn get_array(&self, param_name: &str, fallback: Vec<Value>) -> Vec<Value> {
189                self.get(param_name, fallback)
190            }
191
192            #[must_use]
193            pub fn get_object(
194                &self,
195                param_name: &str,
196                fallback: HashMap<String, Value>,
197            ) -> HashMap<String, Value> {
198                self.get(param_name, fallback)
199            }
200        }
201    };
202}
203
204#[derive(Serialize, Clone)]
205pub struct ParameterStore<'a> {
206    pub name: String,
207    pub details: EvaluationDetails,
208    pub parameters: HashMap<String, Parameter>,
209    pub options: ParameterStoreEvaluationOptions,
210
211    #[serde(skip_serializing, skip_deserializing)]
212    pub _statsig_ref: &'a Statsig,
213}
214
215impl ParameterStore<'_> {
216    pub fn get_opt<T: DeserializeOwned>(&self, user: &StatsigUser, param_name: &str) -> Option<T> {
217        let param = self.parameters.get(param_name)?;
218        match param {
219            Parameter::StaticValue(static_value) => from_value(static_value.value.clone()).ok(),
220            Parameter::Gate(gate) => {
221                let res = self._statsig_ref.check_gate_with_options(
222                    user,
223                    &gate.gate_name,
224                    self.options.into(),
225                );
226                let val = match res {
227                    true => gate.pass_value.clone(),
228                    false => gate.fail_value.clone(),
229                };
230                from_value(val).ok()
231            }
232            Parameter::DynamicConfig(dynamic_config) => {
233                let res = self._statsig_ref.get_dynamic_config_with_options(
234                    user,
235                    &dynamic_config.config_name,
236                    self.options.into(),
237                );
238                res.get_opt(&dynamic_config.param_name)?
239            }
240            Parameter::Experiment(experiment) => {
241                let res = self._statsig_ref.get_experiment_with_options(
242                    user,
243                    &experiment.experiment_name,
244                    self.options.into(),
245                );
246                res.get_opt(&experiment.param_name)?
247            }
248            Parameter::Layer(layer) => {
249                let res = self._statsig_ref.get_layer_with_options(
250                    user,
251                    &layer.layer_name,
252                    self.options.into(),
253                );
254                res.get_opt(&layer.param_name)?
255            }
256        }
257    }
258
259    pub fn get<T: DeserializeOwned>(&self, user: &StatsigUser, param_name: &str, fallback: T) -> T {
260        self.get_opt(user, param_name).unwrap_or(fallback)
261    }
262
263    pub fn get_json_value(
264        &self,
265        user: &StatsigUser,
266        param_name: &str,
267        fallback: Option<Value>,
268    ) -> Value {
269        match fallback {
270            None | Some(Value::Null) => self
271                .get_opt::<Value>(user, param_name)
272                .unwrap_or(Value::Null),
273            Some(Value::Bool(boolean)) => self.get_bool(user, param_name, boolean).into(),
274            Some(Value::Number(number)) => self.get(user, param_name, number).into(),
275            Some(Value::String(string)) => self.get_string(user, param_name, string).into(),
276            Some(Value::Array(vec)) => self.get_array(user, param_name, vec).into(),
277            Some(Value::Object(map)) => self
278                .get_object(user, param_name, map.into_iter().collect())
279                .into_iter()
280                .collect(),
281        }
282    }
283
284    pub fn get_bool(&self, user: &StatsigUser, param_name: &str, fallback: bool) -> bool {
285        self.get(user, param_name, fallback)
286    }
287
288    pub fn get_f64(&self, user: &StatsigUser, param_name: &str, fallback: f64) -> f64 {
289        self.get(user, param_name, fallback)
290    }
291
292    pub fn get_i64(&self, user: &StatsigUser, param_name: &str, fallback: i64) -> i64 {
293        self.get(user, param_name, fallback)
294    }
295
296    pub fn get_string(&self, user: &StatsigUser, param_name: &str, fallback: String) -> String {
297        self.get(user, param_name, fallback)
298    }
299
300    pub fn get_array(
301        &self,
302        user: &StatsigUser,
303        param_name: &str,
304        fallback: Vec<Value>,
305    ) -> Vec<Value> {
306        self.get(user, param_name, fallback)
307    }
308
309    pub fn get_object(
310        &self,
311        user: &StatsigUser,
312        param_name: &str,
313        fallback: HashMap<String, Value>,
314    ) -> HashMap<String, Value> {
315        self.get(user, param_name, fallback)
316    }
317}
318
319impl_common_get_methods!(DynamicConfig);
320impl_common_get_methods!(Experiment);
321impl_common_get_methods!(Layer);
322
323pub enum OverrideAdapterType {
324    LocalOverride,
325}
326
327fn extract_matching_type(
328    value: &HashMap<String, Value>,
329    param_name: &str,
330    fallback: &Option<Value>,
331) -> Option<Value> {
332    let found = value.get(param_name)?;
333    match (fallback, found) {
334        (Some(Value::Bool(_)), Value::Bool(_)) => Some(found.clone()),
335        (Some(Value::Number(_)), Value::Number(_)) => Some(found.clone()),
336        (Some(Value::String(_)), Value::String(_)) => Some(found.clone()),
337        (Some(Value::Array(_)), Value::Array(_)) => Some(found.clone()),
338        (Some(Value::Object(_)), Value::Object(_)) => Some(found.clone()),
339        (Some(Value::Null), Value::Null) => Some(found.clone()),
340        (None, value) => Some(value.clone()),
341        _ => None,
342    }
343}