config_lib/parsers/
noml_parser.rs

1//! # NOML Format Parser
2//!
3//! Integration with the NOML library for advanced configuration features.
4//! This parser leverages the full NOML library to provide:
5//!
6//! - Environment variable resolution
7//! - File includes  
8//! - Variable interpolation
9//! - Native types (@size, @duration, etc.)
10//! - Format preservation for round-trip editing
11
12#[cfg(not(feature = "noml"))]
13use crate::error::Error;
14use crate::error::Result;
15use crate::value::Value;
16use std::collections::BTreeMap;
17
18/// Parse NOML format configuration using the noml library
19#[cfg(feature = "noml")]
20pub fn parse(source: &str) -> Result<Value> {
21    // Parse the document
22    let document = noml::parse_string(source, None)?;
23
24    // Resolve dynamic features (env vars, includes, etc.)
25    let mut resolver = noml::Resolver::new();
26    let resolved = resolver.resolve(&document)?;
27
28    convert_noml_value(resolved)
29}
30
31/// Parse NOML format configuration (fallback when NOML is not available)
32#[cfg(not(feature = "noml"))]
33pub fn parse(_source: &str) -> Result<Value> {
34    Err(Error::general(
35        "NOML parsing requires the 'noml' feature to be enabled",
36    ))
37}
38
39/// Convert NOML Value to config-lib Value
40#[cfg(feature = "noml")]
41fn convert_noml_value(noml_value: noml::Value) -> Result<Value> {
42    match noml_value {
43        noml::Value::Null => Ok(Value::Null),
44        noml::Value::Bool(b) => Ok(Value::Bool(b)),
45        noml::Value::Integer(i) => Ok(Value::Integer(i)),
46        noml::Value::Float(f) => Ok(Value::Float(f)),
47        noml::Value::String(s) => Ok(Value::String(s)),
48        noml::Value::Array(arr) => {
49            let converted: Result<Vec<Value>> = arr.into_iter().map(convert_noml_value).collect();
50            Ok(Value::Array(converted?))
51        }
52        noml::Value::Table(table) => {
53            let mut converted = BTreeMap::new();
54            for (key, value) in table {
55                converted.insert(key, convert_noml_value(value)?);
56            }
57            Ok(Value::Table(converted))
58        }
59        #[cfg(feature = "chrono")]
60        noml::Value::DateTime(_) => Ok(Value::String("datetime_value".to_string())),
61        // Handle NOML-specific types by converting to basic types
62        noml::Value::Binary(_data) => {
63            // Convert binary data to base64 string for compatibility
64            #[cfg(feature = "base64")]
65            {
66                use base64::{engine::general_purpose, Engine as _};
67                Ok(Value::String(general_purpose::STANDARD.encode(_data)))
68            }
69            #[cfg(not(feature = "base64"))]
70            Ok(Value::String("<binary data>".to_string()))
71        }
72        noml::Value::Size(size) => {
73            // Convert size to integer (bytes)
74            Ok(Value::Integer(size as i64))
75        }
76        noml::Value::Duration(duration) => {
77            // Convert duration to float (seconds)
78            Ok(Value::Float(duration))
79        }
80    }
81}
82
83/// Parse NOML with format preservation for round-trip editing
84pub fn parse_with_preservation(source: &str) -> Result<(Value, noml::Document)> {
85    // Parse to get the AST document for format preservation
86    let document = noml::parse_string(source, None)?;
87
88    // Resolve to get the actual values
89    let mut resolver = noml::Resolver::new();
90    let resolved = resolver.resolve(&document)?;
91
92    // Convert to config-lib Value
93    let value = convert_noml_value(resolved)?;
94
95    Ok((value, document))
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_basic_noml() {
104        let config = parse(
105            r#"
106            name = "test"
107            port = 8080
108            debug = true
109        "#,
110        )
111        .unwrap();
112
113        assert_eq!(config.get("name").unwrap().as_string().unwrap(), "test");
114        assert_eq!(config.get("port").unwrap().as_integer().unwrap(), 8080);
115        assert!(config.get("debug").unwrap().as_bool().unwrap());
116    }
117
118    #[test]
119    fn test_noml_features() {
120        std::env::set_var("TEST_VAR", "hello");
121
122        let config = parse(
123            r#"
124            greeting = env("TEST_VAR", "world")
125            size = @size("10MB")
126            timeout = @duration("30s")
127        "#,
128        )
129        .unwrap();
130
131        assert_eq!(
132            config.get("greeting").unwrap().as_string().unwrap(),
133            "hello"
134        );
135        // Size converted to bytes
136        assert_eq!(config.get("size").unwrap().as_integer().unwrap(), 10485760);
137        // Duration converted to seconds
138        assert_eq!(config.get("timeout").unwrap().as_float().unwrap(), 30.0);
139    }
140}