config_lib/parsers/
toml_parser.rs

1//! # TOML Format Parser
2//!
3//! TOML parser with format preservation capabilities.
4//! Uses the NOML library's TOML compatibility for round-trip editing.
5
6use crate::error::Result;
7use crate::value::Value;
8use std::collections::BTreeMap;
9
10/// Parse TOML format configuration
11#[cfg(feature = "noml")]
12pub fn parse(source: &str) -> Result<Value> {
13    // Use NOML's TOML parsing capability for format preservation
14    let noml_value = noml::parse(source)?;
15    convert_noml_value(noml_value)
16}
17
18/// Parse TOML format configuration (fallback when NOML is not available)
19#[cfg(not(feature = "noml"))]
20pub fn parse(_source: &str) -> Result<Value> {
21    Err(Error::general(
22        "TOML parsing requires either the 'noml' feature or a dedicated TOML parser",
23    ))
24}
25
26/// Parse TOML with format preservation for round-trip editing
27#[cfg(feature = "noml")]
28pub fn parse_with_preservation(source: &str) -> Result<(Value, noml::Document)> {
29    // Parse to get the AST document for format preservation
30    let document = noml::parse_string(source, None)?;
31
32    // Resolve to get the actual values
33    let mut resolver = noml::Resolver::new();
34    let resolved = resolver.resolve(&document)?;
35
36    // Convert to config-lib Value
37    let value = convert_noml_value(resolved)?;
38
39    Ok((value, document))
40}
41
42/// Parse TOML with format preservation (fallback when NOML is not available)
43#[cfg(not(feature = "noml"))]
44pub fn parse_with_preservation(_source: &str) -> Result<(Value, ())> {
45    Err(Error::general(
46        "TOML format preservation requires the 'noml' feature",
47    ))
48}
49
50/// Convert NOML Value to config-lib Value
51#[cfg(feature = "noml")]
52fn convert_noml_value(noml_value: noml::Value) -> Result<Value> {
53    match noml_value {
54        noml::Value::Null => Ok(Value::Null),
55        noml::Value::Bool(b) => Ok(Value::Bool(b)),
56        noml::Value::Integer(i) => Ok(Value::Integer(i)),
57        noml::Value::Float(f) => Ok(Value::Float(f)),
58        noml::Value::String(s) => Ok(Value::String(s)),
59        noml::Value::Array(arr) => {
60            let converted: Result<Vec<Value>> = arr.into_iter().map(convert_noml_value).collect();
61            Ok(Value::Array(converted?))
62        }
63        noml::Value::Table(table) => {
64            let mut converted = BTreeMap::new();
65            for (key, value) in table {
66                converted.insert(key, convert_noml_value(value)?);
67            }
68            Ok(Value::Table(converted))
69        }
70        #[cfg(feature = "chrono")]
71        noml::Value::DateTime(dt) => Ok(Value::DateTime(dt)),
72        #[cfg(not(feature = "chrono"))]
73        noml::Value::DateTime(dt) => Ok(Value::String(dt.to_rfc3339())),
74        // Handle NOML-specific types by converting to basic types
75        noml::Value::Binary(_) => Ok(Value::String("binary_data".to_string())),
76        noml::Value::Size(size) => Ok(Value::Integer(size as i64)),
77        noml::Value::Duration(duration) => Ok(Value::Float(duration)),
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_basic_toml() {
87        let config = parse(
88            r#"
89            name = "test"
90            port = 8080
91            debug = true
92            
93            [database]
94            host = "localhost"
95            max_connections = 100
96        "#,
97        )
98        .unwrap();
99
100        assert_eq!(config.get("name").unwrap().as_string().unwrap(), "test");
101        assert_eq!(config.get("port").unwrap().as_integer().unwrap(), 8080);
102        assert_eq!(
103            config.get("database.host").unwrap().as_string().unwrap(),
104            "localhost"
105        );
106    }
107
108    #[test]
109    fn test_toml_arrays() {
110        let config = parse(
111            r#"
112            servers = ["alpha", "beta", "gamma"]
113            ports = [8001, 8002, 8003]
114        "#,
115        )
116        .unwrap();
117
118        let servers = config.get("servers").unwrap().as_array().unwrap();
119        assert_eq!(servers.len(), 3);
120        assert_eq!(servers[0].as_string().unwrap(), "alpha");
121
122        let ports = config.get("ports").unwrap().as_array().unwrap();
123        assert_eq!(ports[0].as_integer().unwrap(), 8001);
124    }
125}