naru-config 0.7.0

A security-first configuration manager with encryption and audit logging
Documentation
use crate::core::config_model::ConfigFile;
use anyhow::Result;

pub trait ConfigFormat: Send + Sync {
    fn name(&self) -> &str;
    fn extension(&self) -> &str;
    fn serialize(&self, config: &ConfigFile) -> Result<String>;
    fn deserialize(&self, data: &str) -> Result<ConfigFile>;
}

pub trait FormatDetector {
    fn detect_format(data: &str) -> Option<&'static str>;
}

pub fn detect_format_from_extension(extension: &str) -> Option<&'static str> {
    match extension.to_lowercase().as_str() {
        "json" => Some("json"),
        "toml" => Some("toml"),
        "yaml" | "yml" => Some("yaml"),
        "env" => Some("env"),
        "properties" => Some("properties"),
        _ => None,
    }
}

pub fn detect_format_from_content(content: &str) -> Option<&'static str> {
    let trimmed = content.trim();

    if trimmed.starts_with('{') || trimmed.starts_with('[') {
        return Some("json");
    }

    if trimmed.starts_with("---") || trimmed.contains(": ") {
        return Some("yaml");
    }

    if trimmed.contains('=')
        && trimmed
            .lines()
            .all(|l| l.contains('=') || l.trim().is_empty() || l.trim().starts_with('#'))
    {
        return Some("env");
    }

    if trimmed.contains('[') && trimmed.contains(']') {
        return Some("toml");
    }

    None
}

use std::fs;
use std::path::Path;

pub fn save_config_as_format<P: AsRef<Path>>(
    path: P,
    config: &ConfigFile,
    format: &dyn ConfigFormat,
) -> Result<()> {
    let serialized = format.serialize(config)?;
    fs::write(path, serialized)?;
    Ok(())
}

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

    #[test]
    fn test_detect_format_from_extension() {
        assert_eq!(detect_format_from_extension("json"), Some("json"));
        assert_eq!(detect_format_from_extension("JSON"), Some("json"));
        assert_eq!(detect_format_from_extension("toml"), Some("toml"));
        assert_eq!(detect_format_from_extension("yaml"), Some("yaml"));
        assert_eq!(detect_format_from_extension("yml"), Some("yaml"));
        assert_eq!(detect_format_from_extension("env"), Some("env"));
        assert_eq!(detect_format_from_extension("unknown"), None);
    }

    #[test]
    fn test_detect_format_from_content_json() {
        let json = r#"{"key": "value"}"#;
        assert_eq!(detect_format_from_content(json), Some("json"));

        let json_array = r#"[1, 2, 3]"#;
        assert_eq!(detect_format_from_content(json_array), Some("json"));
    }

    #[test]
    fn test_detect_format_from_content_yaml() {
        let yaml = "key: value\nfoo: bar";
        assert_eq!(detect_format_from_content(yaml), Some("yaml"));

        let yaml_with_doc = "---\nkey: value";
        assert_eq!(detect_format_from_content(yaml_with_doc), Some("yaml"));
    }

    #[test]
    fn test_detect_format_from_content_env() {
        let env = "KEY=value\nFOO=bar";
        assert_eq!(detect_format_from_content(env), Some("env"));

        let env_with_comments = "# Comment\nKEY=value";
        assert_eq!(detect_format_from_content(env_with_comments), Some("env"));
    }

    #[test]
    fn test_detect_format_from_content_toml() {
        let toml = "[section]\nkey = \"value\"";
        assert_eq!(detect_format_from_content(toml), Some("toml"));
    }
}