unified merge
All checks were successful
Version Check / check-version (pull_request) Successful in 3s

This commit is contained in:
2026-03-18 16:44:59 +01:00
parent b88d93c800
commit b8c49d4c13
6 changed files with 55 additions and 161 deletions

2
Cargo.lock generated
View File

@@ -914,7 +914,7 @@ dependencies = [
[[package]]
name = "mould"
version = "0.4.3"
version = "0.5.0"
dependencies = [
"anyhow",
"clap",

View File

@@ -35,53 +35,6 @@ impl FormatHandler for EnvHandler {
Ok(vars)
}
fn merge(&self, path: &Path, vars: &mut Vec<ConfigItem>) -> 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]

View File

@@ -283,43 +283,6 @@ impl FormatHandler for HierarchicalHandler {
Ok(vars)
}
fn merge(&self, path: &Path, vars: &mut Vec<ConfigItem>) -> 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 {

View File

@@ -52,38 +52,6 @@ impl FormatHandler for IniHandler {
Ok(vars)
}
fn merge(&self, path: &Path, vars: &mut Vec<ConfigItem>) -> 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 {

View File

@@ -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<Vec<ConfigItem>>;
fn merge(&self, path: &Path, vars: &mut Vec<ConfigItem>) -> io::Result<()>;
fn merge(&self, path: &Path, vars: &mut Vec<ConfigItem>) -> 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<()>;
}

View File

@@ -61,38 +61,6 @@ impl FormatHandler for PropertiesHandler {
Ok(vars)
}
fn merge(&self, path: &Path, vars: &mut Vec<ConfigItem>) -> 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 {