From b8c49d4c136c549549efa7a9febdcf87b2951c0e Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Wed, 18 Mar 2026 16:44:59 +0100 Subject: [PATCH] unified merge --- Cargo.lock | 2 +- src/format/env.rs | 76 ++++++++++---------------------------- src/format/hierarchical.rs | 37 ------------------- src/format/ini.rs | 32 ---------------- src/format/mod.rs | 37 ++++++++++++++++++- src/format/properties.rs | 32 ---------------- 6 files changed, 55 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 411fc4b..668debc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -914,7 +914,7 @@ dependencies = [ [[package]] name = "mould" -version = "0.4.3" +version = "0.5.0" dependencies = [ "anyhow", "clap", diff --git a/src/format/env.rs b/src/format/env.rs index 5277283..924c3e7 100644 --- a/src/format/env.rs +++ b/src/format/env.rs @@ -35,53 +35,6 @@ impl FormatHandler for EnvHandler { Ok(vars) } - fn merge(&self, path: &Path, vars: &mut Vec) -> io::Result<()> { - if !path.exists() { - return Ok(()); - } - - let content = fs::read_to_string(path)?; - for line in content.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - - if let Some((key, val)) = line.split_once('=') { - let key = key.trim(); - let parsed_val = val.trim().trim_matches('"').trim_matches('\'').to_string(); - - if let Some(var) = vars.iter_mut().find(|v| v.key == key) { - if var.value.as_deref() != Some(&parsed_val) { - var.value = Some(parsed_val); - var.status = ItemStatus::Modified; - } - } else { - vars.push(ConfigItem { - key: key.to_string(), - path: key.to_string(), - value: Some(parsed_val), - template_value: None, - default_value: None, - depth: 0, - is_group: false, - status: ItemStatus::MissingFromTemplate, - value_type: ValueType::String, - }); - } - } - } - - // Mark missing from active - for var in vars.iter_mut() { - if var.status == ItemStatus::Present && var.value.is_none() { - var.status = ItemStatus::MissingFromActive; - } - } - - Ok(()) - } - fn write(&self, path: &Path, vars: &[ConfigItem]) -> io::Result<()> { let mut file = fs::File::create(path)?; for var in vars { @@ -126,27 +79,36 @@ mod tests { #[test] fn test_merge_env() { - let mut example_file = NamedTempFile::new().unwrap(); - writeln!(example_file, "KEY1=default1\nKEY2=default2").unwrap(); let handler = EnvHandler; - let mut vars = handler.parse(example_file.path()).unwrap(); let mut env_file = NamedTempFile::new().unwrap(); writeln!(env_file, "KEY1=custom1\nKEY3=custom3").unwrap(); + let mut vars = handler.parse(env_file.path()).unwrap(); // Active vars - handler.merge(env_file.path(), &mut vars).unwrap(); + let mut example_file = NamedTempFile::new().unwrap(); + writeln!(example_file, "KEY1=default1\nKEY2=default2").unwrap(); + handler.merge(example_file.path(), &mut vars).unwrap(); // Merge template into active + + // Should preserve order of active, then append template assert_eq!(vars.len(), 3); + + // Active key that exists in template assert_eq!(vars[0].key, "KEY1"); - assert_eq!(vars[0].value.as_deref(), Some("custom1")); + assert_eq!(vars[0].value.as_deref(), Some("custom1")); // Keeps active value + assert_eq!(vars[0].template_value.as_deref(), Some("default1")); // Gets template default assert_eq!(vars[0].status, ItemStatus::Modified); - assert_eq!(vars[1].key, "KEY2"); - assert_eq!(vars[1].value.as_deref(), Some("default2")); + // Active key that DOES NOT exist in template + assert_eq!(vars[1].key, "KEY3"); + assert_eq!(vars[1].value.as_deref(), Some("custom3")); + assert_eq!(vars[1].status, ItemStatus::Present); - assert_eq!(vars[2].key, "KEY3"); - assert_eq!(vars[2].value.as_deref(), Some("custom3")); - assert_eq!(vars[2].status, ItemStatus::MissingFromTemplate); + // Template key that DOES NOT exist in active + assert_eq!(vars[2].key, "KEY2"); + assert_eq!(vars[2].value.as_deref(), None); // Missing from active + assert_eq!(vars[2].template_value.as_deref(), Some("default2")); + assert_eq!(vars[2].status, ItemStatus::MissingFromActive); } #[test] diff --git a/src/format/hierarchical.rs b/src/format/hierarchical.rs index 3efd862..a50ebc8 100644 --- a/src/format/hierarchical.rs +++ b/src/format/hierarchical.rs @@ -283,43 +283,6 @@ impl FormatHandler for HierarchicalHandler { Ok(vars) } - fn merge(&self, path: &Path, vars: &mut Vec) -> io::Result<()> { - if !path.exists() { - return Ok(()); - } - let existing_value = self.read_value(path)?; - let mut existing_vars = Vec::new(); - flatten(&existing_value, "", 0, "", &mut existing_vars); - - 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; - } - } - - // Find keys in active that are not in template - let mut to_add = Vec::new(); - 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; - to_add.push(new_item); - } - } - - // Basic insertion logic for extra keys (could be improved to insert at correct depth/position) - vars.extend(to_add); - - Ok(()) - } - fn write(&self, path: &Path, vars: &[ConfigItem]) -> io::Result<()> { let mut root = Value::Object(Map::new()); for var in vars { diff --git a/src/format/ini.rs b/src/format/ini.rs index 898beee..dbda8d7 100644 --- a/src/format/ini.rs +++ b/src/format/ini.rs @@ -52,38 +52,6 @@ impl FormatHandler for IniHandler { 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 { diff --git a/src/format/mod.rs b/src/format/mod.rs index 9a83531..a86d35e 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -10,7 +10,6 @@ pub mod properties; pub enum ItemStatus { Present, MissingFromActive, - MissingFromTemplate, Modified, } @@ -48,7 +47,41 @@ pub enum FormatType { pub trait FormatHandler { fn parse(&self, path: &Path) -> io::Result>; - fn merge(&self, path: &Path, vars: &mut Vec) -> io::Result<()>; + fn merge(&self, path: &Path, vars: &mut Vec) -> io::Result<()> { + if !path.exists() { + return Ok(()); + } + + let template_vars = self.parse(path).unwrap_or_default(); + + for var in vars.iter_mut() { + if let Some(template_var) = template_vars.iter().find(|v| v.path == var.path) { + var.template_value = template_var.value.clone(); + var.default_value = template_var.value.clone(); + + if var.value != template_var.value { + var.status = ItemStatus::Modified; + } else { + var.status = ItemStatus::Present; + } + } else { + // Exists in active, but not in template + var.status = ItemStatus::Present; + } + } + + // Add items from template that are missing in active + for template_var in template_vars { + if !vars.iter().any(|v| v.path == template_var.path) { + let mut new_item = template_var.clone(); + new_item.status = ItemStatus::MissingFromActive; + new_item.value = None; + vars.push(new_item); + } + } + + Ok(()) + } fn write(&self, path: &Path, vars: &[ConfigItem]) -> io::Result<()>; } diff --git a/src/format/properties.rs b/src/format/properties.rs index efe567f..31f7179 100644 --- a/src/format/properties.rs +++ b/src/format/properties.rs @@ -61,38 +61,6 @@ impl FormatHandler for PropertiesHandler { 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(vars.sort_by(|a, b| a.path.cmp(&b.path))) - } - fn write(&self, path: &Path, vars: &[ConfigItem]) -> io::Result<()> { let mut props = HashMap::new(); for var in vars {