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(Default, Clone)]
23#[cfg(feature = "ffi-support")]
24pub struct BulkEvaluationOptions {
25    pub feature_gate_filter: Option<Vec<String>>,
26    pub dynamic_config_filter: Option<Vec<String>>,
27    pub experiment_filter: Option<Vec<String>>,
28    pub layer_filter: Option<Vec<String>>,
29    pub include_local_override: bool,
30}
31
32#[derive(Default, Clone)]
33#[cfg(feature = "ffi-support")]
34pub struct ResolvedBulkEvaluationOptions {
35    pub feature_gates: Vec<String>,
36    pub dynamic_configs: Vec<String>,
37    pub experiments: Vec<String>,
38    pub layers: Vec<String>,
39    pub include_local_override: bool,
40}
41
42#[derive(Serialize, Deserialize, Clone, Default)]
43#[cfg(feature = "ffi-support")]
44pub struct BulkEvaluationResponse {
45    pub feature_gates: HashMap<String, BulkFeatureGateEvaluation>,
46    pub dynamic_configs: HashMap<String, BulkDynamicConfigEvaluation>,
47    pub experiments: HashMap<String, BulkExperimentEvaluation>,
48    pub layer_configs: HashMap<String, BulkLayerEvaluation>,
49}
50
51#[derive(Serialize, Deserialize, Clone)]
52#[cfg(feature = "ffi-support")]
53pub struct BulkEvaluationCommonFields {
54    #[serde(rename = "ruleID")]
55    pub rule_id: InternedString,
56    #[serde(rename = "idType")]
57    pub id_type: Option<InternedString>,
58    pub details: EvaluationDetails,
59    #[serde(rename = "exposureToken")]
60    pub exposure_token: Option<String>,
61}
62
63#[derive(Serialize, Deserialize, Clone)]
64#[cfg(feature = "ffi-support")]
65pub struct BulkFeatureGateEvaluation {
66    pub value: bool,
67    #[serde(flatten)]
68    pub common: BulkEvaluationCommonFields,
69}
70
71#[derive(Serialize, Deserialize, Clone)]
72#[cfg(feature = "ffi-support")]
73pub struct BulkDynamicConfigEvaluation {
74    pub value: HashMap<String, Value>,
75    #[serde(flatten)]
76    pub common: BulkEvaluationCommonFields,
77}
78
79#[derive(Serialize, Deserialize, Clone)]
80#[cfg(feature = "ffi-support")]
81pub struct BulkExperimentEvaluation {
82    pub value: HashMap<String, Value>,
83    #[serde(rename = "groupName")]
84    pub group_name: Option<String>,
85    #[serde(flatten)]
86    pub common: BulkEvaluationCommonFields,
87}
88
89#[derive(Serialize, Deserialize, Clone)]
90#[cfg(feature = "ffi-support")]
91pub struct BulkLayerEvaluation {
92    pub value: HashMap<String, Value>,
93    #[serde(rename = "groupName")]
94    pub group_name: Option<String>,
95    #[serde(rename = "allocatedExperimentName")]
96    pub allocated_experiment_name: Option<String>,
97    #[serde(flatten)]
98    pub common: BulkEvaluationCommonFields,
99}
100
101#[derive(Serialize, Deserialize, Clone)]
102pub struct FeatureGate {
103    pub name: String,
104    pub value: bool,
105    pub rule_id: String,
106    pub id_type: String,
107    pub details: EvaluationDetails,
108
109    pub(crate) __evaluation: Option<GateEvaluation>,
110}
111
112#[derive(Serialize, Deserialize, Clone)]
113pub struct DynamicConfig {
114    pub name: String,
115    pub value: HashMap<String, Value>,
116    pub rule_id: String,
117    pub id_type: String,
118    pub details: EvaluationDetails,
119
120    pub __evaluation: Option<DynamicConfigEvaluation>,
121}
122
123impl DynamicConfig {
124    #[must_use]
125    pub fn get_opt<T: DeserializeOwned>(&self, param_name: &str) -> Option<T> {
126        match self.value.get(param_name) {
127            Some(value) => from_value(value.clone()).ok(),
128            None => None,
129        }
130    }
131
132    pub fn get_typed_opt(&self, param_name: &str, fallback: Option<Value>) -> Option<Value> {
133        extract_matching_type(&self.value, param_name, &fallback).or(fallback)
134    }
135}
136
137#[derive(Serialize, Deserialize, Clone)]
138pub struct Experiment {
139    pub name: String,
140    pub value: HashMap<String, Value>,
141    pub rule_id: String,
142    pub id_type: String,
143    pub group_name: Option<String>,
144    pub details: EvaluationDetails,
145    pub is_experiment_active: bool,
146
147    pub __evaluation: Option<ExperimentEvaluation>,
148}
149
150impl Experiment {
151    #[must_use]
152    pub fn get_opt<T: DeserializeOwned>(&self, param_name: &str) -> Option<T> {
153        match self.value.get(param_name) {
154            Some(value) => from_value(value.clone()).ok(),
155            None => None,
156        }
157    }
158
159    pub fn get_typed_opt(&self, param_name: &str, fallback: Option<Value>) -> Option<Value> {
160        extract_matching_type(&self.value, param_name, &fallback).or(fallback)
161    }
162}
163
164#[derive(Serialize, Deserialize, Clone)]
165pub struct Layer {
166    pub name: String,
167    pub rule_id: String,
168    pub id_type: String,
169
170    pub group_name: Option<String>,
171    pub details: EvaluationDetails,
172    pub allocated_experiment_name: Option<String>,
173    pub is_experiment_active: bool,
174
175    pub __parameter_rule_ids: Option<HashMap<InternedString, InternedString>>,
176    pub __evaluation: Option<LayerEvaluation>,
177    pub __value: HashMap<String, Value>,
178    pub __user: StatsigUserLoggable,
179    pub __disable_exposure: bool,
180
181    pub __version: Option<u32>, // todo: rm when Java/PHP layer exposures are not a JSON round trip
182
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub __exposure_info: Option<ExtraExposureInfo>,
185
186    #[serde(skip_serializing, skip_deserializing)]
187    pub __event_logger_ptr: Option<Weak<EventLogger>>,
188}
189
190impl Layer {
191    pub fn get_opt<T: DeserializeOwned>(&self, param_name: &str) -> Option<T> {
192        let value = match self.__value.get(param_name) {
193            Some(value) => value.clone(),
194            None => return None,
195        };
196
197        match from_value(value.clone()) {
198            Ok(value) => {
199                self.log_param_exposure(param_name);
200                Some(value)
201            }
202            Err(_) => None,
203        }
204    }
205
206    pub fn get_typed_opt(&self, param_name: &str, fallback: Option<Value>) -> Option<Value> {
207        match extract_matching_type(&self.__value, param_name, &fallback) {
208            Some(value) => {
209                self.log_param_exposure(param_name);
210                Some(value)
211            }
212            None => fallback,
213        }
214    }
215
216    pub fn get_raw_value(&self, param_name: &str) -> Option<Value> {
217        match self.__value.get(param_name) {
218            Some(value) => {
219                self.log_param_exposure(param_name);
220                Some(value.clone())
221            }
222            None => None,
223        }
224    }
225
226    fn log_param_exposure(&self, param_name: &str) -> Option<()> {
227        let logger = self.__event_logger_ptr.as_ref()?.upgrade()?;
228
229        if self.__disable_exposure {
230            logger.increment_non_exposure_checks(&self.name);
231            return None;
232        }
233
234        logger.enqueue(EnqueueLayerParamExpoOp::LayerRef(
235            Utc::now().timestamp_millis() as u64,
236            self,
237            param_name,
238            ExposureTrigger::Auto,
239        ));
240
241        None
242    }
243}
244
245macro_rules! impl_common_get_methods {
246    ($struct_name:ident) => {
247        impl $struct_name {
248            pub fn get<T: DeserializeOwned>(&self, param_name: &str, fallback: T) -> T {
249                self.get_opt(param_name).unwrap_or_else(|| fallback)
250            }
251
252            #[must_use]
253            pub fn get_bool(&self, param_name: &str, fallback: bool) -> bool {
254                self.get(param_name, fallback)
255            }
256
257            #[must_use]
258            pub fn get_f64(&self, param_name: &str, fallback: f64) -> f64 {
259                self.get(param_name, fallback)
260            }
261
262            #[must_use]
263            pub fn get_i64(&self, param_name: &str, fallback: i64) -> i64 {
264                self.get(param_name, fallback)
265            }
266
267            #[must_use]
268            pub fn get_string(&self, param_name: &str, fallback: String) -> String {
269                self.get(param_name, fallback)
270            }
271
272            #[must_use]
273            pub fn get_array(&self, param_name: &str, fallback: Vec<Value>) -> Vec<Value> {
274                self.get(param_name, fallback)
275            }
276
277            #[must_use]
278            pub fn get_object(
279                &self,
280                param_name: &str,
281                fallback: HashMap<String, Value>,
282            ) -> HashMap<String, Value> {
283                self.get(param_name, fallback)
284            }
285        }
286    };
287}
288
289#[derive(Serialize, Clone)]
290pub struct ParameterStore<'a> {
291    pub name: String,
292    pub details: EvaluationDetails,
293    pub parameters: HashMap<String, Parameter>,
294    pub options: ParameterStoreEvaluationOptions,
295
296    #[serde(skip_serializing, skip_deserializing)]
297    pub _statsig_ref: &'a Statsig,
298}
299
300impl ParameterStore<'_> {
301    pub fn get_opt<T: DeserializeOwned>(&self, user: &StatsigUser, param_name: &str) -> Option<T> {
302        let param = self.parameters.get(param_name)?;
303        match param {
304            Parameter::StaticValue(static_value) => from_value(static_value.value.clone()).ok(),
305            Parameter::Gate(gate) => {
306                let res = self._statsig_ref.check_gate_with_options(
307                    user,
308                    &gate.gate_name,
309                    self.options.into(),
310                );
311                let val = match res {
312                    true => gate.pass_value.clone(),
313                    false => gate.fail_value.clone(),
314                };
315                from_value(val).ok()
316            }
317            Parameter::DynamicConfig(dynamic_config) => {
318                let res = self._statsig_ref.get_dynamic_config_with_options(
319                    user,
320                    &dynamic_config.config_name,
321                    self.options.into(),
322                );
323                res.get_opt(&dynamic_config.param_name)?
324            }
325            Parameter::Experiment(experiment) => {
326                let res = self._statsig_ref.get_experiment_with_options(
327                    user,
328                    &experiment.experiment_name,
329                    self.options.into(),
330                );
331                res.get_opt(&experiment.param_name)?
332            }
333            Parameter::Layer(layer) => {
334                let res = self._statsig_ref.get_layer_with_options(
335                    user,
336                    &layer.layer_name,
337                    self.options.into(),
338                );
339                res.get_opt(&layer.param_name)?
340            }
341        }
342    }
343
344    pub fn get<T: DeserializeOwned>(&self, user: &StatsigUser, param_name: &str, fallback: T) -> T {
345        self.get_opt(user, param_name).unwrap_or(fallback)
346    }
347
348    pub fn get_json_value(
349        &self,
350        user: &StatsigUser,
351        param_name: &str,
352        fallback: Option<Value>,
353    ) -> Value {
354        match fallback {
355            None | Some(Value::Null) => self
356                .get_opt::<Value>(user, param_name)
357                .unwrap_or(Value::Null),
358            Some(Value::Bool(boolean)) => self.get_bool(user, param_name, boolean).into(),
359            Some(Value::Number(number)) => self.get(user, param_name, number).into(),
360            Some(Value::String(string)) => self.get_string(user, param_name, string).into(),
361            Some(Value::Array(vec)) => self.get_array(user, param_name, vec).into(),
362            Some(Value::Object(map)) => self
363                .get_object(user, param_name, map.into_iter().collect())
364                .into_iter()
365                .collect(),
366        }
367    }
368
369    pub fn get_bool(&self, user: &StatsigUser, param_name: &str, fallback: bool) -> bool {
370        self.get(user, param_name, fallback)
371    }
372
373    pub fn get_f64(&self, user: &StatsigUser, param_name: &str, fallback: f64) -> f64 {
374        self.get(user, param_name, fallback)
375    }
376
377    pub fn get_i64(&self, user: &StatsigUser, param_name: &str, fallback: i64) -> i64 {
378        self.get(user, param_name, fallback)
379    }
380
381    pub fn get_string(&self, user: &StatsigUser, param_name: &str, fallback: String) -> String {
382        self.get(user, param_name, fallback)
383    }
384
385    pub fn get_array(
386        &self,
387        user: &StatsigUser,
388        param_name: &str,
389        fallback: Vec<Value>,
390    ) -> Vec<Value> {
391        self.get(user, param_name, fallback)
392    }
393
394    pub fn get_object(
395        &self,
396        user: &StatsigUser,
397        param_name: &str,
398        fallback: HashMap<String, Value>,
399    ) -> HashMap<String, Value> {
400        self.get(user, param_name, fallback)
401    }
402}
403
404impl_common_get_methods!(DynamicConfig);
405impl_common_get_methods!(Experiment);
406impl_common_get_methods!(Layer);
407
408pub enum OverrideAdapterType {
409    LocalOverride,
410}
411
412fn extract_matching_type(
413    value: &HashMap<String, Value>,
414    param_name: &str,
415    fallback: &Option<Value>,
416) -> Option<Value> {
417    let found = value.get(param_name)?;
418    match (fallback, found) {
419        (Some(Value::Bool(_)), Value::Bool(_)) => Some(found.clone()),
420        (Some(Value::Number(_)), Value::Number(_)) => Some(found.clone()),
421        (Some(Value::String(_)), Value::String(_)) => Some(found.clone()),
422        (Some(Value::Array(_)), Value::Array(_)) => Some(found.clone()),
423        (Some(Value::Object(_)), Value::Object(_)) => Some(found.clone()),
424        (Some(Value::Null), Value::Null) => Some(found.clone()),
425        (None, value) => Some(value.clone()),
426        _ => None,
427    }
428}