naru-config 0.7.0

A security-first configuration manager with encryption and audit logging
Documentation
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ConfigFile {
    pub project_name: String,
    pub version: String,
    pub environments: HashMap<String, EnvironmentConfig>,
    pub salt: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct EnvironmentConfig {
    pub parent: Option<String>,
    pub entries: HashMap<String, ConfigValueEntry>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ConfigValueEntry {
    pub value: String,
    pub r#type: String,
    pub is_secret: bool,
    pub encrypted: bool,
}

impl ConfigValueEntry {
    pub fn new(value: &str, r#type: &str, is_secret: bool) -> Self {
        ConfigValueEntry {
            value: value.to_string(),
            r#type: r#type.to_string(),
            is_secret,
            encrypted: false,
        }
    }

    pub fn validate(
        &self,
        field: &crate::core::schema_model::FieldDefinition,
    ) -> Result<(), String> {
        crate::core::validator::validate_value(&self.value, field)
    }
}

impl ConfigFile {
    pub fn new(project_name: &str) -> Self {
        ConfigFile {
            project_name: project_name.to_string(),
            version: "1.0.0".to_string(),
            environments: HashMap::new(),
            salt: None,
        }
    }

    pub fn add_environment(&mut self, name: &str) -> &mut EnvironmentConfig {
        self.environments
            .entry(name.to_string())
            .or_insert_with(|| EnvironmentConfig::default())
    }

    pub fn get_environment(&self, name: &str) -> Option<&EnvironmentConfig> {
        self.environments.get(name)
    }

    pub fn get_environment_mut(&mut self, name: &str) -> Option<&mut EnvironmentConfig> {
        self.environments.get_mut(name)
    }
}

impl EnvironmentConfig {
    pub fn new() -> Self {
        EnvironmentConfig {
            parent: None,
            entries: HashMap::new(),
        }
    }

    pub fn with_parent(parent: &str) -> Self {
        EnvironmentConfig {
            parent: Some(parent.to_string()),
            entries: HashMap::new(),
        }
    }

    pub fn set_value(&mut self, key: &str, value: ConfigValueEntry) {
        self.entries.insert(key.to_string(), value);
    }

    pub fn get_value(&self, key: &str) -> Option<&ConfigValueEntry> {
        self.entries.get(key)
    }

    pub fn remove_value(&mut self, key: &str) -> Option<ConfigValueEntry> {
        self.entries.remove(key)
    }

    pub fn keys(&self) -> impl Iterator<Item = &String> {
        self.entries.keys()
    }

    pub fn values(&self) -> impl Iterator<Item = &ConfigValueEntry> {
        self.entries.values()
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    pub fn len(&self) -> usize {
        self.entries.len()
    }
}

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

    #[test]
    fn test_config_value_entry_new() {
        let entry = ConfigValueEntry::new("test_value", "string", true);
        assert_eq!(entry.value, "test_value");
        assert_eq!(entry.r#type, "string");
        assert!(entry.is_secret);
        assert!(!entry.encrypted);
    }

    #[test]
    fn test_config_file_new() {
        let config = ConfigFile::new("TestProject");
        assert_eq!(config.project_name, "TestProject");
        assert_eq!(config.version, "1.0.0");
        assert!(config.environments.is_empty());
    }

    #[test]
    fn test_environment_config_new() {
        let env = EnvironmentConfig::new();
        assert!(env.parent.is_none());
        assert!(env.entries.is_empty());
    }

    #[test]
    fn test_environment_config_with_parent() {
        let env = EnvironmentConfig::with_parent("development");
        assert_eq!(env.parent, Some("development".to_string()));
    }

    #[test]
    fn test_config_file_add_environment() {
        let mut config = ConfigFile::new("Test");
        config.add_environment("production");
        assert!(config.environments.contains_key("production"));
    }

    #[test]
    fn test_config_value_entry_validate_mismatch() {
        let entry = ConfigValueEntry::new("not_an_int", "integer", false);
        let field = crate::core::schema_model::FieldDefinition {
            key: "test".into(),
            r#type: "integer".into(),
            description: None,
            validation: None,
            is_secret: false,
        };
        assert!(entry.validate(&field).is_err());
    }

    #[test]
    fn test_config_file_clone() {
        let mut environments = HashMap::new();
        let mut entries = HashMap::new();
        entries.insert("K1".into(), ConfigValueEntry::new("V1", "string", false));
        environments.insert(
            "dev".into(),
            EnvironmentConfig {
                parent: None,
                entries,
            },
        );

        let config = ConfigFile {
            project_name: "Test".into(),
            version: "1.0".into(),
            environments,
            salt: None,
        };

        let cloned = config.clone();
        assert_eq!(cloned.project_name, config.project_name);
        assert_eq!(cloned.environments.len(), config.environments.len());
    }

    #[test]
    fn test_empty_config_value_entry() {
        let entry = ConfigValueEntry::new("", "string", false);
        assert_eq!(entry.value, "");
    }

    #[test]
    fn test_environment_set_and_get_value() {
        let mut env = EnvironmentConfig::new();
        env.set_value("KEY1", ConfigValueEntry::new("value1", "string", false));

        assert!(env.get_value("KEY1").is_some());
        assert_eq!(env.get_value("KEY1").unwrap().value, "value1");
    }

    #[test]
    fn test_environment_remove_value() {
        let mut env = EnvironmentConfig::new();
        env.set_value("KEY1", ConfigValueEntry::new("value1", "string", false));

        let removed = env.remove_value("KEY1");
        assert!(removed.is_some());
        assert!(env.get_value("KEY1").is_none());
    }

    #[test]
    fn test_environment_len_and_is_empty() {
        let mut env = EnvironmentConfig::new();
        assert!(env.is_empty());
        assert_eq!(env.len(), 0);

        env.set_value("KEY1", ConfigValueEntry::new("value1", "string", false));
        assert!(!env.is_empty());
        assert_eq!(env.len(), 1);
    }
}