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(dt) => Ok(Value::DateTime(dt)),
61        #[cfg(not(feature = "chrono"))]
62        noml::Value::DateTime(_) => Ok(Value::String(
63            "datetime not supported without chrono feature".to_string(),
64        )),
65        noml::Value::Binary(_data) => {
66            // Convert binary data to base64 string for compatibility
67            #[cfg(feature = "base64")]
68            {
69                use base64::{engine::general_purpose, Engine as _};
70                Ok(Value::String(general_purpose::STANDARD.encode(_data)))
71            }
72            #[cfg(not(feature = "base64"))]
73            Ok(Value::String("<binary data>".to_string()))
74        }
75        noml::Value::Size(size) => {
76            // Convert size to integer (bytes)
77            Ok(Value::Integer(size as i64))
78        }
79        noml::Value::Duration(duration) => {
80            // Convert duration to float (seconds)
81            Ok(Value::Float(duration))
82        }
83    }
84}
85
86/// Parse NOML with format preservation for round-trip editing
87pub fn parse_with_preservation(source: &str) -> Result<(Value, noml::Document)> {
88    // Parse to get the AST document for format preservation
89    let document = noml::parse_string(source, None)?;
90
91    // Resolve to get the actual values
92    let mut resolver = noml::Resolver::new();
93    let resolved = resolver.resolve(&document)?;
94
95    // Convert to config-lib Value
96    let value = convert_noml_value(resolved)?;
97
98    Ok((value, document))
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_basic_noml() {
107        let config = parse(
108            r#"
109            name = "test"
110            port = 8080
111            debug = true
112        "#,
113        )
114        .unwrap();
115
116        assert_eq!(config.get("name").unwrap().as_string().unwrap(), "test");
117        assert_eq!(config.get("port").unwrap().as_integer().unwrap(), 8080);
118        assert!(config.get("debug").unwrap().as_bool().unwrap());
119    }
120
121    #[test]
122    fn test_noml_features() {
123        std::env::set_var("TEST_VAR", "hello");
124
125        let config = parse(
126            r#"
127            greeting = env("TEST_VAR", "world")
128            size = @size("10MB")
129            timeout = @duration("30s")
130        "#,
131        )
132        .unwrap();
133
134        assert_eq!(
135            config.get("greeting").unwrap().as_string().unwrap(),
136            "hello"
137        );
138        // Size converted to bytes
139        assert_eq!(config.get("size").unwrap().as_integer().unwrap(), 10485760);
140        // Duration converted to seconds
141        assert_eq!(config.get("timeout").unwrap().as_float().unwrap(), 30.0);
142    }
143}