firebase_rs_sdk/remote_config/
value.rs

1//! Remote Config value helpers mirroring the Firebase JS SDK implementation.
2//!
3//! Based on the logic in `packages/remote-config/src/value.ts` which normalises
4//! Remote Config parameter values and exposes typed accessors.
5
6/// Indicates where a Remote Config value originated from.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum RemoteConfigValueSource {
9    /// Value fetched from the Remote Config backend and activated.
10    Remote,
11    /// Default value supplied by the client via `set_defaults`.
12    Default,
13    /// Static fallback used when the key has no remote or default entry.
14    Static,
15}
16
17impl RemoteConfigValueSource {
18    /// Returns the string identifier used in the JS SDK (`remote`, `default`, or `static`).
19    pub fn as_str(&self) -> &'static str {
20        match self {
21            RemoteConfigValueSource::Remote => "remote",
22            RemoteConfigValueSource::Default => "default",
23            RemoteConfigValueSource::Static => "static",
24        }
25    }
26}
27
28/// Represents a Remote Config parameter value with typed accessors.
29///
30/// This follows the behaviour of the JavaScript `Value` class where missing keys map to a static
31/// source with empty string defaults.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct RemoteConfigValue {
34    source: RemoteConfigValueSource,
35    value: String,
36}
37
38impl RemoteConfigValue {
39    const DEFAULT_BOOLEAN: bool = false;
40    const DEFAULT_NUMBER: f64 = 0.0;
41    const BOOLEAN_TRUTHY_VALUES: [&'static str; 6] = ["1", "true", "t", "yes", "y", "on"];
42
43    pub(crate) fn new(source: RemoteConfigValueSource, value: impl Into<String>) -> Self {
44        Self {
45            source,
46            value: value.into(),
47        }
48    }
49
50    pub(crate) fn static_value() -> Self {
51        Self::new(RemoteConfigValueSource::Static, String::new())
52    }
53
54    /// Returns the raw value as a string.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use firebase_rs_sdk::remote_config::value::RemoteConfigValue;
60    ///
61    /// let value = RemoteConfigValue::default();
62    /// assert_eq!(value.as_string(), "");
63    /// ```
64    pub fn as_string(&self) -> String {
65        self.value.clone()
66    }
67
68    /// Returns the value interpreted as a boolean.
69    ///
70    /// Matches the JS SDK semantics: `true` for case-insensitive values in
71    /// `{"1", "true", "t", "yes", "y", "on"}` when the source is remote/default, otherwise `false`.
72    pub fn as_bool(&self) -> bool {
73        if self.source == RemoteConfigValueSource::Static {
74            return Self::DEFAULT_BOOLEAN;
75        }
76        Self::BOOLEAN_TRUTHY_VALUES
77            .iter()
78            .any(|truthy| self.value.eq_ignore_ascii_case(truthy))
79    }
80
81    /// Returns the value interpreted as a number.
82    ///
83    /// Parsing failures fall back to `0.0`, mirroring the JavaScript implementation.
84    pub fn as_number(&self) -> f64 {
85        if self.source == RemoteConfigValueSource::Static {
86            return Self::DEFAULT_NUMBER;
87        }
88        match self.value.trim().parse::<f64>() {
89            Ok(parsed) if parsed.is_finite() || parsed == 0.0 => parsed,
90            Ok(parsed) if parsed.is_nan() => Self::DEFAULT_NUMBER,
91            Ok(parsed) => parsed,
92            Err(_) => Self::DEFAULT_NUMBER,
93        }
94    }
95
96    /// Returns the source of the value (`remote`, `default`, `static`).
97    pub fn source(&self) -> RemoteConfigValueSource {
98        self.source.clone()
99    }
100}
101
102impl Default for RemoteConfigValue {
103    fn default() -> Self {
104        Self::static_value()
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn boolean_truthy_values_match_js_behaviour() {
114        for truthy in RemoteConfigValue::BOOLEAN_TRUTHY_VALUES {
115            let value = RemoteConfigValue::new(RemoteConfigValueSource::Remote, truthy);
116            assert!(
117                value.as_bool(),
118                "expected truthy value {} to be true",
119                truthy
120            );
121        }
122        let value = RemoteConfigValue::new(RemoteConfigValueSource::Remote, "false");
123        assert!(!value.as_bool());
124    }
125
126    #[test]
127    fn static_boolean_is_false() {
128        let value = RemoteConfigValue::static_value();
129        assert!(!value.as_bool());
130    }
131
132    #[test]
133    fn number_parsing_matches_js_defaults() {
134        let value = RemoteConfigValue::new(RemoteConfigValueSource::Default, "42.5");
135        assert_eq!(value.as_number(), 42.5);
136
137        let invalid = RemoteConfigValue::new(RemoteConfigValueSource::Remote, "NaN");
138        assert_eq!(invalid.as_number(), 0.0);
139
140        let static_value = RemoteConfigValue::static_value();
141        assert_eq!(static_value.as_number(), 0.0);
142    }
143}