Skip to main content

statsig_rust/user/
fast_statsig_user.rs

1use crate::statsig_metadata;
2use crate::{evaluation::dynamic_value::DynamicValue, hashing};
3use serde::{Deserialize, Serialize};
4use serde_with::skip_serializing_none;
5use std::{collections::HashMap, sync::Arc};
6
7use super::{
8    into_optional::IntoOptional,
9    unit_id::UnitID,
10    user_data::{UserData, UserDataMap, UserDataMapOf},
11    user_value::{UserValue, UserValueMap},
12};
13
14pub type FastUserCustomMap = UserValueMap;
15pub type FastUserUnitIDMap = UserDataMapOf<UserValue>;
16
17#[skip_serializing_none]
18#[derive(Clone, Deserialize, Serialize, Default)]
19#[serde(rename_all = "camelCase")]
20pub struct FastUserData {
21    #[serde(rename = "userID")]
22    pub user_id: Option<UserValue>,
23    #[serde(rename = "customIDs")]
24    pub custom_ids: Option<FastUserUnitIDMap>,
25
26    pub email: Option<UserValue>,
27    pub ip: Option<UserValue>,
28    pub user_agent: Option<UserValue>,
29    pub country: Option<UserValue>,
30    pub locale: Option<UserValue>,
31    pub app_version: Option<UserValue>,
32    pub statsig_environment: Option<UserDataMap>,
33
34    #[serde(skip_serializing)]
35    pub private_attributes: Option<UserDataMap>,
36    pub custom: Option<FastUserCustomMap>,
37}
38
39impl FastUserData {
40    pub(crate) fn create_exposure_dedupe_user_hash(&self, unit_id_type: Option<&str>) -> u64 {
41        let user_id_hash = self.user_id.as_ref().map_or(0, UserValue::hash_value);
42        let stable_id_hash = self.get_unit_id_hash("stableID");
43        let unit_type_hash = unit_id_type.map_or(0, |id_type| self.get_unit_id_hash(id_type));
44        let custom_ids_hash_sum = self.sum_custom_id_hashes();
45
46        hashing::hash_one(vec![
47            user_id_hash,
48            stable_id_hash,
49            unit_type_hash,
50            custom_ids_hash_sum,
51        ])
52    }
53
54    pub(crate) fn sum_custom_id_hashes(&self) -> u64 {
55        self.custom_ids.as_ref().map_or(0, |custom_ids| {
56            custom_ids
57                .values()
58                .fold(0u64, |acc, value| acc.wrapping_add(value.hash_value()))
59        })
60    }
61
62    pub(crate) fn get_unit_id_hash(&self, id_type: &str) -> u64 {
63        if id_type.eq_ignore_ascii_case("userid") {
64            return self.user_id.as_ref().map_or(0, UserValue::hash_value);
65        }
66
67        if let Some(custom_ids) = &self.custom_ids {
68            if let Some(id) = custom_ids.get(id_type) {
69                return id.hash_value();
70            }
71
72            if let Some(id) = custom_ids.get(&id_type.to_lowercase()) {
73                return id.hash_value();
74            }
75        }
76
77        0
78    }
79
80    pub(crate) fn to_bytes(&self) -> Option<Vec<u8>> {
81        serde_json::to_vec(self).ok()
82    }
83}
84
85#[derive(Clone)]
86pub struct FastStatsigUser {
87    pub data: Arc<FastUserData>,
88    pub(crate) sdk_version: &'static str,
89}
90
91impl FastStatsigUser {
92    pub fn new(inner: FastUserData) -> Self {
93        Self {
94            data: Arc::new(inner),
95            sdk_version: statsig_metadata::SDK_VERSION,
96        }
97    }
98
99    pub fn get_user_id(&self) -> Option<&str> {
100        self.data.user_id.as_ref().and_then(UserValue::string_value)
101    }
102
103    pub fn set_user_id(&mut self, user_id: impl Into<UnitID>) {
104        let unit_id = user_id.into();
105        let mut_data = Arc::make_mut(&mut self.data);
106        mut_data.user_id = Some(UserValue::from(unit_id));
107    }
108
109    pub fn get_custom_ids(&self) -> Option<HashMap<&str, &str>> {
110        let mapped = self
111            .data
112            .custom_ids
113            .as_ref()?
114            .iter()
115            .map(|(key, value)| (key.as_str(), value.string_value().unwrap_or("")))
116            .collect();
117
118        Some(mapped)
119    }
120
121    pub fn set_custom_ids<K, U>(&mut self, custom_ids: HashMap<K, U>)
122    where
123        K: Into<String>,
124        U: Into<UnitID>,
125    {
126        let custom_ids = custom_ids
127            .into_iter()
128            .map(|(key, value)| (key.into(), UserValue::from(value.into())))
129            .collect();
130
131        let mut_data = Arc::make_mut(&mut self.data);
132        mut_data.custom_ids = Some(custom_ids);
133    }
134
135    pub(crate) fn get_unit_id_by_name(
136        &self,
137        id_type: &str,
138        lowercased_id_type: &str,
139    ) -> Option<&UserValue> {
140        if lowercased_id_type == "userid" {
141            return self.data.user_id.as_ref();
142        }
143
144        let custom_ids = self.data.custom_ids.as_ref()?;
145
146        if let Some(custom_id) = custom_ids.get(id_type) {
147            return Some(custom_id);
148        }
149
150        custom_ids.get(lowercased_id_type)
151    }
152
153    pub fn get_statsig_environment(&self) -> Option<HashMap<&str, &str>> {
154        let mapped = self
155            .data
156            .statsig_environment
157            .as_ref()?
158            .iter()
159            .map(dynamic_entry_to_key_value_refs)
160            .collect();
161
162        Some(mapped)
163    }
164
165    pub fn set_statsig_environment<K, U>(&mut self, statsig_environment: Option<HashMap<K, U>>)
166    where
167        K: Into<String>,
168        U: Into<String>,
169    {
170        let mut_data = Arc::make_mut(&mut self.data);
171        let statsig_environment = match statsig_environment {
172            Some(value) => value,
173            None => {
174                mut_data.statsig_environment = None;
175                return;
176            }
177        };
178
179        let statsig_environment: UserDataMap = statsig_environment
180            .into_iter()
181            .map(|(key, value)| (key.into(), value.into().into()))
182            .collect();
183
184        mut_data.statsig_environment = Some(statsig_environment);
185    }
186
187    pub fn get_custom(&self) -> Option<&FastUserCustomMap> {
188        self.data.custom.as_ref()
189    }
190
191    pub fn set_custom<K, V>(&mut self, value: impl IntoOptional<HashMap<K, V>>)
192    where
193        K: Into<String>,
194        V: Into<UserValue>,
195    {
196        let mut_data = Arc::make_mut(&mut self.data);
197        let value = match value.into_optional() {
198            Some(value) => value,
199            None => {
200                mut_data.custom = None;
201                return;
202            }
203        };
204
205        mut_data.custom = Some(
206            value
207                .into_iter()
208                .map(|(key, value)| (key.into(), value.into()))
209                .collect(),
210        );
211    }
212
213    pub fn get_private_attributes(&self) -> Option<&UserDataMap> {
214        self.data.private_attributes.as_ref()
215    }
216
217    pub fn set_private_attributes<K, V>(&mut self, value: impl IntoOptional<HashMap<K, V>>)
218    where
219        K: Into<String>,
220        V: Into<DynamicValue>,
221    {
222        let mut_data = Arc::make_mut(&mut self.data);
223        let value = match value.into_optional() {
224            Some(value) => value,
225            None => {
226                mut_data.private_attributes = None;
227                return;
228            }
229        };
230
231        mut_data.private_attributes = Some(
232            value
233                .into_iter()
234                .map(|(key, value)| (key.into(), value.into()))
235                .collect(),
236        );
237    }
238
239    pub fn to_public_user(&self) -> crate::StatsigUser {
240        crate::StatsigUser::new(self.to_public_user_data())
241    }
242
243    pub(crate) fn to_public_user_data(&self) -> UserData {
244        UserData {
245            user_id: self.data.user_id.as_ref().map(user_value_to_dynamic_value),
246            custom_ids: self
247                .data
248                .custom_ids
249                .as_ref()
250                .map(user_value_map_to_dynamic_value_map),
251            email: self.data.email.as_ref().map(user_value_to_dynamic_value),
252            ip: self.data.ip.as_ref().map(user_value_to_dynamic_value),
253            user_agent: self
254                .data
255                .user_agent
256                .as_ref()
257                .map(user_value_to_dynamic_value),
258            country: self.data.country.as_ref().map(user_value_to_dynamic_value),
259            locale: self.data.locale.as_ref().map(user_value_to_dynamic_value),
260            app_version: self
261                .data
262                .app_version
263                .as_ref()
264                .map(user_value_to_dynamic_value),
265            statsig_environment: self.data.statsig_environment.clone(),
266            private_attributes: self.data.private_attributes.clone(),
267            custom: self.data.custom.as_ref().map(|values| {
268                values
269                    .iter()
270                    .map(|(key, value)| (key.clone(), user_value_to_dynamic_value(value)))
271                    .collect()
272            }),
273        }
274    }
275}
276
277macro_rules! string_field_accessor {
278    ($getter_name:ident, $setter_name:ident, $field:ident) => {
279        pub fn $getter_name(&self) -> Option<&str> {
280            self.data.$field.as_ref().and_then(UserValue::string_value)
281        }
282
283        pub fn $setter_name(&mut self, value: impl IntoOptional<String>) {
284            let value = value.into_optional();
285            let mut_data = Arc::make_mut(&mut self.data);
286            match value {
287                Some(value) => {
288                    mut_data.$field = Some(UserValue::from(value));
289                }
290                None => mut_data.$field = None,
291            }
292        }
293    };
294}
295
296impl FastStatsigUser {
297    string_field_accessor!(get_email, set_email, email);
298    string_field_accessor!(get_ip, set_ip, ip);
299    string_field_accessor!(get_user_agent, set_user_agent, user_agent);
300    string_field_accessor!(get_country, set_country, country);
301    string_field_accessor!(get_locale, set_locale, locale);
302    string_field_accessor!(get_app_version, set_app_version, app_version);
303}
304
305impl FastStatsigUser {
306    pub fn to_bytes(&self) -> Option<Vec<u8>> {
307        self.data.to_bytes()
308    }
309}
310
311fn dynamic_entry_to_key_value_refs<'a>(
312    entry: (&'a String, &'a DynamicValue),
313) -> (&'a str, &'a str) {
314    let (key, value) = entry;
315
316    (
317        key.as_str(),
318        value
319            .string_value
320            .as_ref()
321            .map(|value| value.value.as_str())
322            .unwrap_or(""),
323    )
324}
325
326fn user_value_to_dynamic_value(value: &UserValue) -> DynamicValue {
327    DynamicValue::from_json_value(value)
328}
329
330fn user_value_map_to_dynamic_value_map(values: &UserDataMapOf<UserValue>) -> UserDataMap {
331    values
332        .iter()
333        .map(|(key, value)| (key.clone(), user_value_to_dynamic_value(value)))
334        .collect()
335}