Skip to main content

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