1use bon::bon;
2use hex;
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::collections::HashMap;
6use validator::Validate;
7
8#[derive(Debug, Clone, Serialize, Deserialize, Validate, Default)]
9pub struct User {
10 #[serde(skip_serializing_if = "Option::is_none")]
11 #[validate(length(
12 min = 1,
13 max = 100,
14 message = "userID must be between 1 and 100 characters"
15 ))]
16 #[serde(rename = "userID")]
17 pub user_id: Option<String>,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
20 #[validate(email(message = "Invalid email format"))]
21 pub email: Option<String>,
22
23 #[serde(skip_serializing_if = "Option::is_none")]
24 #[validate(length(min = 7, max = 45, message = "Invalid IP address format"))]
25 pub ip: Option<String>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
28 #[serde(rename = "userAgent")]
29 pub user_agent: Option<String>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
32 #[validate(length(min = 2, max = 2, message = "Country must be a 2-letter ISO code"))]
33 pub country: Option<String>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub locale: Option<String>,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
39 #[serde(rename = "appVersion")]
40 pub app_version: Option<String>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub custom: Option<HashMap<String, serde_json::Value>>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
46 #[serde(rename = "privateAttributes")]
47 pub private_attributes: Option<HashMap<String, serde_json::Value>>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
50 #[serde(rename = "customIDs")]
51 pub custom_ids: Option<HashMap<String, String>>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
54 #[serde(rename = "statsigEnvironment")]
55 pub statsig_environment: Option<StatsigEnvironment>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
59pub struct StatsigEnvironment {
60 #[serde(rename = "tier")]
61 #[validate(custom(function = "validate_tier"))]
62 pub tier: EnvironmentTier,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(rename_all = "lowercase")]
67pub enum EnvironmentTier {
68 Production,
69 Staging,
70 Development,
71}
72
73fn validate_tier(tier: &EnvironmentTier) -> Result<(), validator::ValidationError> {
74 match tier {
75 EnvironmentTier::Production | EnvironmentTier::Staging | EnvironmentTier::Development => {
76 Ok(())
77 }
78 }
79}
80
81#[bon]
82impl User {
83 #[builder]
84 pub fn new(
85 #[builder(into)] user_id: Option<String>,
86 #[builder(into)] email: Option<String>,
87 #[builder(into)] ip: Option<String>,
88 #[builder(into)] user_agent: Option<String>,
89 #[builder(into)] country: Option<String>,
90 #[builder(into)] locale: Option<String>,
91 #[builder(into)] app_version: Option<String>,
92 #[builder(with = |iter: impl IntoIterator<Item = (impl Into<String>, serde_json::Value)>| {
93 iter.into_iter().map(|(k, v)| (k.into(), v)).collect()
94 })]
95 custom: Option<HashMap<String, serde_json::Value>>,
96 #[builder(with = |iter: impl IntoIterator<Item = (impl Into<String>, serde_json::Value)>| {
97 iter.into_iter().map(|(k, v)| (k.into(), v)).collect()
98 })]
99 private_attributes: Option<HashMap<String, serde_json::Value>>,
100 #[builder(with = |iter: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>| {
101 iter.into_iter().map(|(k, v)| (k.into(), v.into())).collect()
102 })]
103 custom_ids: Option<HashMap<String, String>>,
104 statsig_environment: Option<StatsigEnvironment>,
105 ) -> crate::error::Result<Self> {
106 let user = Self {
107 user_id,
108 email,
109 ip,
110 user_agent,
111 country,
112 locale,
113 app_version,
114 custom,
115 private_attributes,
116 custom_ids,
117 statsig_environment,
118 };
119
120 user.validate().map_err(|e| {
121 crate::error::StatsigError::user_validation(format!("User validation failed: {e}"))
122 })?;
123
124 Ok(user)
125 }
126}
127
128impl User {
129 pub fn validate_user(&self) -> crate::error::Result<()> {
130 self.validate().map_err(|e| {
131 crate::error::StatsigError::user_validation(format!("User validation failed: {e}"))
132 })
133 }
134
135 pub fn with_user_id(user_id: impl Into<String>) -> UserBuilder<user_builder::SetUserId> {
139 Self::builder().user_id(user_id)
140 }
141
142 pub fn get_primary_id(&self) -> Option<&str> {
143 self.user_id
144 .as_deref()
145 .or(self.email.as_deref())
146 .or_else(|| {
147 self.custom_ids
148 .as_ref()
149 .and_then(|ids| ids.values().next().map(|s| s.as_str()))
150 })
151 }
152
153 pub fn user_id(&self) -> Option<&str> {
155 self.user_id.as_deref()
156 }
157
158 pub fn hash_for_cache(&self) -> String {
163 let mut hasher = Sha256::new();
164
165 if let Some(user_id) = &self.user_id {
166 hasher.update(user_id.as_bytes());
167 }
168 if let Some(email) = &self.email {
169 hasher.update(email.as_bytes());
170 }
171 if let Some(custom_ids) = &self.custom_ids {
172 for (key, value) in custom_ids {
173 hasher.update(key.as_bytes());
174 hasher.update(value.as_bytes());
175 }
176 }
177
178 hex::encode(hasher.finalize())
179 }
180}
181
182impl Default for StatsigEnvironment {
183 fn default() -> Self {
184 Self {
185 tier: EnvironmentTier::Development,
186 }
187 }
188}