use serde_json::Value;
use toml_edit::Item;
use super::{
is_toml,
json::{indent_of, is_tool_entry, json_text, parse_json},
op::{MergeOp, Op},
parse_toml,
toml::members,
};
use crate::Result;
fn get_mut<'a>(doc: &'a mut Value, path: &[String]) -> Option<&'a mut Value> {
path.iter()
.try_fold(doc, |v, k| v.as_object_mut()?.get_mut(k))
}
fn is_empty_container(v: &Value) -> bool {
match v {
Value::Array(a) => a.is_empty(),
Value::Object(o) => o.is_empty(),
_ => false,
}
}
fn remove_json(doc: &mut Value, op: &MergeOp) {
let Some(target) = get_mut(doc, op.path()) else {
return;
};
match &op.0 {
Op::ArrayEntry { value, .. } => {
if let Some(a) = target.as_array_mut() {
a.retain(|v| v != value);
}
}
Op::ObjectMember { key, .. } => {
if let Some(o) = target.as_object_mut() {
o.shift_remove(key);
}
}
Op::OwnedEntries { field, owned, .. } => {
if let Some(a) = target.as_array_mut() {
a.retain(|e| !is_tool_entry(e, field, owned));
}
}
Op::GroupEntries {
entries,
field,
owned,
..
} => {
if let Some(groups) = target.as_array_mut() {
groups.retain_mut(|g| {
let Some(es) = g[entries.as_str()].as_array_mut() else {
return true;
};
let before = es.len();
es.retain(|e| !is_tool_entry(e, field, owned));
!(es.len() < before && es.is_empty())
});
}
}
}
prune_json(doc, op.path());
}
fn prune_json(doc: &mut Value, path: &[String]) {
for depth in (1..=path.len()).rev() {
let Some(parent) = get_mut(doc, &path[..depth - 1]).and_then(Value::as_object_mut) else {
return;
};
if parent.get(&path[depth - 1]).is_some_and(is_empty_container) {
parent.shift_remove(&path[depth - 1]);
} else {
return;
}
}
}
fn render_json_unmerge(existing: &str, ops: &[MergeOp], display: &str) -> Result<Option<String>> {
let mut doc = parse_json(display, existing)?;
let before = doc.clone();
for op in ops {
remove_json(&mut doc, op);
}
if doc == before {
return Ok(None);
}
if is_empty_container(&doc) {
return Ok(Some(String::new()));
}
let out = json_text(&doc, &indent_of(existing), existing.ends_with('\n'));
Ok((out != existing).then_some(out))
}
fn render_toml_unmerge(existing: &str, ops: &[MergeOp], display: &str) -> Result<Option<String>> {
let mut doc = parse_toml(display, existing)?;
for op in ops {
let (path, key, _) = members(op)?;
remove_toml(doc.as_table_mut(), path, key);
}
let out = doc.to_string();
if out == existing {
return Ok(None);
}
Ok(Some(if out.trim().is_empty() {
String::new()
} else {
out
}))
}
fn remove_toml(table: &mut toml_edit::Table, path: &[String], key: &str) {
let Some((first, rest)) = path.split_first() else {
table.remove(key);
return;
};
let Some(Item::Table(child)) = table.get_mut(first) else {
return;
};
remove_toml(child, rest, key);
if child.is_empty() {
table.remove(first);
}
}
pub(crate) fn render_unmerge(
file: &str,
existing: &str,
ops: &[MergeOp],
) -> Result<Option<String>> {
if is_toml(file) {
render_toml_unmerge(existing, ops, file)
} else {
render_json_unmerge(existing, ops, file)
}
}
#[cfg(test)]
mod tests;