statsig_rust/user/
statsig_user_loggable.rs

1use super::user_data::UserData;
2use crate::DynamicValue;
3use serde::{ser::SerializeStruct, Deserialize, Serialize};
4use serde_json::Value;
5use std::{collections::HashMap, sync::Arc};
6
7const TAG: &str = "StatsigUserLoggable";
8
9#[derive(Clone, Default)]
10pub struct StatsigUserLoggable {
11    pub data: Arc<UserData>,
12    pub environment: Option<HashMap<String, DynamicValue>>,
13    pub global_custom: Option<HashMap<String, DynamicValue>>,
14}
15
16impl StatsigUserLoggable {
17    pub fn new(
18        user_inner: &Arc<UserData>,
19        environment: Option<HashMap<String, DynamicValue>>,
20        global_custom: Option<HashMap<String, DynamicValue>>,
21    ) -> Self {
22        Self {
23            data: user_inner.clone(),
24            environment,
25            global_custom,
26        }
27    }
28
29    pub fn null() -> Self {
30        Self::default()
31    }
32}
33
34// ----------------------------------------------------- [Serialization]
35
36impl Serialize for StatsigUserLoggable {
37    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
38    where
39        S: serde::Serializer,
40    {
41        let mut state = serializer.serialize_struct(TAG, 10)?;
42
43        let data = self.data.as_ref();
44        serialize_data_field(&mut state, "userID", &data.user_id)?;
45        serialize_data_field(&mut state, "customIDs", &data.custom_ids)?;
46        serialize_data_field(&mut state, "email", &data.email)?;
47        serialize_data_field(&mut state, "ip", &data.ip)?;
48        serialize_data_field(&mut state, "userAgent", &data.user_agent)?;
49        serialize_data_field(&mut state, "country", &data.country)?;
50        serialize_data_field(&mut state, "locale", &data.locale)?;
51        serialize_data_field(&mut state, "appVersion", &data.app_version)?;
52
53        serialize_custom_field(&mut state, &data.custom, &self.global_custom)?;
54        serialize_data_field(&mut state, "statsigEnvironment", &self.environment)?;
55
56        // DO NOT SERIALIZE "privateAttributes"
57
58        state.end()
59    }
60}
61
62impl<'de> Deserialize<'de> for StatsigUserLoggable {
63    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
64    where
65        D: serde::Deserializer<'de>,
66    {
67        let mut value = Value::deserialize(deserializer)?;
68        let env = value["statsigEnvironment"].take();
69        let data = serde_json::from_value::<UserData>(value).map_err(|e| {
70            serde::de::Error::custom(format!("Error deserializing StatsigUserInner: {e}"))
71        })?;
72
73        let environment = serde_json::from_value::<Option<HashMap<String, DynamicValue>>>(env)
74            .map_err(|e| {
75                serde::de::Error::custom(format!("Error deserializing StatsigUserInner: {e}"))
76            })?;
77
78        Ok(StatsigUserLoggable {
79            data: Arc::new(data),
80            environment,
81            global_custom: None, // there is no way to discern between user-defined and global custom fields
82        })
83    }
84}
85
86fn serialize_data_field<S, T>(
87    state: &mut S,
88    field: &'static str,
89    value: &Option<T>,
90) -> Result<(), S::Error>
91where
92    S: SerializeStruct,
93    T: Serialize,
94{
95    if let Some(value) = value {
96        state.serialize_field(field, value)?;
97    }
98    Ok(())
99}
100
101fn serialize_custom_field<S>(
102    state: &mut S,
103    custom: &Option<HashMap<String, DynamicValue>>,
104    global_custom: &Option<HashMap<String, DynamicValue>>,
105) -> Result<(), S::Error>
106where
107    S: SerializeStruct,
108{
109    if global_custom.is_none() && custom.is_none() {
110        return Ok(());
111    }
112
113    let mut map = HashMap::new();
114
115    if let Some(global_custom) = global_custom.as_ref() {
116        for (k, v) in global_custom {
117            map.insert(k, v);
118        }
119    }
120
121    if let Some(custom) = custom.as_ref() {
122        for (k, v) in custom {
123            map.insert(k, v);
124        }
125    }
126
127    serialize_data_field(state, "custom", &Some(map))
128}