1use crate::error::{ConfigError, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::time::Duration;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(untagged)]
11pub enum ConfigValue {
12 Null,
14 Bool(bool),
16 Integer(i64),
18 Float(f64),
20 String(String),
22 Array(Vec<ConfigValue>),
24 Object(HashMap<String, ConfigValue>),
26 #[serde(with = "duration_serde")]
28 Duration(Duration),
29}
30
31impl ConfigValue {
32 pub fn is_null(&self) -> bool {
34 matches!(self, ConfigValue::Null)
35 }
36
37 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 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 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 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 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 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 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 if let Ok(secs) = s.parse::<u64>() {
134 return Ok(Duration::from_secs(secs));
135 }
136
137 parse_duration_string(s)
139 }
140 _ => Err(ConfigError::TypeConversion {
141 from: format!("{self:?}"),
142 to: "duration".to_string(),
143 }),
144 }
145 }
146
147 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 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 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 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 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
222impl 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
310fn 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") };
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
354mod 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}