Skip to main content

statsig_rust/
statsig_types_raw.rs

1use std::collections::HashMap;
2
3use serde::Serialize;
4use serde_with::skip_serializing_none;
5
6use crate::evaluation::evaluation_details::EvaluationDetails;
7use crate::evaluation::{
8    dynamic_returnable::DynamicReturnable, evaluation_types::ExtraExposureInfo,
9};
10use crate::interned_string::InternedString;
11use crate::specs_response::explicit_params::ExplicitParameters;
12use crate::user::StatsigUserLoggable;
13use crate::{log_e, SecondaryExposure};
14
15const TAG: &str = "SpecTypesRaw";
16
17#[derive(Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct FeatureGateRaw<'a> {
20    pub name: &'a str,
21
22    pub value: bool,
23
24    #[serde(rename = "ruleID")]
25    pub rule_id: SuffixedRuleId<'a>,
26
27    pub id_type: Option<&'a InternedString>,
28
29    pub details: &'a EvaluationDetails,
30}
31
32impl<'a> FeatureGateRaw<'a> {
33    pub fn empty(name: &'a str, details: &'a EvaluationDetails) -> Self {
34        Self {
35            name,
36            value: false,
37            details,
38            rule_id: SuffixedRuleId {
39                rule_id: InternedString::empty_ref(),
40                rule_id_suffix: None,
41            },
42            id_type: None,
43        }
44    }
45
46    pub fn unperformant_to_json_string(&self) -> String {
47        match serde_json::to_string(self) {
48            Ok(s) => s,
49            Err(e) => {
50                log_e!(TAG, "Failed to convert FeatureGateRaw to string: {}", e);
51                format!(r#"{{"name": "{}", "value": false}}"#, self.name)
52            }
53        }
54    }
55}
56
57#[derive(Serialize)]
58#[serde(rename_all = "camelCase")]
59pub struct DynamicConfigRaw<'a> {
60    pub name: &'a str,
61
62    pub value: Option<&'a DynamicReturnable>,
63
64    #[serde(rename = "ruleID")]
65    pub rule_id: SuffixedRuleId<'a>,
66
67    pub id_type: Option<&'a InternedString>,
68
69    pub details: &'a EvaluationDetails,
70}
71
72impl<'a> DynamicConfigRaw<'a> {
73    pub fn empty(name: &'a str, details: &'a EvaluationDetails) -> Self {
74        Self {
75            name,
76            value: None,
77            details,
78            rule_id: SuffixedRuleId {
79                rule_id: InternedString::empty_ref(),
80                rule_id_suffix: None,
81            },
82            id_type: None,
83        }
84    }
85
86    pub fn unperformant_to_json_string(&self) -> String {
87        match serde_json::to_string(self) {
88            Ok(s) => s,
89            Err(e) => {
90                log_e!(TAG, "Failed to convert DynamicConfigRaw to string: {}", e);
91                format!(r#"{{"name": "{}", "value": null}}"#, self.name)
92            }
93        }
94    }
95}
96
97#[derive(Serialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ExperimentRaw<'a> {
100    pub name: &'a str,
101
102    pub value: Option<&'a DynamicReturnable>,
103
104    #[serde(rename = "ruleID")]
105    pub rule_id: SuffixedRuleId<'a>,
106
107    pub id_type: Option<&'a InternedString>,
108
109    pub group_name: Option<&'a InternedString>,
110
111    pub is_experiment_active: Option<bool>,
112
113    pub details: &'a EvaluationDetails,
114
115    pub secondary_exposures: Option<&'a Vec<SecondaryExposure>>,
116}
117
118impl<'a> ExperimentRaw<'a> {
119    pub fn empty(name: &'a str, details: &'a EvaluationDetails) -> Self {
120        Self {
121            name,
122            value: None,
123            details,
124            rule_id: SuffixedRuleId {
125                rule_id: InternedString::empty_ref(),
126                rule_id_suffix: None,
127            },
128            id_type: None,
129            group_name: None,
130            is_experiment_active: None,
131            secondary_exposures: None,
132        }
133    }
134
135    pub fn unperformant_to_json_string(&self) -> String {
136        match serde_json::to_string(self) {
137            Ok(s) => s,
138            Err(e) => {
139                log_e!(TAG, "Failed to convert ExperimentRaw to string: {}", e);
140                format!(r#"{{"name": "{}"}}"#, self.name)
141            }
142        }
143    }
144}
145
146fn is_interned_string_none_or_empty(value: &Option<&InternedString>) -> bool {
147    match value {
148        Some(value) => value.is_empty(),
149        None => true,
150    }
151}
152
153#[derive(Serialize)]
154#[serde(rename_all = "camelCase")]
155#[skip_serializing_none]
156pub struct LayerRaw<'a> {
157    pub name: &'a str,
158
159    pub value: Option<&'a DynamicReturnable>,
160
161    #[serde(rename = "ruleID")]
162    pub rule_id: SuffixedRuleId<'a>,
163
164    pub id_type: Option<&'a InternedString>,
165
166    pub group_name: Option<&'a InternedString>,
167
168    pub is_experiment_active: Option<bool>,
169
170    pub details: &'a EvaluationDetails,
171
172    #[serde(skip_serializing_if = "is_interned_string_none_or_empty")]
173    pub allocated_experiment_name: Option<&'a InternedString>,
174
175    pub disable_exposure: bool,
176
177    pub user: StatsigUserLoggable,
178
179    pub secondary_exposures: Option<&'a Vec<SecondaryExposure>>,
180
181    pub undelegated_secondary_exposures: Option<&'a Vec<SecondaryExposure>>,
182
183    pub explicit_parameters: Option<ExplicitParameters>,
184
185    pub parameter_rule_ids: Option<&'a HashMap<InternedString, InternedString>>,
186
187    pub exposure_info: Option<ExtraExposureInfo>,
188}
189
190impl<'a> LayerRaw<'a> {
191    pub fn empty(name: &'a str, details: &'a EvaluationDetails) -> Self {
192        Self {
193            name,
194            details,
195            rule_id: SuffixedRuleId {
196                rule_id: InternedString::empty_ref(),
197                rule_id_suffix: None,
198            },
199            id_type: None,
200            group_name: None,
201            is_experiment_active: None,
202            value: None,
203            allocated_experiment_name: None,
204            disable_exposure: false,
205            user: StatsigUserLoggable::null(),
206            secondary_exposures: None,
207            undelegated_secondary_exposures: None,
208            explicit_parameters: None,
209            parameter_rule_ids: None,
210            exposure_info: None,
211        }
212    }
213
214    pub fn unperformant_to_json_string(&self) -> String {
215        match serde_json::to_string(self) {
216            Ok(s) => s,
217            Err(e) => {
218                log_e!(TAG, "Failed to convert LayerRaw to string: {}", e);
219                format!(r#"{{"name": "{}"}}"#, self.name)
220            }
221        }
222    }
223}
224
225#[derive(Clone, serde::Deserialize)]
226#[serde(rename_all = "camelCase")]
227#[cfg(feature = "ffi-support")]
228pub struct PartialLayerRaw {
229    pub name: InternedString,
230
231    #[serde(rename = "ruleID")]
232    pub rule_id: Option<InternedString>,
233
234    pub id_type: Option<InternedString>,
235
236    pub group_name: Option<InternedString>,
237
238    pub details: EvaluationDetails,
239
240    pub allocated_experiment_name: Option<InternedString>,
241    pub disable_exposure: bool,
242    pub user: StatsigUserLoggable,
243    pub secondary_exposures: Option<Vec<SecondaryExposure>>,
244    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
245    pub explicit_parameters: Option<ExplicitParameters>,
246    pub parameter_rule_ids: Option<HashMap<InternedString, InternedString>>,
247    pub exposure_info: Option<ExtraExposureInfo>,
248}
249
250#[cfg(feature = "ffi-support")]
251impl From<&LayerRaw<'_>> for PartialLayerRaw {
252    fn from(raw: &LayerRaw<'_>) -> Self {
253        Self {
254            name: InternedString::from_str_ref(raw.name),
255            rule_id: Some(InternedString::from_string(
256                raw.rule_id.unperformant_to_string(),
257            )),
258            id_type: raw.id_type.cloned(),
259            group_name: raw.group_name.cloned(),
260            details: raw.details.clone(),
261            allocated_experiment_name: raw.allocated_experiment_name.cloned(),
262            disable_exposure: raw.disable_exposure,
263            user: raw.user.clone(),
264            secondary_exposures: raw.secondary_exposures.cloned(),
265            undelegated_secondary_exposures: raw.undelegated_secondary_exposures.cloned(),
266            explicit_parameters: raw.explicit_parameters.clone(),
267            parameter_rule_ids: raw.parameter_rule_ids.cloned(),
268            exposure_info: raw.exposure_info.clone(),
269        }
270    }
271}
272
273pub struct SuffixedRuleId<'a> {
274    pub rule_id: &'a InternedString,
275    pub rule_id_suffix: Option<&'a str>,
276}
277
278impl SuffixedRuleId<'_> {
279    pub fn try_as_unprefixed_str(&self) -> Option<&str> {
280        if self.rule_id_suffix.is_some() {
281            // cannot return &str if we need to concat the suffix, use unperformant_to_string instead
282            return None;
283        }
284
285        Some(self.rule_id.as_str())
286    }
287
288    pub fn unperformant_to_string(&self) -> String {
289        if let Some(suffix) = self.rule_id_suffix {
290            return format!("{}:{}", self.rule_id.as_str(), suffix);
291        }
292
293        self.rule_id.as_str().to_string()
294    }
295}
296
297impl<'a> std::fmt::Display for SuffixedRuleId<'a> {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        f.write_str(self.rule_id.as_str())?;
300        if let Some(suffix) = self.rule_id_suffix {
301            f.write_str(":")?;
302            f.write_str(suffix)?;
303        }
304        Ok(())
305    }
306}
307
308impl<'a> Serialize for SuffixedRuleId<'a> {
309    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
310    where
311        S: serde::Serializer,
312    {
313        serializer.collect_str(self)
314    }
315}