use super::{ConfigItem, FormatHandler, ItemStatus, ValueType}; use ini::Ini; use std::io; use std::path::Path; pub struct IniHandler; impl FormatHandler for IniHandler { fn parse(&self, path: &Path) -> io::Result> { let conf = Ini::load_from_file(path) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; let mut vars = Vec::new(); for (section, prop) in &conf { let section_name = section.unwrap_or_default(); if !section_name.is_empty() { vars.push(ConfigItem { key: section_name.to_string(), path: section_name.to_string(), value: None, template_value: None, default_value: None, depth: 0, is_group: true, status: ItemStatus::Present, value_type: ValueType::Null, }); } for (key, value) in prop { let path = if section_name.is_empty() { key.to_string() } else { format!("{}.{}", section_name, key) }; vars.push(ConfigItem { key: key.to_string(), path, value: Some(value.to_string()), template_value: Some(value.to_string()), default_value: Some(value.to_string()), depth: if section_name.is_empty() { 0 } else { 1 }, is_group: false, status: ItemStatus::Present, value_type: ValueType::String, }); } } Ok(vars) } fn merge(&self, path: &Path, vars: &mut Vec) -> io::Result<()> { if !path.exists() { return Ok(()); } let existing_vars = self.parse(path).unwrap_or_default(); for var in vars.iter_mut() { if let Some(existing) = existing_vars.iter().find(|v| v.path == var.path) { if var.value != existing.value { var.value = existing.value.clone(); var.status = ItemStatus::Modified; } } else { var.status = ItemStatus::MissingFromActive; } } // Add items from active that are not in template for existing in existing_vars { if !vars.iter().any(|v| v.path == existing.path) { let mut new_item = existing.clone(); new_item.status = ItemStatus::MissingFromTemplate; new_item.template_value = None; new_item.default_value = None; vars.push(new_item); } } Ok(()) } fn write(&self, path: &Path, vars: &[ConfigItem]) -> io::Result<()> { let mut conf = Ini::new(); for var in vars { if !var.is_group { let val = var.value.as_deref() .or(var.template_value.as_deref()) .unwrap_or(""); if let Some((section, key)) = var.path.split_once('.') { conf.with_section(Some(section)).set(key, val); } else { conf.with_section(None::).set(&var.path, val); } } } conf.write_to_file(path) .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) } } #[cfg(test)] mod tests { use super::*; use tempfile::NamedTempFile; use std::io::Write; #[test] fn test_parse_ini() { let mut file = NamedTempFile::new().unwrap(); writeln!(file, "[server]\nport=8080\n[database]\nhost=localhost").unwrap(); let handler = IniHandler; let vars = handler.parse(file.path()).unwrap(); assert!(vars.iter().any(|v| v.path == "server" && v.is_group)); assert!(vars.iter().any(|v| v.path == "server.port" && v.value.as_deref() == Some("8080"))); assert!(vars.iter().any(|v| v.path == "database.host" && v.value.as_deref() == Some("localhost"))); } }