Skip to main content

launchdarkly_server_sdk_evaluation/
flag_value.rs

1use log::warn;
2use serde::{Deserialize, Serialize};
3
4use crate::util::f64_to_i64_safe;
5
6/// FlagValue represents any of the data types supported by JSON, all of which can be used for a
7/// LaunchDarkly feature flag variation or a custom context attribute.
8#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
9#[serde(untagged)]
10pub enum FlagValue {
11    /// Used when the value is a boolean.
12    Bool(bool),
13    /// Used when the value is a string.
14    Str(String),
15    /// Used when the value is a number.
16    Number(f64),
17    /// Used when the value is an arbitrary JSON value.
18    Json(serde_json::Value),
19}
20
21impl From<bool> for FlagValue {
22    fn from(b: bool) -> FlagValue {
23        FlagValue::Bool(b)
24    }
25}
26
27impl From<String> for FlagValue {
28    fn from(s: String) -> FlagValue {
29        FlagValue::Str(s)
30    }
31}
32
33impl From<f64> for FlagValue {
34    fn from(f: f64) -> FlagValue {
35        FlagValue::Number(f)
36    }
37}
38
39impl From<i64> for FlagValue {
40    fn from(i: i64) -> FlagValue {
41        FlagValue::Number(i as f64)
42    }
43}
44
45impl From<serde_json::Value> for FlagValue {
46    fn from(v: serde_json::Value) -> Self {
47        use serde_json::Value;
48        match v {
49            Value::Bool(b) => b.into(),
50            Value::Number(n) => {
51                if let Some(f) = n.as_f64() {
52                    f.into()
53                } else {
54                    warn!("unrepresentable number {n}, converting to string");
55                    FlagValue::Json(format!("{n}").into())
56                }
57            }
58            Value::String(s) => s.into(),
59            Value::Null | Value::Object(_) | Value::Array(_) => FlagValue::Json(v),
60        }
61    }
62}
63
64impl FlagValue {
65    /// Attempts to convert the FlagValue into a boolean representation, returning None if the
66    /// conversion is invalid.
67    pub fn as_bool(&self) -> Option<bool> {
68        match self {
69            FlagValue::Bool(b) => Some(*b),
70            _ => {
71                warn!("variation type is not bool but {self:?}");
72                None
73            }
74        }
75    }
76
77    /// Attempts to convert the FlagValue into a string representation, returning None if the
78    /// conversion is invalid.
79    pub fn as_string(&self) -> Option<String> {
80        match self {
81            FlagValue::Str(s) => Some(s.clone()),
82            _ => {
83                warn!("variation type is not str but {self:?}");
84                None
85            }
86        }
87    }
88
89    /// Attempts to convert the FlagValue into a float representation, returning None if the
90    /// conversion is invalid.
91    pub fn as_float(&self) -> Option<f64> {
92        match self {
93            FlagValue::Number(f) => Some(*f),
94            _ => {
95                warn!("variation type is not number but {self:?}");
96                None
97            }
98        }
99    }
100
101    /// Attempts to convert the FlagValue into a integer representation, returning None if the
102    /// conversion is invalid.
103    pub fn as_int(&self) -> Option<i64> {
104        match self {
105            FlagValue::Number(f) => f64_to_i64_safe(*f),
106            _ => {
107                warn!("variation type is not number but {self:?}");
108                None
109            }
110        }
111    }
112
113    /// Attempts to convert the FlagValue into an arbitrary JSON representation, returning None if the
114    /// conversion is invalid.
115    pub fn as_json(&self) -> Option<serde_json::Value> {
116        use serde_json::Value;
117        match self {
118            FlagValue::Bool(b) => Some(Value::from(*b)),
119            FlagValue::Str(s) => Some(Value::from(s.as_str())),
120            FlagValue::Number(f) => Some(Value::from(*f)),
121            FlagValue::Json(v) => Some(v.clone()),
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use serde_json::json;
130    use spectral::prelude::*;
131
132    #[test]
133    fn float_bounds() {
134        let test_cases = vec![
135            (1.99, Some(1)),
136            (9007199254740990.0, Some(9007199254740990)),
137            (9007199254740991.0, Some(9007199254740991)),
138            (9007199254740992.0, None),
139            (-1.99, Some(-1)),
140            (-9007199254740990.0, Some(-9007199254740990)),
141            (-9007199254740991.0, Some(-9007199254740991)),
142            (-9007199254740992.0, None),
143        ];
144        for (have, expect) in test_cases {
145            assert_that!(FlagValue::Number(have).as_int()).is_equal_to(expect);
146        }
147    }
148
149    #[test]
150    fn deserialization() {
151        fn test_case(json: &str, expected: FlagValue) {
152            assert_eq!(serde_json::from_str::<FlagValue>(json).unwrap(), expected);
153        }
154
155        test_case("1.0", FlagValue::Number(1.0));
156        test_case("1", FlagValue::Number(1.0));
157        test_case("true", FlagValue::Bool(true));
158        test_case("\"foo\"", FlagValue::Str("foo".to_string()));
159        test_case("{}", FlagValue::Json(json!({})));
160    }
161
162    // Fractional numbers must deserialize to the same f64 that Go's encoding/json produces, so
163    // that a numeric flag value evaluates identically across LaunchDarkly SDKs. This requires
164    // serde_json's correctly-rounded parser, which the float-roundtrip feature selects.
165    #[cfg(feature = "float-roundtrip")]
166    #[test]
167    fn fractional_deserialization_matches_go() {
168        fn test_case(json: &str, expected: f64) {
169            assert_eq!(
170                serde_json::from_str::<FlagValue>(json).unwrap(),
171                FlagValue::Number(expected)
172            );
173        }
174
175        test_case("130.65331632653061", 130.65331632653061);
176        test_case("130.65331632653062", 130.65331632653061);
177        test_case("130.65331632653063", 130.65331632653064);
178    }
179
180    #[test]
181    fn can_handle_converting_between_types() {
182        let value: FlagValue = true.into();
183        assert_eq!(Some(true), value.as_bool());
184        assert!(value.as_string().is_none());
185        assert!(value.as_float().is_none());
186        assert!(value.as_float().is_none());
187        assert!(value.as_int().is_none());
188
189        let value: FlagValue = String::from("testing").into();
190        assert!(value.as_bool().is_none());
191        assert_eq!(Some(String::from("testing")), value.as_string());
192        assert!(value.as_float().is_none());
193        assert!(value.as_float().is_none());
194        assert!(value.as_int().is_none());
195
196        let value: FlagValue = 1_f64.into();
197        assert!(value.as_bool().is_none());
198        assert!(value.as_string().is_none());
199        assert_eq!(Some(1_f64), value.as_float());
200        assert_eq!(Some(1_i64), value.as_int());
201
202        let value: FlagValue = 1_i64.into();
203        assert!(value.as_bool().is_none());
204        assert!(value.as_string().is_none());
205        assert_eq!(Some(1_f64), value.as_float());
206        assert_eq!(Some(1_i64), value.as_int());
207
208        let value: FlagValue = serde_json::Value::Bool(true).into();
209        assert_eq!(Some(true), value.as_bool());
210        assert_eq!(Some(serde_json::Value::Bool(true)), value.as_json());
211
212        let value: FlagValue = serde_json::Value::String("testing".to_string()).into();
213        assert_eq!(Some(String::from("testing")), value.as_string());
214        assert_eq!(
215            Some(serde_json::Value::String("testing".to_string())),
216            value.as_json()
217        );
218
219        let value: FlagValue = json!(1_f64).into();
220        assert_eq!(Some(1_f64), value.as_float());
221        assert_eq!(Some(json!(1_f64)), value.as_json());
222
223        let value: FlagValue = serde_json::Value::Array(vec![serde_json::Value::Bool(true)]).into();
224        assert_eq!(
225            Some(serde_json::Value::Array(vec![serde_json::Value::Bool(
226                true
227            )])),
228            value.as_json()
229        );
230    }
231}