launchdarkly_server_sdk_evaluation/
attribute_value.rs1use 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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
20#[serde(untagged)]
21pub enum AttributeValue {
22 String(String),
24 Array(Vec<AttributeValue>),
26 Number(f64),
28 Bool(bool),
30 Object(HashMap<String, AttributeValue>),
32 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 pub fn as_str(&self) -> Option<&str> {
134 match self {
135 AttributeValue::String(s) => Some(s),
136 _ => None,
137 }
138 }
139
140 pub fn to_f64(&self) -> Option<f64> {
142 match self {
143 AttributeValue::Number(f) => Some(*f),
144 _ => None,
145 }
146 }
147
148 pub fn as_bool(&self) -> Option<bool> {
150 match self {
151 AttributeValue::Bool(b) => Some(*b),
152 _ => None,
153 }
154 }
155
156 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 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 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 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}