config-lib 0.9.0

Enterprise-grade multi-format configuration library supporting 8 formats (CONF, INI, Properties, JSON, XML, HCL, TOML, NOML) with sub-50ns caching, hot reloading, and comprehensive validation.
Documentation
use crate::error::{Error, Result};
use crate::value::Value;
use std::collections::BTreeMap;

/// Parse Properties format configuration
pub fn parse(source: &str) -> Result<Value> {
    let mut parser = PropertiesParser::new(source.to_string());
    parser.parse()
}

/// High-performance Java Properties format parser
///
/// Properties format specification:
/// - Simple key=value pairs (one per line)
/// - Comments start with # or !
/// - Supports : as separator alternative to =
/// - Backslash line continuation
/// - Unicode escapes (\uXXXX)
/// - Standard Java Properties format
///
/// Performance optimizations:
/// - String-based parsing for maximum speed
/// - Single pass parsing
/// - Minimal allocations
/// - Zero-copy where possible
pub struct PropertiesParser {
    input: String,
    position: usize,
    line: usize,
    column: usize,
}

impl PropertiesParser {
    /// Create a new Properties parser with the given input string
    pub fn new(input: String) -> Self {
        Self {
            input,
            position: 0,
            line: 1,
            column: 1,
        }
    }

    /// Parse the input string as Java Properties format
    pub fn parse(&mut self) -> Result<Value> {
        let mut properties = BTreeMap::new();

        while !self.at_end() {
            self.skip_whitespace_and_comments();

            if self.at_end() {
                break;
            }

            let (key, value) = self.parse_property()?;
            properties.insert(key, value);
        }

        Ok(Value::table(properties))
    }

    fn parse_property(&mut self) -> Result<(String, Value)> {
        let key = self.parse_key()?;
        self.skip_whitespace();

        // Expect separator (= or :)
        if self.current_char() != '=' && self.current_char() != ':' {
            return Err(Error::Parse {
                message: format!("Expected '=' or ':', found '{}'", self.current_char()),
                line: self.line,
                column: self.column,
                file: None,
            });
        }

        self.advance(); // Skip separator
        self.skip_whitespace();

        let value = self.parse_value()?;

        Ok((key, value))
    }

    fn parse_key(&mut self) -> Result<String> {
        let mut key = String::new();

        while !self.at_end() {
            let ch = self.current_char();

            match ch {
                '=' | ':' => break,
                '\\' => {
                    self.advance();
                    if self.at_end() {
                        return Err(Error::Parse {
                            message: "Unexpected end of input in key".to_string(),
                            line: self.line,
                            column: self.column,
                            file: None,
                        });
                    }

                    let escaped = self.parse_escape()?;
                    key.push_str(&escaped);
                }
                '\n' | '\r' => {
                    return Err(Error::Parse {
                        message: "Unexpected newline in key".to_string(),
                        line: self.line,
                        column: self.column,
                        file: None,
                    });
                }
                _ => {
                    key.push(ch);
                    self.advance();
                }
            }
        }

        if key.trim().is_empty() {
            return Err(Error::Parse {
                message: "Empty key name".to_string(),
                line: self.line,
                column: self.column,
                file: None,
            });
        }

        Ok(key.trim().to_string())
    }

    fn parse_value(&mut self) -> Result<Value> {
        let mut value = String::new();

        while !self.at_end() {
            let ch = self.current_char();

            match ch {
                '\\' => {
                    self.advance();
                    if self.at_end() {
                        break;
                    }

                    // Check for line continuation
                    if self.current_char() == '\n' || self.current_char() == '\r' {
                        self.skip_newline();
                        self.skip_whitespace();
                        continue;
                    }

                    let escaped = self.parse_escape()?;
                    value.push_str(&escaped);
                }
                '\n' | '\r' => break,
                _ => {
                    value.push(ch);
                    self.advance();
                }
            }
        }

        let trimmed = value.trim();
        Ok(self.infer_value_type(trimmed))
    }

    fn parse_escape(&mut self) -> Result<String> {
        let ch = self.current_char();
        self.advance();

        match ch {
            'n' => Ok("\n".to_string()),
            't' => Ok("\t".to_string()),
            'r' => Ok("\r".to_string()),
            '\\' => Ok("\\".to_string()),
            '=' => Ok("=".to_string()),
            ':' => Ok(":".to_string()),
            ' ' => Ok(" ".to_string()),
            'u' => self.parse_unicode_escape(),
            _ => Ok(ch.to_string()),
        }
    }

