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,
}
}