Skip to main content

statsig_rust/
statsig_types.rs

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