    fn parse_unicode_escape(&mut self) -> Result<String> {
        let mut hex_digits = String::new();

        for _ in 0..4 {
            if self.at_end() {
                return Err(Error::Parse {
                    message: "Incomplete unicode escape".to_string(),
                    line: self.line,
                    column: self.column,
                    file: None,
                });
            }

            let ch = self.current_char();
            if ch.is_ascii_hexdigit() {
                hex_digits.push(ch);
                self.advance();
            } else {
                return Err(Error::Parse {
                    message: format!("Invalid hex digit in unicode escape: '{ch}'"),
                    line: self.line,
                    column: self.column,
                    file: None,
                });
            }
        }

        let code_point = match u32::from_str_radix(&hex_digits, 16) {
            Ok(cp) => cp,
            Err(_) => {
                return Err(Error::Parse {
                    message: format!("Invalid hex digits in unicode escape: {hex_digits}"),
                    line: self.line,
                    column: self.column,
                    file: None,
                });
            }
        };
        if let Some(unicode_char) = char::from_u32(code_point) {
            Ok(unicode_char.to_string())
        } else {
            Err(Error::Parse {
                message: format!("Invalid unicode code point: {code_point}"),
                line: self.line,
                column: self.column,
                file: None,
            })
        }
    }

    fn infer_value_type(&self, value: &str) -> Value {
        if value.is_empty() {
            return Value::string(String::new());
        }

        // Boolean values (common in Java properties)
        match value.to_lowercase().as_str() {
            "true" => return Value::bool(true),
            "false" => return Value::bool(false),
            _ => {}
        }

        // Integer values
        if let Ok(int_val) = value.parse::<i64>() {
            return Value::integer(int_val);
        }

        // Float values
        if let Ok(float_val) = value.parse::<f64>() {
            return Value::float(float_val);
        }

        // Default to string
        Value::string(value.to_string())
    }

    fn skip_whitespace_and_comments(&mut self) {
        while !self.at_end() {
            match self.current_char() {
                ' ' | '\t' => self.advance(),
                '\n' | '\r' => self.skip_newline(),
                '#' | '!' => self.skip_comment(),
                _ => break,
            }
        }
    }

    fn skip_whitespace(&mut self) {
        while !self.at_end() && (self.current_char() == ' ' || self.current_char() == '\t') {
            self.advance();
        }
    }

    fn skip_comment(&mut self) {
        while !self.at_end() && self.current_char() != '\n' && self.current_char() != '\r' {
            self.advance();
        }
    }

    fn skip_newline(&mut self) {
        if !self.at_end() && self.current_char() == '\r' {
            self.advance();
        }
        if !self.at_end() && self.current_char() == '\n' {
            self.advance();
        }
    }

    fn current_char(&self) -> char {
        self.input.chars().nth(self.position).unwrap_or('\0')
    }

    fn at_end(&self) -> bool {
        self.position >= self.input.len()
    }

    fn advance(&mut self) {
        if !self.at_end() {
            if self.current_char() == '\n' {
                self.line += 1;
                self.column = 1;
            } else {
                self.column += 1;
            }
            self.position += 1;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_simple_properties() {
        let input = "key1=value1\nkey2=123\nbool_key=true";
        let mut parser = PropertiesParser::new(input.to_string());
        let result = parser.parse().unwrap();

        if let Value::Table(table) = result {
            assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
            assert_eq!(table.get("key2").unwrap().as_integer().unwrap(), 123);
            assert!(table.get("bool_key").unwrap().as_bool().unwrap());
        }
    }

    #[test]
    fn test_comments() {
        let input = "# This is a comment\nkey1=value1\n! Another comment\nkey2=value2";
        let mut parser = PropertiesParser::new(input.to_string());
        let result = parser.parse().unwrap();

        if let Value::Table(table) = result {
            assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
            assert_eq!(table.get("key2").unwrap().as_string().unwrap(), "value2");
        }
    }

    #[test]
    fn test_escape_sequences() {
        let input = r"key1=line1\nline2\ttab";
        let mut parser = PropertiesParser::new(input.to_string());
        let result = parser.parse().unwrap();

        if let Value::Table(table) = result {
            assert_eq!(
                table.get("key1").unwrap().as_string().unwrap(),
                "line1\nline2\ttab"
            );
        }
    }

    #[test]
    fn test_colon_separator() {
        let input = "key1:value1\nkey2: value2";
        let mut parser = PropertiesParser::new(input.to_string());
        let result = parser.parse().unwrap();

        if let Value::Table(table) = result {
            assert_eq!(table.get("key1").unwrap().as_string().unwrap(), "value1");
            assert_eq!(table.get("key2").unwrap().as_string().unwrap(), "value2");
        }
    }
}