launchdarkly_server_sdk_evaluation/
attribute_value.rs

1use std::collections::HashMap;
2
3use chrono::{self, LocalResult, TimeZone, Utc};
4
5use lazy_static::lazy_static;
6use log::warn;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::util::f64_to_i64_safe;
12
13lazy_static! {
14    static ref VERSION_NUMERIC_COMPONENTS_REGEX: Regex =
15        Regex::new(r"^\d+(\.\d+)?(\.\d+)?").unwrap();
16}
17
18/// An attribute value represents possible values that can be stored in a [crate::Context].
19#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
20#[serde(untagged)]
21pub enum AttributeValue {
22    /// Stores a string value.
23    String(String),
24    /// Stores an array of attribute values.
25    Array(Vec<AttributeValue>),
26    /// Stores a number.
27    Number(f64),
28    /// Stores a boolean.
29    Bool(bool),
30    /// Stores a map of attribute values.
31    Object(HashMap<String, AttributeValue>),
32    /// Stores a null value.
33    Null,
34}
35
36impl From<&str> for AttributeValue {
37    fn from(s: &str) -> AttributeValue {
38        AttributeValue::String(s.to_owned())
39    }
40}
41
42impl From<String> for AttributeValue {
43    fn from(s: String) -> AttributeValue {
44        AttributeValue::String(s)
45    }
46}
47
48impl From<bool> for AttributeValue {
49    fn from(b: bool) -> AttributeValue {
50        AttributeValue::Bool(b)
51    }
52}
53
54impl From<i64> for AttributeValue {
55    fn from(i: i64) -> Self {
56        AttributeValue::Number(i as f64)
57    }
58}
59
60impl From<f64> for AttributeValue {
61    fn from(f: f64) -> Self {
62        AttributeValue::Number(f)
63    }
64}
65
66impl<T> From<Vec<T>> for AttributeValue
67where
68    AttributeValue: From<T>,
69{
70    fn from(v: Vec<T>) -> AttributeValue {
71        v.into_iter().collect()
72    }
73}
74
75impl<S, T> From<HashMap<S, T>> for AttributeValue
76where
77    String: From<S>,
78    AttributeValue: From<T>,
79{
80    fn from(hashmap: HashMap<S, T>) -> AttributeValue {
81        hashmap.into_iter().collect()
82    }
83}
84
85impl<T> FromIterator<T> for AttributeValue
86where
87    AttributeValue: From<T>,
88{
89    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
90        AttributeValue::Array(iter.into_iter().map(AttributeValue::from).collect())
91    }
92}
93
94impl<S, T> FromIterator<(S, T)> for AttributeValue
95where
96    String: From<S>,
97    AttributeValue: From<T>,
98{
99    fn from_iter<I: IntoIterator<Item = (S, T)>>(iter: I) -> Self {
100        AttributeValue::Object(
101            iter.into_iter()
102                .map(|(k, v)| (k.into(), v.into()))
103                .collect(),
104        )
105    }
106}
107
108impl From<&Value> for AttributeValue {
109    fn from(v: &Value) -> Self {
110        match v {
111            Value::Null => AttributeValue::Null,
112            Value::Bool(b) => AttributeValue::Bool(*b),
113            Value::Number(n) => match n.as_f64() {
114                Some(float) => AttributeValue::Number(float),
115                None => {
116                    warn!("could not interpret '{:?}' as f64", n);
117                    AttributeValue::String(n.to_string())
118                }
119            },
120            Value::String(str) => AttributeValue::String(str.clone()),
121            Value::Array(arr) => {
122                AttributeValue::Array(arr.iter().map(AttributeValue::from).collect())
123            }
124            Value::Object(obj) => {
125                AttributeValue::Object(obj.iter().map(|(k, v)| (k.into(), v.into())).collect())
126            }
127        }
128    }
129}
130
131impl AttributeValue {
132    /// Returns None unless self is a String. It will not convert.
133    pub fn as_str(&self) -> Option<&str> {
134        match self {
135            AttributeValue::String(s) => Some(s),
136            _ => None,
137        }
138    }
139
140    /// Returns the wrapped value as a float for numeric types, and None otherwise.
141    pub fn to_f64(&self) -> Option<f64> {
142        match self {
143            AttributeValue::Number(f) => Some(*f),
144            _ => None,
145        }
146    }
147
148    /// Returns None unless self is a bool. It will not convert.
149    pub fn as_bool(&self) -> Option<bool> {
150        match self {
151            AttributeValue::Bool(b) => Some(*b),
152            _ => None,
153        }
154    }
155
156    /// Attempt to convert any of the following into a chrono::DateTime in UTC:
157    ///
158    ///  * RFC3339/ISO8601 timestamp (example: "2016-04-16T17:09:12.759-07:00")
159    ///  * Unix epoch milliseconds as number
160    ///
161    /// It will return None if the conversion fails or if no conversion is possible.
162    pub fn to_datetime(&self) -> Option<chrono::DateTime<Utc>> {
163        match self {
164            AttributeValue::Number(millis) => {
165                f64_to_i64_safe(*millis).and_then(|millis| match Utc.timestamp_millis_opt(millis) {
166                    LocalResult::None | LocalResult::Ambiguous(_, _) => None,
167                    LocalResult::Single(time) => Some(time),
168                })
169            }
170            AttributeValue::String(s) => chrono::DateTime::parse_from_rfc3339(s)
171                .map(|dt| dt.with_timezone(&Utc))
172                .ok(),
173            AttributeValue::Bool(_) | AttributeValue::Null => None,
174            other => {
175                warn!(
176                    "Don't know how or whether to convert attribute value {:?} to datetime",
177                    other
178                );
179                None
180            }
181        }
182    }
183
184    /// Attempt to parse a string attribute into a semver version.
185    ///
186    /// It will return None if it cannot parse it, or for non-string attributes.
187    pub fn as_semver(&self) -> Option<semver::Version> {
188        let version_str = self.as_str()?;
189        semver::Version::parse(version_str)
190            .ok()
191            .or_else(|| AttributeValue::parse_semver_loose(version_str))
192            .map(|mut version| {
193                version.build = semver::BuildMetadata::EMPTY;
194                version
195            })
196    }
197
198    fn parse_semver_loose(version_str: &str) -> Option<semver::Version> {
199        let parts = VERSION_NUMERIC_COMPONENTS_REGEX.captures(version_str)?;
200
201        let numeric_parts = parts.get(0).unwrap();
202        let mut transformed_version_str = numeric_parts.as_str().to_string();
203
204        for i in 1..parts.len() {
205            if parts.get(i).is_none() {
206                transformed_version_str.push_str(".0");
207            }
208        }
209
210        let rest = &version_str[numeric_parts.end()..];
211        transformed_version_str.push_str(rest);
212
213        semver::Version::parse(&transformed_version_str).ok()
214    }
215
216    /// Find the AttributeValue based off the provided predicate `p`.
217    pub fn find<P>(&self, p: P) -> Option<&AttributeValue>
218    where
219        P: Fn(&AttributeValue) -> bool,
220    {
221        match self {
222            AttributeValue::String(_)
223            | AttributeValue::Number(_)
224            | AttributeValue::Bool(_)
225            | AttributeValue::Object(_) => {
226                if p(self) {
227                    Some(self)
228                } else {
229                    None
230                }
231            }
232            AttributeValue::Array(values) => values.iter().find(|v| p(v)),
233            AttributeValue::Null => None,
234        }
235    }
236
237    #[allow(clippy::float_cmp)]
238    pub(crate) fn as_bucketable(&self) -> Option<String> {
239        match self {
240            AttributeValue::String(s) => Some(s.clone()),
241            AttributeValue::Number(f) => {
242                // We only support integer values as bucketable
243                f64_to_i64_safe(*f).and_then(|i| {
244                    if i as f64 == *f {
245                        Some(i.to_string())
246                    } else {
247                        None
248                    }
249                })
250            }
251            _ => None,
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::AttributeValue;
259    use maplit::hashmap;
260
261    #[test]
262    fn collect_array() {
263        assert_eq!(
264            Some(10_i64).into_iter().collect::<AttributeValue>(),
265            AttributeValue::Array(vec![AttributeValue::Number(10_f64)])
266        );
267    }
268
269    #[test]
270    fn collect_object() {
271        assert_eq!(
272            Some(("abc", 10_i64))
273                .into_iter()
274                .collect::<AttributeValue>(),
275            AttributeValue::Object(hashmap! {"abc".to_string() => AttributeValue::Number(10_f64)})
276        );
277    }
278
279    #[test]
280    fn deserialization() {
281        fn test_case(json: &str, expected: AttributeValue) {
282            assert_eq!(
283                serde_json::from_str::<AttributeValue>(json).unwrap(),
284                expected
285            );
286        }
287
288        test_case("1.0", AttributeValue::Number(1.0));
289        test_case("1", AttributeValue::Number(1.0));
290        test_case("true", AttributeValue::Bool(true));
291        test_case("\"foo\"", AttributeValue::String("foo".to_string()));
292        test_case("{}", AttributeValue::Object(hashmap![]));
293        test_case(
294            r#"{"foo":123}"#,
295            AttributeValue::Object(hashmap!["foo".to_string() => AttributeValue::Number(123.0)]),
296        );
297    }
298}