statsig_rust/evaluation/
dynamic_value.rs

1use chrono::{DateTime, NaiveDateTime};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use serde_json::{json, Value as JsonValue};
4use std::{collections::HashMap, fmt::Debug};
5
6use crate::hashing::ahash_str;
7
8use super::dynamic_string::DynamicString;
9
10#[macro_export]
11macro_rules! dyn_value {
12    ($x:expr) => {{
13        $crate::DynamicValue::from_json_value($x)
14    }};
15}
16
17#[derive(Debug, Clone, Default)]
18pub struct DynamicValue {
19    pub null: Option<()>,
20    pub bool_value: Option<bool>,
21    pub int_value: Option<i64>,
22    pub float_value: Option<f64>,
23    pub timestamp_value: Option<i64>,
24    pub string_value: Option<DynamicString>,
25    pub array_value: Option<Vec<DynamicValue>>,
26    pub object_value: Option<HashMap<String, DynamicValue>>,
27    pub json_value: JsonValue,
28    pub hash_value: u64,
29}
30
31impl DynamicValue {
32    #[must_use]
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    #[must_use]
38    pub fn from_json_value(value: impl Serialize) -> Self {
39        Self::from(json!(value))
40    }
41
42    #[must_use]
43    pub fn for_timestamp_evaluation(timestamp: i64) -> DynamicValue {
44        DynamicValue {
45            int_value: Some(timestamp),
46            ..DynamicValue::default()
47        }
48    }
49
50    fn try_parse_timestamp(s: &str) -> Option<i64> {
51        // Fast-path: try parsing as integer first
52        if let Ok(ts) = s.parse::<i64>() {
53            return Some(ts);
54        }
55
56        // Fast-reject: if the string is out of range or lacks typical date delimiters
57        if s.len() < 8 || s.len() > 20 || (!s.contains('-') && !s.contains('T') && !s.contains(':'))
58        {
59            return None;
60        }
61
62        // Try RFC3339 (e.g. "2023-01-01T12:00:00Z")
63        if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
64            return Some(dt.timestamp_millis());
65        }
66
67        // Try common datetime format (e.g. "2023-01-01 12:00:00")
68        if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
69            return Some(ndt.and_utc().timestamp_millis());
70        }
71
72        None
73    }
74}
75
76impl PartialEq for DynamicValue {
77    fn eq(&self, other: &Self) -> bool {
78        self.null == other.null
79            && self.bool_value == other.bool_value
80            && self.int_value == other.int_value
81            && self.float_value == other.float_value
82            && self.string_value == other.string_value
83            && self.array_value == other.array_value
84            && self.object_value == other.object_value
85    }
86}
87
88// ------------------------------------------------------------------------------- [Serialization]
89
90impl Serialize for DynamicValue {
91    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
92    where
93        S: Serializer,
94    {
95        self.json_value.serialize(serializer)
96    }
97}
98
99impl<'de> Deserialize<'de> for DynamicValue {
100    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
101    where
102        D: Deserializer<'de>,
103    {
104        let json_value = JsonValue::deserialize(deserializer)?;
105        Ok(DynamicValue::from(json_value))
106    }
107}
108
109// ------------------------------------------------------------------------------- [From<T> Implementations]
110
111impl From<JsonValue> for DynamicValue {
112    fn from(json_value: JsonValue) -> Self {
113        // perf optimization: avoid stringifying the json value if it's already a string
114        let mut stringified_json_value = None;
115        let hash_value = if let JsonValue::String(s) = &json_value {
116            ahash_str(s)
117        } else {
118            let actual = json_value.to_string();
119            let hash = ahash_str(&actual);
120            stringified_json_value = Some(actual);
121            hash
122        };
123
124        match &json_value {
125            JsonValue::Null => DynamicValue {
126                null: Some(()),
127                json_value,
128                hash_value,
129                ..DynamicValue::new()
130            },
131
132            JsonValue::Bool(b) => DynamicValue {
133                bool_value: Some(*b),
134                string_value: Some(DynamicString::from(b.to_string())),
135                json_value,
136                hash_value,
137                ..DynamicValue::new()
138            },
139
140            JsonValue::Number(n) => {
141                let mut float_value = n.as_f64();
142                let mut int_value = n.as_i64();
143                if let (Some(f), None) = (float_value, int_value) {
144                    let iv = f as i64;
145                    if iv as f64 == f {
146                        int_value = Some(iv);
147                    }
148                } else if let (None, Some(i)) = (float_value, int_value) {
149                    let fv = i as f64;
150                    if fv as i64 == i {
151                        float_value = Some(fv)
152                    }
153                }
154
155                let string_value = float_value
156                    .map(|f| f.to_string())
157                    .or_else(|| int_value.map(|i| i.to_string()))
158                    .or(stringified_json_value);
159
160                DynamicValue {
161                    float_value,
162                    int_value,
163                    string_value: string_value.map(DynamicString::from),
164                    json_value,
165                    hash_value,
166                    ..DynamicValue::new()
167                }
168            }
169
170            JsonValue::String(s) => {
171                let timestamp_value = Self::try_parse_timestamp(s);
172                let float_value = s.parse().ok();
173                let int_value = s.parse().ok();
174                DynamicValue {
175                    string_value: Some(DynamicString::from(s.clone())),
176                    json_value,
177                    timestamp_value,
178                    int_value,
179                    float_value,
180                    hash_value,
181                    ..DynamicValue::new()
182                }
183            }
184
185            JsonValue::Array(arr) => DynamicValue {
186                array_value: Some(arr.iter().map(|v| DynamicValue::from(v.clone())).collect()),
187                string_value: Some(DynamicString::from(
188                    stringified_json_value.unwrap_or(json_value.to_string()),
189                )),
190                json_value,
191                hash_value,
192                ..DynamicValue::new()
193            },
194
195            JsonValue::Object(obj) => DynamicValue {
196                object_value: Some(
197                    obj.into_iter()
198                        .map(|(k, v)| (k.clone(), DynamicValue::from(v.clone())))
199                        .collect(),
200                ),
201                json_value,
202                hash_value,
203                ..DynamicValue::new()
204            },
205        }
206    }
207}
208
209impl From<String> for DynamicValue {
210    fn from(value: String) -> Self {
211        Self::from(json!(value))
212    }
213}
214
215impl From<&str> for DynamicValue {
216    fn from(value: &str) -> Self {
217        Self::from(json!(value))
218    }
219}
220
221impl From<usize> for DynamicValue {
222    fn from(value: usize) -> Self {
223        Self::from(json!(value))
224    }
225}
226
227impl From<i64> for DynamicValue {
228    fn from(value: i64) -> Self {
229        Self::from(json!(value))
230    }
231}
232
233impl From<i32> for DynamicValue {
234    fn from(value: i32) -> Self {
235        Self::from(json!(value))
236    }
237}
238
239impl From<f64> for DynamicValue {
240    fn from(value: f64) -> Self {
241        Self::from(json!(value))
242    }
243}
244
245impl From<bool> for DynamicValue {
246    fn from(value: bool) -> Self {
247        Self::from(json!(value))
248    }
249}
250
251impl From<Vec<JsonValue>> for DynamicValue {
252    fn from(value: Vec<JsonValue>) -> Self {
253        DynamicValue::from(json!(value))
254    }
255}