pub(crate) fn apply_value_diff(
item: &mut toml_edit::Item,
old: &serde_json::Value,
new: &serde_json::Value,
) {
if old == new {
return;
}
match (old, new) {
(serde_json::Value::Object(old_map), serde_json::Value::Object(new_map)) => {
let table = if let Some(t) = item.as_table_mut() {
t
} else if let Some(inline) = item.as_inline_table_mut() {
let removed: Vec<String> = old_map
.keys()
.filter(|k| !new_map.contains_key(k.as_str()))
.cloned()
.collect();
for k in &removed {
inline.remove(k);
}
for (key, new_val) in new_map {
if let Some(old_val) = old_map.get(key) {
if old_val != new_val {
inline.insert(key, json_to_toml_value(new_val));
}
} else {
inline.insert(key, json_to_toml_value(new_val));
}
}
return;
} else {
*item = json_to_toml_item(new);
return;
};
let removed: Vec<String> = old_map
.keys()
.filter(|k| !new_map.contains_key(k.as_str()))
.cloned()
.collect();
for k in &removed {
table.remove(k);
}
for (key, new_val) in new_map {
if let Some(old_val) = old_map.get(key) {
if old_val != new_val
&& let Some(child) = table.get_mut(key)
{
apply_value_diff(child, old_val, new_val);
}
} else {
table.insert(key, json_to_toml_item(new_val));
}
}
}
(serde_json::Value::Array(old_arr), serde_json::Value::Array(new_arr))
if old_arr.len() == new_arr.len() =>
{
if let Some(arr) = item.as_array_mut() {
for (i, (o, n)) in old_arr.iter().zip(new_arr.iter()).enumerate() {
if o != n
&& let Some(v) = arr.get_mut(i)
{
*v = json_to_toml_value(n);
}
}
} else if let Some(aot) = item.as_array_of_tables_mut() {
for (i, (o, n)) in old_arr.iter().zip(new_arr.iter()).enumerate() {
if o != n
&& let Some(table_item) = aot.get_mut(i)
{
let mut tbl_item = toml_edit::Item::Table(table_item.clone());
apply_value_diff(&mut tbl_item, o, n);
if let toml_edit::Item::Table(t) = tbl_item {
*table_item = t;
}
}
}
} else {
*item = json_to_toml_item(new);
}
}
_ => {
*item = json_to_toml_item(new);
}
}
}
fn json_to_toml_value(val: &serde_json::Value) -> toml_edit::Value {
match val {
serde_json::Value::String(s) => toml_edit::Value::from(s.as_str()),
serde_json::Value::Bool(b) => toml_edit::Value::from(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
toml_edit::Value::from(i)
} else {
toml_edit::Value::from(n.as_f64().unwrap_or(0.0))
}
}
serde_json::Value::Array(arr) => {
let mut a = toml_edit::Array::new();
for v in arr {
a.push(json_to_toml_value(v));
}
toml_edit::Value::Array(a)
}
serde_json::Value::Object(map) => {
let mut t = toml_edit::InlineTable::new();
for (k, v) in map {
t.insert(k, json_to_toml_value(v));
}
toml_edit::Value::InlineTable(t)
}
serde_json::Value::Null => {
eprintln!("warning: TOML has no null type; value converted to empty string");
toml_edit::Value::from("")
}
}
}
fn json_to_toml_item(val: &serde_json::Value) -> toml_edit::Item {
match val {
serde_json::Value::Object(map) => {
let mut table = toml_edit::Table::new();
for (k, v) in map {
table.insert(k, json_to_toml_item(v));
}
toml_edit::Item::Table(table)
}
serde_json::Value::Array(arr) if !arr.is_empty() && arr.iter().all(|v| v.is_object()) => {
let mut aot = toml_edit::ArrayOfTables::new();
for v in arr {
if let serde_json::Value::Object(map) = v {
let mut table = toml_edit::Table::new();
for (k, v2) in map {
table.insert(k, json_to_toml_item(v2));
}
aot.push(table);
}
}
toml_edit::Item::ArrayOfTables(aot)
}
_ => toml_edit::Item::Value(json_to_toml_value(val)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_toml(s: &str) -> toml_edit::DocumentMut {
s.parse::<toml_edit::DocumentMut>().unwrap()
}
fn json(s: &str) -> serde_json::Value {
serde_json::from_str(s).unwrap()
}
#[test]
fn apply_value_diff_updates_scalar() {
let mut doc = parse_toml("key = \"old\"\n");
let old = json(r#"{"key": "old"}"#);
let new = json(r#"{"key": "new"}"#);
apply_value_diff(doc.as_item_mut(), &old, &new);
let result = doc.to_string();
assert!(
result.contains("\"new\""),
"scalar should be updated: {result}"
);
}
#[test]
fn apply_value_diff_removes_deleted_key() {
let mut doc = parse_toml("a = 1\nb = 2\n");
let old = json(r#"{"a": 1, "b": 2}"#);
let new = json(r#"{"a": 1}"#);
apply_value_diff(doc.as_item_mut(), &old, &new);
let result = doc.to_string();
assert!(
!result.contains("b ="),
"removed key should be gone: {result}"
);
assert!(result.contains("a = 1"));
}
#[test]
fn apply_value_diff_adds_new_key() {
let mut doc = parse_toml("a = 1\n");
let old = json(r#"{"a": 1}"#);
let new = json(r#"{"a": 1, "c": 3}"#);
apply_value_diff(doc.as_item_mut(), &old, &new);
let result = doc.to_string();
assert!(
result.contains("c = 3"),
"new key 'c = 3' should appear: {result}"
);
}
#[test]
fn apply_value_diff_noop_on_equal() {
let original = "key = \"same\"\n";
let mut doc = parse_toml(original);
let val = json(r#"{"key": "same"}"#);
apply_value_diff(doc.as_item_mut(), &val, &val);
assert_eq!(doc.to_string(), original);
}
#[test]
fn apply_value_diff_same_length_array() {
let mut doc = parse_toml("arr = [1, 2, 3]\n");
let old = json(r#"{"arr": [1, 2, 3]}"#);
let new = json(r#"{"arr": [1, 99, 3]}"#);
apply_value_diff(doc.as_item_mut(), &old, &new);
let result = doc.to_string();
assert!(
result.contains(", 99,") || result.contains("[99,") || result.contains(", 99]"),
"array element 99 should appear in array context: {result}"
);
assert!(
!result.contains(", 2, 3]"),
"old array layout should not persist: {result}"
);
}
#[test]
fn apply_value_diff_different_length_array_replaces() {
let mut doc = parse_toml("arr = [1, 2]\n");
let old = json(r#"{"arr": [1, 2]}"#);
let new = json(r#"{"arr": [1, 2, 3]}"#);
apply_value_diff(doc.as_item_mut(), &old, &new);
let result = doc.to_string();
assert!(
result.contains("1, 2, 3"),
"extended array [1, 2, 3] should appear: {result}"
);
}
#[test]
fn apply_value_diff_preserves_inline_table_format() {
let original = "options = { debug = true, verbose = false }\n";
let mut doc = parse_toml(original);
let old = json(r#"{"options": {"debug": true, "verbose": false}}"#);
let new = json(r#"{"options": {"debug": false, "verbose": false}}"#);
apply_value_diff(doc.as_item_mut(), &old, &new);
let result = doc.to_string();
assert!(
!result.contains("[options]"),
"inline table must not expand to section: {result}"
);
assert!(
result.contains("debug = false"),
"debug should be updated: {result}"
);
assert!(
result.contains("verbose = false"),
"verbose should be preserved: {result}"
);
}
#[test]
fn apply_value_diff_inline_table_add_and_remove_keys() {
let original = "opts = { a = 1, b = 2 }\n";
let mut doc = parse_toml(original);
let old = json(r#"{"opts": {"a": 1, "b": 2}}"#);
let new = json(r#"{"opts": {"a": 1, "c": 3}}"#);
apply_value_diff(doc.as_item_mut(), &old, &new);
let result = doc.to_string();
assert!(
!result.contains("[opts]"),
"inline table must not expand: {result}"
);
assert!(
!result.contains("b ="),
"removed key 'b' should be gone: {result}"
);
assert!(
result.contains("c = 3"),
"new key 'c' should appear: {result}"
);
}
#[test]
fn json_to_toml_value_null_maps_to_empty_string_with_warning() {
let val = json_to_toml_value(&serde_json::Value::Null);
assert_eq!(val.as_str(), Some(""), "null should map to empty string");
}
#[test]
fn json_to_toml_item_object_becomes_table() {
let item = json_to_toml_item(&json(r#"{"k": "v"}"#));
assert!(item.is_table(), "object should become a table");
}
#[test]
fn json_to_toml_item_array_of_objects_becomes_aot() {
let item = json_to_toml_item(&json(r#"[{"a": 1}, {"b": 2}]"#));
assert!(
item.is_array_of_tables(),
"array of objects should become array-of-tables"
);
}
}