agent-migrate-core 0.6.1

Core models and transactional filesystem engine for agent configuration migration
Documentation
use std::path::Path;

use serde_json::Value;

use crate::transaction::rename::resolve_renames;
use crate::transaction::{ConflictPolicy, PlannedWrite, TransactionError, WritePlan};

pub(super) fn resolve_plan(
    plan: &WritePlan,
    root: &Path,
    policy: ConflictPolicy,
) -> Result<WritePlan, TransactionError> {
    if policy == ConflictPolicy::Rename {
        return resolve_renames(plan, root);
    }
    let mut resolved = WritePlan::default();
    for write in plan.writes() {
        let target = root.join(write.path.as_path());
        if target.exists() && std::fs::read(&target).is_ok_and(|content| content == write.content) {
            continue;
        }
        let next = target
            .exists()
            .then(|| resolve_conflict(write, policy, &target))
            .transpose()?
            .unwrap_or_else(|| write.clone());
        resolved.insert(next);
    }
    Ok(resolved)
}

fn resolve_conflict(
    write: &PlannedWrite,
    policy: ConflictPolicy,
    target: &Path,
) -> Result<PlannedWrite, TransactionError> {
    match policy {
        ConflictPolicy::Fail => Err(TransactionError::Conflict {
            path: target.to_path_buf(),
        }),
        ConflictPolicy::Replace => Ok(write.clone()),
        ConflictPolicy::Merge => merge_write(write, target),
        ConflictPolicy::Rename => Err(TransactionError::UnsupportedConflictPolicy(
            ConflictPolicy::Rename,
        )),
    }
}

fn merge_write(write: &PlannedWrite, target: &Path) -> Result<PlannedWrite, TransactionError> {
    let existing = std::fs::read(target).map_err(|source| TransactionError::Io {
        path: target.to_path_buf(),
        source,
    })?;
    let extension = target.extension().and_then(std::ffi::OsStr::to_str);
    let content = match extension {
        Some("json") => merge_json(&existing, &write.content, target)?,
        Some("toml") => merge_toml(&existing, &write.content, target)?,
        Some("yaml" | "yml") => merge_yaml(&existing, &write.content, target)?,
        _ => {
            return Err(TransactionError::UnsupportedConflictPolicy(
                ConflictPolicy::Merge,
            ));
        }
    };
    Ok(PlannedWrite::new(write.path.clone(), content).with_mode(write.mode))
}

fn merge_json(existing: &[u8], incoming: &[u8], path: &Path) -> Result<Vec<u8>, TransactionError> {
    let mut target: Value =
        serde_json::from_slice(existing).map_err(|source| TransactionError::StructuredMerge {
            path: path.to_path_buf(),
            message: source.to_string(),
        })?;
    let source: Value =
        serde_json::from_slice(incoming).map_err(|source| TransactionError::StructuredMerge {
            path: path.to_path_buf(),
            message: source.to_string(),
        })?;
    deep_merge(&mut target, source);
    serde_json::to_vec_pretty(&target).map_err(|source| TransactionError::StructuredMerge {
        path: path.to_path_buf(),
        message: source.to_string(),
    })
}

fn merge_toml(existing: &[u8], incoming: &[u8], path: &Path) -> Result<Vec<u8>, TransactionError> {
    let target_text =
        std::str::from_utf8(existing).map_err(|source| TransactionError::StructuredMerge {
            path: path.to_path_buf(),
            message: source.to_string(),
        })?;
    let source_text =
        std::str::from_utf8(incoming).map_err(|source| TransactionError::StructuredMerge {
            path: path.to_path_buf(),
            message: source.to_string(),
        })?;
    let mut target: Value = toml_edit::de::from_str(target_text).map_err(|source| {
        TransactionError::StructuredMerge {
            path: path.to_path_buf(),
            message: source.to_string(),
        }
    })?;
    let source: Value = toml_edit::de::from_str(source_text).map_err(|source| {
        TransactionError::StructuredMerge {
            path: path.to_path_buf(),
            message: source.to_string(),
        }
    })?;
    deep_merge(&mut target, source);
    toml_edit::ser::to_string_pretty(&target)
        .map(String::into_bytes)
        .map_err(|source| TransactionError::StructuredMerge {
            path: path.to_path_buf(),
            message: source.to_string(),
        })
}

fn merge_yaml(existing: &[u8], incoming: &[u8], path: &Path) -> Result<Vec<u8>, TransactionError> {
    let mut target: Value =
        serde_yaml::from_slice(existing).map_err(|source| TransactionError::StructuredMerge {
            path: path.to_path_buf(),
            message: source.to_string(),
        })?;
    let source: Value =
        serde_yaml::from_slice(incoming).map_err(|source| TransactionError::StructuredMerge {
            path: path.to_path_buf(),
            message: source.to_string(),
        })?;
    deep_merge(&mut target, source);
    serde_yaml::to_string(&target)
        .map(String::into_bytes)
        .map_err(|source| TransactionError::StructuredMerge {
            path: path.to_path_buf(),
            message: source.to_string(),
        })
}

fn deep_merge(target: &mut Value, source: Value) {
    match (target, source) {
        (Value::Object(target), Value::Object(source)) => {
            for (key, value) in source {
                match target.get_mut(&key) {
                    Some(existing) => deep_merge(existing, value),
                    None => {
                        let _previous = target.insert(key, value);
                    }
                }
            }
        }
        (target, source) => *target = source,
    }
}