Skip to main content

statsig_rust/user/
user_value.rs

1#[cfg(feature = "ordered_user_data_maps")]
2use ahash::RandomState as AHashState;
3#[cfg(feature = "ordered_user_data_maps")]
4use indexmap::IndexMap;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use serde_json::{
7    value::{to_raw_value, RawValue},
8    Number as JsonNumber, Value as JsonValue,
9};
10use std::borrow::Cow;
11#[cfg(not(feature = "ordered_user_data_maps"))]
12use std::collections::HashMap;
13
14use crate::{
15    hashing::ahash_str,
16    interned_string::InternedString,
17    log_w,
18    value_parsing::{maybe_parse_f64, maybe_parse_i64, try_parse_timestamp},
19    DynamicValue,
20};
21
22const TAG: &str = "UserValue";
23
24lazy_static::lazy_static! {
25    static ref TRUE_HASH: u64 = ahash_str("true");
26    static ref FALSE_HASH: u64 = ahash_str("false");
27}
28
29#[cfg(feature = "ordered_user_data_maps")]
30pub type UserValueMap = IndexMap<String, UserValue, AHashState>;
31#[cfg(not(feature = "ordered_user_data_maps"))]
32pub type UserValueMap = HashMap<String, UserValue>;
33
34#[derive(Clone, Debug, PartialEq)]
35pub struct UserString {
36    pub value: InternedString,
37    pub lowercased_value: InternedString,
38    pub int_value: Option<i64>,
39    pub float_value: Option<f64>,
40    pub timestamp_value: Option<i64>,
41}
42
43impl UserString {
44    pub fn new(value: String) -> Self {
45        let int_value = maybe_parse_i64(&value);
46        let float_value = maybe_parse_f64(&value);
47        let timestamp_value = try_parse_timestamp(&value, int_value);
48        let value = InternedString::from_string_uninterned(value);
49        let lowercased_value = if value.as_str().bytes().any(|byte| byte.is_ascii_uppercase()) {
50            InternedString::from_string_uninterned(value.as_str().to_lowercase())
51        } else {
52            value.clone()
53        };
54
55        Self {
56            int_value,
57            float_value,
58            timestamp_value,
59            value,
60            lowercased_value,
61        }
62    }
63}
64
65/// Eager, evaluation-ready representation for constructor-hot user fields.
66///
67/// This keeps Python-created `StatsigUser` data off the heavier `DynamicValue`
68/// path while preserving the same primitive coercions the evaluator relies on.
69#[derive(Clone, Debug)]
70pub enum UserValue {
71    Null,
72    Bool(bool),
73    Int {
74        value: i128,
75        string_value: String,
76    },
77    Float {
78        value: f64,
79        int_value: Option<i64>,
80        string_value: String,
81        hash_string: String,
82    },
83    String(UserString),
84    Array {
85        values: Vec<UserValue>,
86        string_value: UserString,
87    },
88    Object {
89        values: UserValueMap,
90        serialized_value: Box<RawValue>,
91    },
92}
93
94impl PartialEq for UserValue {
95    fn eq(&self, other: &Self) -> bool {
96        match (self, other) {
97            (Self::Null, Self::Null) => true,
98            (Self::Bool(left), Self::Bool(right)) => left == right,
99            (
100                Self::Int {
101                    value: left_value,
102                    string_value: left_string,
103                },
104                Self::Int {
105                    value: right_value,
106                    string_value: right_string,
107                },
108            ) => left_value == right_value && left_string == right_string,
109            (
110                Self::Float {
111                    value: left_value,
112                    int_value: left_int,
113                    string_value: left_string,
114                    hash_string: left_hash,
115                },
116                Self::Float {
117                    value: right_value,
118                    int_value: right_int,
119                    string_value: right_string,
120                    hash_string: right_hash,
121                },
122            ) => {
123                left_value == right_value
124                    && left_int == right_int
125                    && left_string == right_string
126                    && left_hash == right_hash
127            }
128            (Self::String(left), Self::String(right)) => left == right,
129            (
130                Self::Array {
131                    values: left_values,
132                    string_value: left_string,
133                },
134                Self::Array {
135                    values: right_values,
136                    string_value: right_string,
137                },
138            ) => left_values == right_values && left_string == right_string,
139            (
140                Self::Object {
141                    values: left_values,
142                    serialized_value: left_serialized,
143                },
144                Self::Object {
145                    values: right_values,
146                    serialized_value: right_serialized,
147                },
148            ) => left_values == right_values && left_serialized.get() == right_serialized.get(),
149            _ => false,
150        }
151    }
152}
153
154impl UserValue {
155    #[must_use]
156    pub fn new() -> Self {
157        Self::Null
158    }
159
160    #[must_use]
161    pub fn from_string(value: impl Into<String>) -> Self {
162        Self::String(UserString::new(value.into()))
163    }
164
165    #[must_use]
166    pub fn from_bool(value: bool) -> Self {
167        Self::Bool(value)
168    }
169
170    #[must_use]
171    pub fn from_i64(value: i64) -> Self {
172        Self::from_i128(value.into())
173    }
174
175    #[must_use]
176    pub fn from_u64(value: u64) -> Self {
177        Self::from_i128(value.into())
178    }
179
180    #[must_use]
181    fn from_i128(value: i128) -> Self {
182        Self::Int {
183            value,
184            string_value: value.to_string(),
185        }
186    }
187
188    #[must_use]
189    pub fn from_f64(value: f64) -> Self {
190        let num = match JsonNumber::from_f64(value) {
191            Some(num) => num,
192            None => {
193                log_w!(
194                    TAG,
195                    "Failed to convert f64 to serde_json::Number: {}",
196                    value
197                );
198                return Self::from_i64(value as i64);
199            }
200        };
201
202        let mut float_value = num.as_f64().unwrap_or(value);
203        let mut int_value = num.as_i64();
204        if let Some(i) = int_value {
205            float_value = i as f64;
206        } else {
207            let i = float_value as i64;
208            if i as f64 == float_value {
209                int_value = Some(i);
210            }
211        }
212
213        Self::Float {
214            value: float_value,
215            int_value,
216            string_value: float_value.to_string(),
217            hash_string: JsonValue::Number(num).to_string(),
218        }
219    }
220
221    #[must_use]
222    pub fn from_array(values: Vec<UserValue>) -> Self {
223        let serialized = serde_json::to_string(&values).unwrap_or_else(|_| "[]".to_string());
224        Self::Array {
225            values,
226            string_value: UserString::new(serialized),
227        }
228    }
229
230    #[must_use]
231    pub fn from_object(values: UserValueMap) -> Self {
232        let serialized_value = to_raw_value(&values).unwrap_or_else(|error| {
233            log_w!(TAG, "Failed to serialize object UserValue: {}", error);
234            to_raw_value(&JsonValue::Object(Default::default()))
235                .expect("empty JSON object should always serialize")
236        });
237        Self::Object {
238            values,
239            serialized_value,
240        }
241    }
242
243    #[must_use]
244    pub fn from_json_value(value: JsonValue) -> Self {
245        match value {
246            JsonValue::Null => Self::Null,
247            JsonValue::Bool(value) => Self::from_bool(value),
248            JsonValue::Number(value) => {
249                if let Some(value) = value.as_i64() {
250                    Self::from_i64(value)
251                } else if let Some(value) = value.as_u64() {
252                    Self::from_u64(value)
253                } else if let Some(value) = value.as_f64() {
254                    Self::from_f64(value)
255                } else {
256                    Self::Null
257                }
258            }
259            JsonValue::String(value) => Self::from_string(value),
260            JsonValue::Array(values) => {
261                Self::from_array(values.into_iter().map(Self::from_json_value).collect())
262            }
263            JsonValue::Object(values) => Self::from_object(
264                values
265                    .into_iter()
266                    .map(|(key, value)| (key, Self::from_json_value(value)))
267                    .collect(),
268            ),
269        }
270    }
271
272    #[must_use]
273    pub fn hash_value(&self) -> u64 {
274        match self {
275            Self::Null => 0,
276            Self::Bool(true) => *TRUE_HASH,
277            Self::Bool(false) => *FALSE_HASH,
278            Self::Int { string_value, .. } => ahash_str(string_value),
279            Self::Float { hash_string, .. } => ahash_str(hash_string),
280            Self::String(value)
281            | Self::Array {
282                string_value: value,
283                ..
284            } => ahash_str(value.value.as_str()),
285            Self::Object {
286                serialized_value, ..
287            } => ahash_str(serialized_value.get()),
288        }
289    }
290
291    #[must_use]
292    pub fn string_value(&self) -> Option<&str> {
293        match self {
294            Self::Null | Self::Object { .. } => None,
295            Self::Bool(true) => Some("true"),
296            Self::Bool(false) => Some("false"),
297            Self::Int { string_value, .. } | Self::Float { string_value, .. } => {
298                Some(string_value.as_str())
299            }
300            Self::String(value)
301            | Self::Array {
302                string_value: value,
303                ..
304            } => Some(value.value.as_str()),
305        }
306    }
307}
308
309impl Default for UserValue {
310    fn default() -> Self {
311        Self::new()
312    }
313}
314
315impl Serialize for UserValue {
316    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
317    where
318        S: Serializer,
319    {
320        match self {
321            Self::Null => serializer.serialize_unit(),
322            Self::Bool(value) => serializer.serialize_bool(*value),
323            Self::Int { value, .. } => {
324                if let Ok(value) = i64::try_from(*value) {
325                    serializer.serialize_i64(value)
326                } else if let Ok(value) = u64::try_from(*value) {
327                    serializer.serialize_u64(value)
328                } else {
329                    serializer.serialize_i128(*value)
330                }
331            }
332            Self::Float { value, .. } => serializer.serialize_f64(*value),
333            Self::String(value) => serializer.serialize_str(value.value.as_str()),
334            Self::Array { values, .. } => values.serialize(serializer),
335            Self::Object {
336                serialized_value, ..
337            } => serialized_value.serialize(serializer),
338        }
339    }
340}
341
342impl<'de> Deserialize<'de> for UserValue {
343    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
344    where
345        D: Deserializer<'de>,
346    {
347        Ok(Self::from_json_value(JsonValue::deserialize(deserializer)?))
348    }
349}
350
351impl From<JsonValue> for UserValue {
352    fn from(value: JsonValue) -> Self {
353        Self::from_json_value(value)
354    }
355}
356
357impl From<DynamicValue> for UserValue {
358    fn from(value: DynamicValue) -> Self {
359        Self::from_json_value(value.json_value)
360    }
361}
362
363impl From<String> for UserValue {
364    fn from(value: String) -> Self {
365        Self::from_string(value)
366    }
367}
368
369impl From<&str> for UserValue {
370    fn from(value: &str) -> Self {
371        Self::from_string(value)
372    }
373}
374
375impl From<i64> for UserValue {
376    fn from(value: i64) -> Self {
377        Self::from_i64(value)
378    }
379}
380
381impl From<i32> for UserValue {
382    fn from(value: i32) -> Self {
383        Self::from_i64(value as i64)
384    }
385}
386
387impl From<usize> for UserValue {
388    fn from(value: usize) -> Self {
389        Self::from_u64(value as u64)
390    }
391}
392
393impl From<f64> for UserValue {
394    fn from(value: f64) -> Self {
395        Self::from_f64(value)
396    }
397}
398
399impl From<bool> for UserValue {
400    fn from(value: bool) -> Self {
401        Self::from_bool(value)
402    }
403}
404
405#[derive(Clone, Copy)]
406/// Read-only bridge that lets evaluator code consume both legacy `DynamicValue`
407/// data and the faster `UserValue` representation without duplicating operators.
408pub enum UserValueRef<'a> {
409    Dynamic(&'a DynamicValue),
410    User(&'a UserValue),
411}
412
413impl<'a> From<&'a DynamicValue> for UserValueRef<'a> {
414    fn from(value: &'a DynamicValue) -> Self {
415        Self::Dynamic(value)
416    }
417}
418
419impl<'a> From<&'a UserValue> for UserValueRef<'a> {
420    fn from(value: &'a UserValue) -> Self {
421        Self::User(value)
422    }
423}
424
425impl<'a> UserValueRef<'a> {
426    pub fn is_null(self) -> bool {
427        match self {
428            Self::Dynamic(value) => value.null.is_some() || value.json_value.is_null(),
429            Self::User(UserValue::Null) => true,
430            Self::User(_) => false,
431        }
432    }
433
434    pub fn bool_value(self) -> Option<bool> {
435        match self {
436            Self::Dynamic(value) => value.bool_value,
437            Self::User(UserValue::Bool(value)) => Some(*value),
438            Self::User(_) => None,
439        }
440    }
441
442    pub fn int_value(self) -> Option<i64> {
443        match self {
444            Self::Dynamic(value) => value.int_value,
445            Self::User(UserValue::Int { value, .. }) => i64::try_from(*value).ok(),
446            Self::User(UserValue::Float { int_value, .. }) => *int_value,
447            Self::User(UserValue::String(value)) => value.int_value,
448            Self::User(_) => None,
449        }
450    }
451
452    pub fn float_value(self) -> Option<f64> {
453        match self {
454            Self::Dynamic(value) => value.float_value,
455            Self::User(UserValue::Int { value, .. }) => Some(*value as f64),
456            Self::User(UserValue::Float { value, .. }) => Some(*value),
457            Self::User(UserValue::String(value)) => value.float_value,
458            Self::User(_) => None,
459        }
460    }
461
462    pub fn timestamp_value(self) -> Option<i64> {
463        match self {
464            Self::Dynamic(value) => value.timestamp_value,
465            Self::User(UserValue::String(value)) => value.timestamp_value,
466            Self::User(_) => None,
467        }
468    }
469
470    pub fn string_value(self) -> Option<&'a str> {
471        match self {
472            Self::Dynamic(value) => value
473                .string_value
474                .as_ref()
475                .map(|value| value.value.as_str()),
476            Self::User(value) => value.string_value(),
477        }
478    }
479
480    pub fn lowercased_lookup_key(self) -> Option<&'a InternedString> {
481        match self {
482            Self::Dynamic(value) => value
483                .string_value
484                .as_ref()
485                .map(|value| &value.lowercased_value),
486            Self::User(UserValue::String(value)) => Some(&value.lowercased_value),
487            Self::User(UserValue::Array { string_value, .. }) => {
488                Some(&string_value.lowercased_value)
489            }
490            Self::User(_) => None,
491        }
492    }
493
494    pub fn lowercased_string_value(self) -> Option<Cow<'a, str>> {
495        match self {
496            Self::Dynamic(value) => value
497                .string_value
498                .as_ref()
499                .map(|value| Cow::Borrowed(value.lowercased_value.as_str())),
500            Self::User(UserValue::Bool(true)) => Some(Cow::Borrowed("true")),
501            Self::User(UserValue::Bool(false)) => Some(Cow::Borrowed("false")),
502            Self::User(UserValue::Int { string_value, .. }) => {
503                Some(Cow::Borrowed(string_value.as_str()))
504            }
505            Self::User(UserValue::Float { string_value, .. }) => {
506                Some(Cow::Borrowed(string_value.as_str()))
507            }
508            Self::User(UserValue::String(value)) => {
509                Some(Cow::Borrowed(value.lowercased_value.as_str()))
510            }
511            Self::User(UserValue::Array { string_value, .. }) => {
512                Some(Cow::Borrowed(string_value.lowercased_value.as_str()))
513            }
514            Self::User(UserValue::Null | UserValue::Object { .. }) => None,
515        }
516    }
517
518    pub fn serialized_value(self) -> Cow<'a, str> {
519        match self {
520            Self::Dynamic(value) => match &value.string_value {
521                Some(value) => Cow::Borrowed(value.value.as_str()),
522                None => Cow::Owned(value.json_value.to_string()),
523            },
524            Self::User(UserValue::Null) => Cow::Borrowed("null"),
525            Self::User(UserValue::Object {
526                serialized_value, ..
527            }) => Cow::Borrowed(serialized_value.get()),
528            Self::User(_) => Cow::Borrowed(self.string_value().unwrap_or_default()),
529        }
530    }
531
532    pub fn array_len(self) -> Option<usize> {
533        match self {
534            Self::Dynamic(value) => value.array_value.as_ref().map(Vec::len),
535            Self::User(UserValue::Array { values, .. }) => Some(values.len()),
536            Self::User(_) => None,
537        }
538    }
539
540    pub fn array_item(self, index: usize) -> Option<Self> {
541        match self {
542            Self::Dynamic(value) => value
543                .array_value
544                .as_ref()?
545                .get(index)
546                .map(UserValueRef::Dynamic),
547            Self::User(UserValue::Array { values, .. }) => {
548                values.get(index).map(UserValueRef::User)
549            }
550            Self::User(_) => None,
551        }
552    }
553
554    pub fn object_len(self) -> Option<usize> {
555        match self {
556            Self::Dynamic(value) => value.object_value.as_ref().map(|values| values.len()),
557            Self::User(UserValue::Object { values, .. }) => Some(values.len()),
558            Self::User(_) => None,
559        }
560    }
561
562    pub fn object_get(self, key: &str) -> Option<Self> {
563        match self {
564            Self::Dynamic(value) => value
565                .object_value
566                .as_ref()?
567                .get(key)
568                .map(UserValueRef::Dynamic),
569            Self::User(UserValue::Object { values, .. }) => values.get(key).map(UserValueRef::User),
570            Self::User(_) => None,
571        }
572    }
573}