distributed_config/
value.rs

1//! Configuration value types and conversions
2
3use crate::error::{ConfigError, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::time::Duration;
7
8/// A dynamic configuration value that can hold various types
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(untagged)]
11pub enum ConfigValue {
12    /// Null/None value
13    Null,
14    /// Boolean value
15    Bool(bool),
16    /// Integer value
17    Integer(i64),
18    /// Floating point value
19    Float(f64),
20    /// String value
21    String(String),
22    /// Array of values
23    Array(Vec<ConfigValue>),
24    /// Object/Map of values
25    Object(HashMap<String, ConfigValue>),
26    /// Duration value (serialized as seconds)
27    #[serde(with = "duration_serde")]
28    Duration(Duration),
29}
30
31impl ConfigValue {
32    /// Check if the value is null
33    pub fn is_null(&self) -> bool {
34        matches!(self, ConfigValue::Null)
35    }
36
37    /// Convert to boolean, returning error if conversion fails
38    pub fn as_bool(&self) -> Result<bool> {
39        match self {
40            ConfigValue::Bool(b) => Ok(*b),
41            ConfigValue::String(s) => match s.to_lowercase().as_str() {
42                "true" | "yes" | "1" | "on" => Ok(true),
43                "false" | "no" | "0" | "off" => Ok(false),
44                _ => Err(ConfigError::TypeConversion {
45                    from: "string".to_string(),
46                    to: "bool".to_string(),
47                }),
48            },
49            ConfigValue::Integer(i) => Ok(*i != 0),
50            _ => Err(ConfigError::TypeConversion {
51                from: format!("{self:?}"),
52                to: "bool".to_string(),
53            }),
54        }
55    }
56
57    /// Convert to integer, returning error if conversion fails
58    pub fn as_integer(&self) -> Result<i64> {
59        match self {
60            ConfigValue::Integer(i) => Ok(*i),
61            ConfigValue::Float(f) => Ok(*f as i64),
62            ConfigValue::String(s) => s.parse().map_err(|_| ConfigError::TypeConversion {
63                from: "string".to_string(),
64                to: "integer".to_string(),
65            }),
66            ConfigValue::Bool(b) => Ok(if *b { 1 } else { 0 }),
67            _ => Err(ConfigError::TypeConversion {
68                from: format!("{self:?}"),
69                to: "integer".to_string(),
70            }),
71        }
72    }
73
74    /// Convert to float, returning error if conversion fails
75    pub fn as_float(&self) -> Result<f64> {
76        match self {
77            ConfigValue::Float(f) => Ok(*f),
78            ConfigValue::Integer(i) => Ok(*i as f64),
79            ConfigValue::String(s) => s.parse().map_err(|_| ConfigError::TypeConversion {
80                from: "string".to_string(),
81                to: "float".to_string(),
82            }),
83            _ => Err(ConfigError::TypeConversion {
84                from: format!("{self:?}"),
85                to: "float".to_string(),
86            }),
87        }
88    }
89
90    /// Convert to string
91    pub fn as_string(&self) -> Result<String> {
92        match self {
93            ConfigValue::String(s) => Ok(s.clone()),
94            ConfigValue::Integer(i) => Ok(i.to_string()),
95            ConfigValue::Float(f) => Ok(f.to_string()),
96            ConfigValue::Bool(b) => Ok(b.to_string()),
97            _ => Err(ConfigError::TypeConversion {
98                from: format!("{self:?}"),
99                to: "string".to_string(),
100            }),
101        }
102    }
103
104    /// Convert to array, returning error if conversion fails
105    pub fn as_array(&self) -> Result<&Vec<ConfigValue>> {
106        match self {
107            ConfigValue::Array(arr) => Ok(arr),
108            _ => Err(ConfigError::TypeConversion {
109                from: format!("{self:?}"),
110                to: "array".to_string(),
111            }),
112        }
113    }
114
115    /// Convert to object/map, returning error if conversion fails
116    pub fn as_object(&self) -> Result<&HashMap<String, ConfigValue>> {
117        match self {
118            ConfigValue::Object(obj) => Ok(obj),
119            _ => Err(ConfigError::TypeConversion {
120                from: format!("{self:?}"),
121                to: "object".to_string(),
122            }),
123        }
124    }
125
126    /// Convert to duration, returning error if conversion fails
127    pub fn as_duration(&self) -> Result<Duration> {
128        match self {
129            ConfigValue::Duration(d) => Ok(*d),
130            ConfigValue::Integer(i) => Ok(Duration::from_secs(*i as u64)),
131            ConfigValue::String(s) => {
132                // Try parsing as seconds first
133                if let Ok(secs) = s.parse::<u64>() {
134                    return Ok(Duration::from_secs(secs));
135                }
136
137                // Try parsing duration strings like "30s", "5m", "1h"
138                parse_duration_string(s)
139            }
140            _ => Err(ConfigError::TypeConversion {
141                from: format!("{self:?}"),
142                to: "duration".to_string(),
143            }),
144        }
145    }
146
147    /// Get a nested value by dot-separated path
148    pub fn get_path(&self, path: &str) -> Option<&ConfigValue> {
149        if path.is_empty() {
150            return Some(self);
151        }
152
153        let parts: Vec<&str> = path.split('.').collect();
154        let mut current = self;
155
156        for part in parts {
157            match current {
158                ConfigValue::Object(obj) => {
159                    current = obj.get(part)?;
160                }
161                _ => return None,
162            }
163        }
164
165        Some(current)
166    }
167
168    /// Set a nested value by dot-separated path
169    pub fn set_path(&mut self, path: &str, value: ConfigValue) -> Result<()> {
170        if path.is_empty() {
171            *self = value;
172            return Ok(());
173        }
174
175        let parts: Vec<&str> = path.split('.').collect();
176        let mut current = self;
177
178        // Navigate to the parent of the target
179        for part in &parts[..parts.len() - 1] {
180            match current {
181                ConfigValue::Object(obj) => {
182                    current = obj
183                        .entry(part.to_string())
184                        .or_insert_with(|| ConfigValue::Object(HashMap::new()));
185                }
186                _ => {
187                    return Err(ConfigError::Other(
188                        "Cannot set path on non-object value".to_string(),
189                    ));
190                }
191            }
192        }
193
194        // Set the final value
195        if let ConfigValue::Object(obj) = current {
196            obj.insert(parts[parts.len() - 1].to_string(), value);
197            Ok(())
198        } else {
199            Err(ConfigError::Other(
200                "Cannot set path on non-object value".to_string(),
201            ))
202        }
203    }
204
205    /// Merge another ConfigValue into this one
206    pub fn merge(&mut self, other: ConfigValue) {
207        match (self, other) {
208            (ConfigValue::Object(left), ConfigValue::Object(right)) => {
209                for (key, value) in right {
210                    if let Some(existing) = left.get_mut(&key) {
211                        existing.merge(value);
212                    } else {
213                        left.insert(key, value);
214                    }
215                }
216            }
217            (left, right) => *left = right,
218        }
219    }
220}
221
222// Implement From traits for convenient creation
223impl From<bool> for ConfigValue {
224    fn from(value: bool) -> Self {
225        ConfigValue::Bool(value)
226    }
227}
228
229impl From<i64> for ConfigValue {
230    fn from(value: i64) -> Self {
231        ConfigValue::Integer(value)
232    }
233}
234
235impl From<i32> for ConfigValue {
236    fn from(value: i32) -> Self {
237        ConfigValue::Integer(value as i64)
238    }
239}
240
241impl From<u32> for ConfigValue {
242    fn from(value: u32) -> Self {
243        ConfigValue::Integer(value as i64)
244    }
245}
246
247impl From<f64> for ConfigValue {
248    fn from(value: f64) -> Self {
249        ConfigValue::Float(value)
250    }
251}
252
253impl From<String> for ConfigValue {
254    fn from(value: String) -> Self {
255        ConfigValue::String(value)
256    }
257}
258
259impl From<&str> for ConfigValue {
260    fn from(value: &str) -> Self {
261        ConfigValue::String(value.to_string())
262    }
263}
264
265impl From<Duration> for ConfigValue {
266    fn from(value: Duration) -> Self {
267        ConfigValue::Duration(value)
268    }
269}
270
271impl From<Vec<ConfigValue>> for ConfigValue {
272    fn from(value: Vec<ConfigValue>) -> Self {
273        ConfigValue::Array(value)
274    }
275}
276
277impl From<HashMap<String, ConfigValue>> for ConfigValue {
278    fn from(value: HashMap<String, ConfigValue>) -> Self {
279        ConfigValue::Object(value)
280    }
281}
282
283impl From<serde_json::Value> for ConfigValue {
284    fn from(value: serde_json::Value) -> Self {
285        match value {
286            serde_json::Value::Null => ConfigValue::Null,
287            serde_json::Value::Bool(b) => ConfigValue::Bool(b),
288            serde_json::Value::Number(n) => {
289                if let Some(i) = n.as_i64() {
290                    ConfigValue::Integer(i)
291                } else if let Some(f) = n.as_f64() {
292                    ConfigValue::Float(f)
293                } else {
294                    ConfigValue::Null
295                }
296            }
297            serde_json::Value::String(s) => ConfigValue::String(s),
298            serde_json::Value::Array(arr) => {
299                ConfigValue::Array(arr.into_iter().map(ConfigValue::from).collect())
300            }
301            serde_json::Value::Object(obj) => ConfigValue::Object(
302                obj.into_iter()
303                    .map(|(k, v)| (k, ConfigValue::from(v)))
304                    .collect(),
305            ),
306        }
307    }
308}
309
310/// Helper function to parse duration strings like "30s", "5m", "1h"
311fn parse_duration_string(s: &str) -> Result<Duration> {
312    let s = s.trim();
313
314    if s.is_empty() {
315        return Err(ConfigError::TypeConversion {
316            from: "empty string".to_string(),
317            to: "duration".to_string(),
318        });
319    }
320
321    let (number_part, unit_part) = if s.chars().last().unwrap().is_alphabetic() {
322        let split_pos = s.len() - 1;
323        (&s[..split_pos], &s[split_pos..])
324    } else {
325        (s, "s") // Default to seconds
326    };
327
328    let number: f64 = number_part
329        .parse()
330        .map_err(|_| ConfigError::TypeConversion {
331            from: s.to_string(),
332            to: "duration".to_string(),
333        })?;
334
335    let duration = match unit_part {
336        "ns" => Duration::from_nanos((number * 1.0) as u64),
337        "us" | "μs" => Duration::from_micros((number * 1.0) as u64),
338        "ms" => Duration::from_millis((number * 1.0) as u64),
339        "s" => Duration::from_secs_f64(number),
340        "m" => Duration::from_secs_f64(number * 60.0),
341        "h" => Duration::from_secs_f64(number * 3600.0),
342        "d" => Duration::from_secs_f64(number * 86400.0),
343        _ => {
344            return Err(ConfigError::TypeConversion {
345                from: s.to_string(),
346                to: "duration".to_string(),
347            });
348        }
349    };
350
351    Ok(duration)
352}
353
354/// Custom serde module for Duration
355mod duration_serde {
356    use super::*;
357    use serde::{Deserializer, Serializer};
358
359    pub fn serialize<S>(duration: &Duration, serializer: S) -> std::result::Result<S::Ok, S::Error>
360    where
361        S: Serializer,
362    {
363        serializer.serialize_u64(duration.as_secs())
364    }
365
366    pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<Duration, D::Error>
367    where
368        D: Deserializer<'de>,
369    {
370        let secs = u64::deserialize(deserializer)?;
371        Ok(Duration::from_secs(secs))
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn test_config_value_conversions() {
381        let bool_val = ConfigValue::Bool(true);
382        assert!(bool_val.as_bool().unwrap());
383        assert_eq!(bool_val.as_integer().unwrap(), 1);
384
385        let int_val = ConfigValue::Integer(42);
386        assert_eq!(int_val.as_integer().unwrap(), 42);
387        assert_eq!(int_val.as_float().unwrap(), 42.0);
388
389        let str_val = ConfigValue::String("test".to_string());
390        assert_eq!(str_val.as_string().unwrap(), "test");
391    }
392
393    #[test]
394    fn test_path_operations() {
395        let mut config = ConfigValue::Object(HashMap::new());
396        config
397            .set_path("app.database.host", "localhost".into())
398            .unwrap();
399
400        assert_eq!(
401            config
402                .get_path("app.database.host")
403                .unwrap()
404                .as_string()
405                .unwrap(),
406            "localhost"
407        );
408    }
409
410    #[test]
411    fn test_duration_parsing() {
412        assert_eq!(
413            parse_duration_string("30s").unwrap(),
414            Duration::from_secs(30)
415        );
416        assert_eq!(
417            parse_duration_string("5m").unwrap(),
418            Duration::from_secs(300)
419        );
420        assert_eq!(
421            parse_duration_string("1h").unwrap(),
422            Duration::from_secs(3600)
423        );
424        assert_eq!(
425            parse_duration_string("2d").unwrap(),
426            Duration::from_secs(172800)
427        );
428    }
429}