Skip to main content

bare_config/
toml.rs

1//! TOML format support for configuration content.
2//!
3//! This module provides the `TomlContent` type for working
4//! with TOML configuration files.
5//!
6//! # Features
7//!
8//! - Full CRUD operations (select, insert, update, delete, upsert)
9//! - Pretty-printed output using `toml::to_string_pretty`
10//! - Consistent round-trip serialization
11//!
12//! # Example
13//!
14//! ```
15//! use bare_config::toml::TomlContent;
16//! use bare_config::{ConfigContent, key::Key};
17//! use bare_config::value::Value;
18//! use std::str::FromStr;
19//!
20//! // Parse TOML configuration
21//! let config = TomlContent::from_str(r#"
22//! [database]
23//! url = "localhost"
24//! "#).unwrap();
25//!
26//! // Select a value
27//! let value = config.select(&Key::from_str(".database.url").unwrap()).unwrap();
28//! assert_eq!(value.as_str(), Some("localhost"));
29//!
30//! // Insert a new value
31//! let mut config = TomlContent::from_str("").unwrap();
32//! config.insert(&Key::from_str(".server.port").unwrap(), &Value::string("8080")).unwrap();
33//!
34//! // Format as string (pretty-printed)
35//! let output = config.to_string();
36//! println!("{}", output);
37//! ```
38
39use core::fmt;
40use core::str::FromStr;
41
42use crate::ConfigContent;
43use crate::error::{ConfigError, ConfigResult};
44use crate::key::{Key, KeySegment};
45use crate::value::Value;
46use toml::Value as TomlValue;
47use toml::map::Map;
48
49/// TOML configuration content.
50///
51/// This type provides CRUD operations for TOML configuration data.
52#[derive(Debug, Clone, PartialEq)]
53pub struct TomlContent {
54    data: TomlValue,
55}
56
57#[allow(dead_code)]
58impl TomlContent {
59    /// Creates a new TomlContent from a toml::Value.
60    pub fn from_value(value: TomlValue) -> Self {
61        Self { data: value }
62    }
63
64    /// Returns a reference to the underlying toml::Value.
65    pub fn as_value(&self) -> &TomlValue {
66        &self.data
67    }
68
69    /// Converts to the underlying toml::Value.
70    pub fn into_value(self) -> TomlValue {
71        self.data
72    }
73
74    fn get_value_at_key(&self, key: &Key) -> ConfigResult<&TomlValue> {
75        let mut current = &self.data;
76
77        for segment in key.segments() {
78            match segment {
79                KeySegment::Key(s) => {
80                    current = match current {
81                        TomlValue::Table(t) => t
82                            .get(s)
83                            .ok_or_else(|| ConfigError::KeyNotFound(key.to_key_string()))?,
84                        _ => {
85                            return Err(ConfigError::KeyNotFound(key.to_key_string()));
86                        }
87                    };
88                }
89                KeySegment::Index(i) => {
90                    current = match current {
91                        TomlValue::Array(arr) => arr
92                            .get(*i)
93                            .ok_or_else(|| ConfigError::KeyNotFound(key.to_key_string()))?,
94                        _ => {
95                            return Err(ConfigError::KeyNotFound(key.to_key_string()));
96                        }
97                    };
98                }
99                KeySegment::Attribute(_) => {
100                    return Err(ConfigError::InvalidKey(
101                        "TOML does not support attributes".to_string(),
102                    ));
103                }
104            }
105        }
106
107        Ok(current)
108    }
109
110    fn contains_key_at(&self, key: &Key) -> bool {
111        self.get_value_at_key(key).is_ok()
112    }
113
114    fn keys_at(&self) -> Vec<Key> {
115        match &self.data {
116            TomlValue::Table(t) => t
117                .keys()
118                .filter_map(|k| Key::from_str(&format!(".{k}")).ok())
119                .collect(),
120            TomlValue::Array(arr) => (0..arr.len())
121                .filter_map(|i| Key::from_str(&format!(".[{i}]")).ok())
122                .collect(),
123            _ => vec![],
124        }
125    }
126
127    fn len_at(&self) -> usize {
128        match &self.data {
129            TomlValue::Table(t) => t.len(),
130            TomlValue::Array(arr) => arr.len(),
131            _ => 0,
132        }
133    }
134
135    fn delete_at(&mut self, key: &Key) -> ConfigResult<()> {
136        if key.segments().is_empty() {
137            return Err(ConfigError::DeleteError("Cannot delete root".to_string()));
138        }
139
140        let segments = key.segments();
141        let last_idx = segments.len() - 1;
142
143        let mut current = &mut self.data;
144
145        for (i, segment) in segments.iter().enumerate() {
146            if i == last_idx {
147                match segment {
148                    KeySegment::Key(s) => {
149                        if let TomlValue::Table(t) = current {
150                            drop(t.remove(s));
151                            return Ok(());
152                        }
153                    }
154                    KeySegment::Index(idx) => {
155                        if let TomlValue::Array(arr) = current {
156                            if *idx < arr.len() {
157                                drop(arr.remove(*idx));
158                                return Ok(());
159                            }
160                        }
161                    }
162                    KeySegment::Attribute(_) => {
163                        return Err(ConfigError::InvalidKey(
164                            "TOML does not support attributes".to_string(),
165                        ));
166                    }
167                }
168            } else {
169                match segment {
170                    KeySegment::Key(s) => {
171                        if let TomlValue::Table(t) = current {
172                            if let Some(next) = t.get_mut(s) {
173                                current = next;
174                            } else {
175                                return Err(ConfigError::KeyNotFound(key.to_key_string()));
176                            }
177                        } else {
178                            return Err(ConfigError::KeyNotFound(key.to_key_string()));
179                        }
180                    }
181                    KeySegment::Index(idx) => {
182                        if let TomlValue::Array(arr) = current {
183                            if let Some(next) = arr.get_mut(*idx) {
184                                current = next;
185                            } else {
186                                return Err(ConfigError::KeyNotFound(key.to_key_string()));
187                            }
188                        } else {
189                            return Err(ConfigError::KeyNotFound(key.to_key_string()));
190                        }
191                    }
192                    KeySegment::Attribute(_) => {
193                        return Err(ConfigError::InvalidKey(
194                            "TOML does not support attributes".to_string(),
195                        ));
196                    }
197                }
198            }
199        }
200
201        Err(ConfigError::KeyNotFound(key.to_key_string()))
202    }
203
204    fn upsert_at(&mut self, key: &Key, value: &Value) -> ConfigResult<()> {
205        if key.segments().is_empty() {
206            self.data = toml_value_from_value(value);
207            return Ok(());
208        }
209
210        let segments = key.segments();
211        let last_idx = segments.len() - 1;
212
213        let mut current = &mut self.data;
214
215        for (i, segment) in segments.iter().enumerate() {
216            if i == last_idx {
217                match segment {
218                    KeySegment::Key(s) => {
219                        if let TomlValue::Table(t) = current {
220                            drop(t.insert(s.clone(), toml_value_from_value(value)));
221                            return Ok(());
222                        }
223                    }
224                    KeySegment::Index(idx) => {
225                        if let TomlValue::Array(arr) = current {
226                            if *idx < arr.len() {
227                                arr[*idx] = toml_value_from_value(value);
228                            } else if *idx == arr.len() {
229                                arr.push(toml_value_from_value(value));
230                            } else {
231                                return Err(ConfigError::KeyNotFound(key.to_key_string()));
232                            }
233                            return Ok(());
234                        }
235                    }
236                    KeySegment::Attribute(_) => {
237                        return Err(ConfigError::InvalidKey(
238                            "TOML does not support attributes".to_string(),
239                        ));
240                    }
241                }
242            } else {
243                match segment {
244                    KeySegment::Key(s) => {
245                        let next = match current {
246                            TomlValue::Table(t) => {
247                                let key_exists = t.contains_key(s);
248                                if !key_exists {
249                                    drop(t.insert(s.clone(), TomlValue::Table(Map::new())));
250                                }
251                                t.get_mut(s)
252                                    .ok_or_else(|| ConfigError::KeyNotFound(key.to_key_string()))?
253                            }
254                            _ => {
255                                return Err(ConfigError::KeyNotFound(key.to_key_string()));
256                            }
257                        };
258                        current = next;
259                    }
260                    KeySegment::Index(idx) => {
261                        let next = match current {
262                            TomlValue::Array(arr) => {
263                                if *idx < arr.len() {
264                                    &mut arr[*idx]
265                                } else if *idx == arr.len() {
266                                    arr.push(TomlValue::String(String::new()));
267                                    arr.last_mut().ok_or_else(|| {
268                                        ConfigError::KeyNotFound(key.to_key_string())
269                                    })?
270                                } else {
271                                    return Err(ConfigError::KeyNotFound(key.to_key_string()));
272                                }
273                            }
274                            _ => {
275                                return Err(ConfigError::KeyNotFound(key.to_key_string()));
276                            }
277                        };
278                        current = next;
279                    }
280                    KeySegment::Attribute(_) => {
281                        return Err(ConfigError::InvalidKey(
282                            "TOML does not support attributes".to_string(),
283                        ));
284                    }
285                }
286            }
287        }
288
289        Ok(())
290    }
291}
292
293impl ConfigContent for TomlContent {
294    fn select(&self, key: &Key) -> ConfigResult<Value> {
295        let toml_value = self.get_value_at_key(key)?;
296        Ok(value_from_toml_value(toml_value))
297    }
298
299    fn insert(&mut self, key: &Key, value: &Value) -> ConfigResult<()> {
300        if self.contains_key_at(key) {
301            return Err(ConfigError::KeyAlreadyExists(key.to_key_string()));
302        }
303        self.upsert_at(key, value)
304    }
305
306    fn update(&mut self, key: &Key, value: &Value) -> ConfigResult<()> {
307        if !self.contains_key_at(key) {
308            return Err(ConfigError::KeyDoesNotExist(key.to_key_string()));
309        }
310        self.upsert_at(key, value)
311    }
312
313    fn delete(&mut self, key: &Key) -> ConfigResult<()> {
314        self.delete_at(key)
315    }
316
317    fn upsert(&mut self, key: &Key, value: &Value) -> ConfigResult<()> {
318        self.upsert_at(key, value)
319    }
320}
321
322impl FromStr for TomlContent {
323    type Err = ConfigError;
324
325    fn from_str(s: &str) -> Result<Self, Self::Err> {
326        let value: toml::Value =
327            toml::from_str(s).map_err(|e| ConfigError::ParseError(e.to_string()))?;
328        Ok(Self { data: value })
329    }
330}
331
332impl fmt::Display for TomlContent {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        let output = toml::to_string_pretty(&self.data).map_err(|_| fmt::Error)?;
335        write!(f, "{}", output)
336    }
337}
338
339fn value_from_toml_value(toml: &TomlValue) -> Value {
340    match toml {
341        TomlValue::String(s) if s.is_empty() => Value::Null,
342        TomlValue::String(s) => Value::String(s.clone()),
343        TomlValue::Integer(i) => Value::Integer(*i),
344        TomlValue::Float(f) => Value::Float(*f),
345        TomlValue::Boolean(b) => Value::Bool(*b),
346        TomlValue::Datetime(dt) => Value::String(dt.to_string()),
347        TomlValue::Array(arr) => Value::Array(arr.iter().map(value_from_toml_value).collect()),
348        TomlValue::Table(t) => Value::Map(
349            t.iter()
350                .map(|(k, v)| (k.clone(), value_from_toml_value(v)))
351                .collect(),
352        ),
353    }
354}
355
356fn toml_value_from_value(value: &Value) -> TomlValue {
357    match value {
358        Value::Null => TomlValue::String(String::new()),
359        Value::Bool(b) => TomlValue::Boolean(*b),
360        Value::Integer(i) => TomlValue::Integer(*i),
361        Value::Float(f) => {
362            // TOML doesn't support NaN/Infinity, serialize as string
363            if f.is_nan() {
364                TomlValue::String("NaN".to_string())
365            } else if f.is_infinite() {
366                if f.is_sign_positive() {
367                    TomlValue::String("Infinity".to_string())
368                } else {
369                    TomlValue::String("-Infinity".to_string())
370                }
371            } else {
372                TomlValue::Float(*f)
373            }
374        }
375        Value::String(s) => TomlValue::String(s.clone()),
376        Value::Array(arr) => TomlValue::Array(arr.iter().map(toml_value_from_value).collect()),
377        Value::Map(m) => {
378            let mut t = Map::new();
379            for (k, v) in m {
380                drop(t.insert(k.clone(), toml_value_from_value(v)));
381            }
382            TomlValue::Table(t)
383        }
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_toml_from_str() {
393        let content = TomlContent::from_str("key = \"value\"").expect("test should succeed");
394        assert!(
395            content
396                .select(&Key::from_str(".key").expect("test should succeed"))
397                .is_ok()
398        );
399    }
400
401    #[test]
402    fn test_toml_to_string() {
403        let content = TomlContent::from_str("key = \"value\"").expect("test should succeed");
404        let s = content.to_string();
405        assert!(s.contains("key"));
406    }
407
408    #[test]
409    fn test_toml_select() {
410        let content = TomlContent::from_str("name = \"test\"").expect("test should succeed");
411        let value = content
412            .select(&Key::from_str(".name").expect("test should succeed"))
413            .expect("test should succeed");
414        assert_eq!(value.as_str(), Some("test"));
415    }
416
417    #[test]
418    fn test_toml_select_nested() {
419        let content =
420            TomlContent::from_str("[database]\nurl = \"localhost\"").expect("test should succeed");
421        let value = content
422            .select(&Key::from_str(".database.url").expect("test should succeed"))
423            .expect("test should succeed");
424        assert_eq!(value.as_str(), Some("localhost"));
425    }
426
427    #[test]
428    fn test_toml_insert() {
429        let mut content = TomlContent::from_str("").expect("test should succeed");
430        content
431            .insert(
432                &Key::from_str(".name").expect("test should succeed"),
433                &Value::string("test"),
434            )
435            .expect("test should succeed");
436        let value = content
437            .select(&Key::from_str(".name").expect("test should succeed"))
438            .expect("test should succeed");
439        assert_eq!(value.as_str(), Some("test"));
440    }
441
442    #[test]
443    fn test_toml_update() {
444        let mut content = TomlContent::from_str("name = \"old\"").expect("test should succeed");
445        content
446            .update(
447                &Key::from_str(".name").expect("test should succeed"),
448                &Value::string("new"),
449            )
450            .expect("test should succeed");
451        let value = content
452            .select(&Key::from_str(".name").expect("test should succeed"))
453            .expect("test should succeed");
454        assert_eq!(value.as_str(), Some("new"));
455    }
456
457    #[test]
458    fn test_toml_delete() {
459        let mut content = TomlContent::from_str("name = \"test\"").expect("test should succeed");
460        content
461            .delete(&Key::from_str(".name").expect("test should succeed"))
462            .expect("test should succeed");
463        assert!(
464            content
465                .select(&Key::from_str(".name").expect("test should succeed"))
466                .is_err()
467        );
468    }
469
470    #[test]
471    fn test_toml_upsert() {
472        let mut content = TomlContent::from_str("").expect("test should succeed");
473        content
474            .upsert(
475                &Key::from_str(".name").expect("test should succeed"),
476                &Value::string("test"),
477            )
478            .expect("test should succeed");
479        let value = content
480            .select(&Key::from_str(".name").expect("test should succeed"))
481            .expect("test should succeed");
482        assert_eq!(value.as_str(), Some("test"));
483    }
484
485    #[test]
486    fn test_toml_contains_key() {
487        let content = TomlContent::from_str("name = \"test\"").expect("test should succeed");
488        assert!(
489            content
490                .select(&Key::from_str(".name").expect("test should succeed"))
491                .is_ok()
492        );
493    }
494
495    #[test]
496    fn test_toml_display_is_stable_and_pretty() {
497        let content = TomlContent::from_str("b = 2\n[a]\nx = 1").expect("test should succeed");
498        let first = content.to_string();
499        let second = content.to_string();
500        assert_eq!(first, second);
501        assert!(first.contains("\n"));
502    }
503
504    #[test]
505    fn test_toml_string_basic() {
506        let content = TomlContent::from_str(r#"str = "hello""#).expect("test should succeed");
507        let value = content
508            .select(&Key::from_str(".str").expect("test should succeed"))
509            .expect("test should succeed");
510        assert_eq!(value.as_str(), Some("hello"));
511    }
512
513    #[test]
514    fn test_toml_string_multiline() {
515        let content =
516            TomlContent::from_str("str = '''\nmultiline\nstring'''").expect("test should succeed");
517        let value = content
518            .select(&Key::from_str(".str").expect("test should succeed"))
519            .expect("test should succeed");
520        assert!(
521            value
522                .as_str()
523                .expect("test should succeed")
524                .contains("multiline")
525        );
526    }
527
528    #[test]
529    fn test_toml_integer_decimal() {
530        let content = TomlContent::from_str("int = 42").expect("test should succeed");
531        let value = content
532            .select(&Key::from_str(".int").expect("test should succeed"))
533            .expect("test should succeed");
534        assert_eq!(value.as_integer(), Some(42));
535    }
536
537    #[test]
538    fn test_toml_integer_negative() {
539        let content = TomlContent::from_str("int = -17").expect("test should succeed");
540        let value = content
541            .select(&Key::from_str(".int").expect("test should succeed"))
542            .expect("test should succeed");
543        assert_eq!(value.as_integer(), Some(-17));
544    }
545
546    #[test]
547    fn test_toml_integer_underscore() {
548        let content = TomlContent::from_str("int = 1_000").expect("test should succeed");
549        let value = content
550            .select(&Key::from_str(".int").expect("test should succeed"))
551            .expect("test should succeed");
552        assert_eq!(value.as_integer(), Some(1000));
553    }
554
555    #[test]
556    fn test_toml_float_basic() {
557        let content = TomlContent::from_str("flt = 3.14").expect("test should succeed");
558        let value = content
559            .select(&Key::from_str(".flt").expect("test should succeed"))
560            .expect("test should succeed");
561        let f = value.as_float().expect("test should succeed");
562        assert!((f - 3.14).abs() < 1e-6);
563    }
564
565    #[test]
566    fn test_toml_float_scientific() {
567        let content = TomlContent::from_str("flt = 1e10").expect("test should succeed");
568        let value = content
569            .select(&Key::from_str(".flt").expect("test should succeed"))
570            .expect("test should succeed");
571        let f = value.as_float().expect("test should succeed");
572        assert!((f - 1e10).abs() < 1.0);
573    }
574
575    #[test]
576    fn test_toml_boolean() {
577        let content = TomlContent::from_str("t = true\nf = false").expect("test should succeed");
578        assert_eq!(
579            content
580                .select(&Key::from_str(".t").expect("test should succeed"))
581                .expect("test should succeed")
582                .as_bool(),
583            Some(true)
584        );
585        assert_eq!(
586            content
587                .select(&Key::from_str(".f").expect("test should succeed"))
588                .expect("test should succeed")
589                .as_bool(),
590            Some(false)
591        );
592    }
593
594    #[test]
595    fn test_toml_array() {
596        let content = TomlContent::from_str("arr = [1, 2, 3]").expect("test should succeed");
597        let value = content
598            .select(&Key::from_str(".arr[0]").expect("test should succeed"))
599            .expect("test should succeed");
600        assert_eq!(value.as_integer(), Some(1));
601    }
602
603    #[test]
604    fn test_toml_array_of_strings() {
605        let content =
606            TomlContent::from_str(r#"arr = ["a", "b", "c"]"#).expect("test should succeed");
607        let value = content
608            .select(&Key::from_str(".arr[1]").expect("test should succeed"))
609            .expect("test should succeed");
610        assert_eq!(value.as_str(), Some("b"));
611    }
612
613    #[test]
614    fn test_toml_inline_table() {
615        let content =
616            TomlContent::from_str(r#"point = {x = 1, y = 2}"#).expect("test should succeed");
617        let value = content
618            .select(&Key::from_str(".point.x").expect("test should succeed"))
619            .expect("test should succeed");
620        assert_eq!(value.as_integer(), Some(1));
621    }
622
623    #[test]
624    fn test_toml_nested_table() {
625        let content = TomlContent::from_str("[server]\nhost = \"localhost\"\n[server.port]")
626            .expect("test should succeed");
627        let value = content
628            .select(&Key::from_str(".server.host").expect("test should succeed"))
629            .expect("test should succeed");
630        assert_eq!(value.as_str(), Some("localhost"));
631    }
632
633    #[test]
634    fn test_toml_array_of_tables() {
635        let content =
636            TomlContent::from_str("[[users]]\nname = \"Alice\"\n[[users]]\nname = \"Bob\"")
637                .expect("test should succeed");
638        let value = content
639            .select(&Key::from_str(".users[0].name").expect("test should succeed"))
640            .expect("test should succeed");
641        assert_eq!(value.as_str(), Some("Alice"));
642    }
643
644    #[test]
645    fn test_toml_delete_nested() {
646        let mut content = TomlContent::from_str("[database]\nhost = \"localhost\"\nport = 5432")
647            .expect("test should succeed");
648        content
649            .delete(&Key::from_str(".database.port").expect("test should succeed"))
650            .expect("test should succeed");
651        assert!(
652            content
653                .select(&Key::from_str(".database.port").expect("test should succeed"))
654                .is_err()
655        );
656    }
657
658    #[test]
659    fn test_toml_update_nested() {
660        let mut content =
661            TomlContent::from_str("[database]\nhost = \"old\"").expect("test should succeed");
662        content
663            .update(
664                &Key::from_str(".database.host").expect("test should succeed"),
665                &Value::string("new"),
666            )
667            .expect("test should succeed");
668        let value = content
669            .select(&Key::from_str(".database.host").expect("test should succeed"))
670            .expect("test should succeed");
671        assert_eq!(value.as_str(), Some("new"));
672    }
673
674    #[test]
675    fn test_toml_special_float() {
676        let content = TomlContent::from_str("inf = inf\nneg_inf = -inf\nnan = nan")
677            .expect("test should succeed");
678        assert!(
679            content
680                .select(&Key::from_str(".inf").expect("test should succeed"))
681                .expect("test should succeed")
682                .as_float()
683                .expect("test should succeed")
684                .is_infinite()
685        );
686        assert!(
687            content
688                .select(&Key::from_str(".neg_inf").expect("test should succeed"))
689                .expect("test should succeed")
690                .as_float()
691                .expect("test should succeed")
692                .is_sign_negative()
693        );
694        assert!(
695            content
696                .select(&Key::from_str(".nan").expect("test should succeed"))
697                .expect("test should succeed")
698                .as_float()
699                .expect("test should succeed")
700                .is_nan()
701        );
702    }
703}