1pub fn sync_table(doc: &mut toml_edit::Table, target: &toml::Table) {
5 let to_remove = doc
6 .iter()
7 .map(|(k, _)| k.to_string())
8 .filter(|k| !target.contains_key(k))
9 .collect::<Vec<_>>();
10
11 for k in &to_remove {
12 doc.remove(k);
13 }
14 for (key, target_value) in target {
15 match target_value {
16 toml::Value::Table(sub_target) => {
17 let entry = doc
18 .entry(key)
19 .or_insert(toml_edit::Item::Table(Default::default()));
20 if let Some(sub_doc) = entry.as_table_mut() {
21 sync_table(sub_doc, sub_target);
22 }
23 }
24 _ => {
25 if let toml::Value::String(value) = target_value {
26 if let Some(inline) =
27 doc.get_mut(key).and_then(|item| item.as_inline_table_mut())
28 {
29 if inline.contains_key("value") {
30 let unchanged = inline
31 .get("value")
32 .and_then(toml_edit::Value::as_str)
33 .is_some_and(|existing| existing == value);
34 if !unchanged {
35 inline.insert("value", toml_edit::Value::from(value.as_str()));
36 }
37 continue;
38 }
39 }
40 }
41 if let Some(existing) = doc.get(key).and_then(|t| t.as_value()) {
42 if values_equal(existing, target_value) {
43 continue;
44 }
45 }
46 doc.insert(
47 key,
48 toml_edit::value(convert_to_edit_toml_value(target_value)),
49 );
50 }
51 }
52 }
53}
54
55fn values_equal(edit: &toml_edit::Value, target: &toml::Value) -> bool {
56 match (edit, target) {
57 (toml_edit::Value::String(a), toml::Value::String(b)) => a.value() == b,
58 (toml_edit::Value::Integer(a), toml::Value::Integer(b)) => a.value() == b,
59 (toml_edit::Value::Float(a), toml::Value::Float(b)) => a.value() == b,
60 (toml_edit::Value::Boolean(a), toml::Value::Boolean(b)) => a.value() == b,
61 (toml_edit::Value::Array(a), toml::Value::Array(b)) => {
62 a.len() == b.len() && a.iter().zip(b).all(|(a, b)| values_equal(a, b))
63 }
64 _ => false,
65 }
66}
67
68fn convert_to_edit_toml_value(v: &toml::Value) -> toml_edit::Value {
69 match v {
70 toml::Value::String(s) => toml_edit::Value::from(s.as_str()),
71 toml::Value::Integer(i) => toml_edit::Value::from(*i),
72 toml::Value::Float(f) => toml_edit::Value::from(*f),
73 toml::Value::Boolean(b) => toml_edit::Value::from(*b),
74 toml::Value::Datetime(d) => toml_edit::Value::from(*d),
75 toml::Value::Array(arr) => {
76 toml_edit::Value::Array(arr.iter().map(convert_to_edit_toml_value).collect())
77 }
78 toml::Value::Table(table) => {
79 let mut inline = toml_edit::InlineTable::new();
80 for (k, v) in table {
81 inline.insert(k, convert_to_edit_toml_value(v));
82 }
83 toml_edit::Value::InlineTable(inline)
84 }
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_convert_to_edit_toml_value_string() {
94 let v = toml::Value::String("hello".to_string());
95 let result = convert_to_edit_toml_value(&v);
96 assert!(matches!(result, toml_edit::Value::String(_)));
97 if let toml_edit::Value::String(s) = result {
98 assert_eq!(s.value(), "hello");
99 }
100 }
101
102 #[test]
103 fn test_convert_to_edit_toml_value_integer() {
104 let v = toml::Value::Integer(42);
105 let result = convert_to_edit_toml_value(&v);
106 assert!(matches!(result, toml_edit::Value::Integer(_)));
107 if let toml_edit::Value::Integer(i) = result {
108 assert_eq!(*i.value(), 42);
109 }
110 }
111
112 #[test]
113 fn test_convert_to_edit_toml_value_float() {
114 let v = toml::Value::Float(3.14);
115 let result = convert_to_edit_toml_value(&v);
116 assert!(matches!(result, toml_edit::Value::Float(_)));
117 if let toml_edit::Value::Float(f) = result {
118 assert_eq!(*f.value(), 3.14);
119 }
120 }
121
122 #[test]
123 fn test_convert_to_edit_toml_value_boolean() {
124 for b in [true, false] {
125 let v = toml::Value::Boolean(b);
126 let result = convert_to_edit_toml_value(&v);
127 assert!(matches!(result, toml_edit::Value::Boolean(_)));
128 if let toml_edit::Value::Boolean(val) = result {
129 assert_eq!(*val.value(), b);
130 }
131 }
132 }
133
134 #[test]
135 fn test_convert_to_edit_toml_value_datetime() {
136 let dt: toml::value::Datetime = "1979-05-27T07:32:00Z".parse().unwrap();
137 let v = toml::Value::Datetime(dt.clone());
138 let result = convert_to_edit_toml_value(&v);
139 assert!(matches!(result, toml_edit::Value::Datetime(_)));
140 if let toml_edit::Value::Datetime(d) = result {
141 assert_eq!(d.value().to_string(), dt.to_string());
142 }
143 }
144
145 #[test]
146 fn test_convert_to_edit_toml_value_array() {
147 let v = toml::Value::Array(vec![
148 toml::Value::String("a".to_string()),
149 toml::Value::Integer(1),
150 toml::Value::Boolean(false),
151 ]);
152 let result = convert_to_edit_toml_value(&v);
153 assert!(matches!(result, toml_edit::Value::Array(_)));
154 if let toml_edit::Value::Array(arr) = result {
155 assert_eq!(arr.len(), 3);
156 assert!(matches!(
157 arr.iter().next().unwrap(),
158 toml_edit::Value::String(_)
159 ));
160 }
161 }
162
163 #[test]
164 fn test_convert_to_edit_toml_value_nested_array() {
165 let v = toml::Value::Array(vec![
166 toml::Value::Array(vec![toml::Value::Integer(1), toml::Value::Integer(2)]),
167 toml::Value::Array(vec![toml::Value::Integer(3)]),
168 ]);
169 let result = convert_to_edit_toml_value(&v);
170 assert!(matches!(result, toml_edit::Value::Array(_)));
171 if let toml_edit::Value::Array(outer) = result {
172 assert_eq!(outer.len(), 2);
173 assert!(matches!(
174 outer.iter().next().unwrap(),
175 toml_edit::Value::Array(_)
176 ));
177 }
178 }
179
180 #[test]
181 fn test_convert_to_edit_toml_value_table() {
182 let mut table = toml::value::Table::new();
183 table.insert("key1".to_string(), toml::Value::String("val".to_string()));
184 table.insert("key2".to_string(), toml::Value::Integer(99));
185 let v = toml::Value::Table(table);
186 let result = convert_to_edit_toml_value(&v);
187 assert!(matches!(result, toml_edit::Value::InlineTable(_)));
188 if let toml_edit::Value::InlineTable(inline) = result {
189 assert_eq!(inline.len(), 2);
190 let v1 = inline.get("key1").unwrap();
191 assert!(matches!(v1, toml_edit::Value::String(_)));
192 if let toml_edit::Value::String(s) = v1 {
193 assert_eq!(s.value(), "val");
194 }
195 let v2 = inline.get("key2").unwrap();
196 assert!(matches!(v2, toml_edit::Value::Integer(_)));
197 if let toml_edit::Value::Integer(i) = v2 {
198 assert_eq!(*i.value(), 99);
199 }
200 }
201 }
202
203 #[test]
204 fn test_convert_to_edit_toml_value_nested_table() {
205 let mut inner = toml::value::Table::new();
206 inner.insert("x".to_string(), toml::Value::Boolean(true));
207 let mut outer = toml::value::Table::new();
208 outer.insert("inner".to_string(), toml::Value::Table(inner));
209 let v = toml::Value::Table(outer);
210 let result = convert_to_edit_toml_value(&v);
211 assert!(matches!(result, toml_edit::Value::InlineTable(_)));
212 if let toml_edit::Value::InlineTable(inline) = result {
213 let nested = inline.get("inner").unwrap();
214 assert!(matches!(nested, toml_edit::Value::InlineTable(_)));
215 if let toml_edit::Value::InlineTable(inner_inline) = nested {
216 assert_eq!(inner_inline.len(), 1);
217 let xv = inner_inline.get("x").unwrap();
218 assert!(matches!(xv, toml_edit::Value::Boolean(_)));
219 if let toml_edit::Value::Boolean(b) = xv {
220 assert!(*b.value());
221 }
222 }
223 }
224 }
225
226 fn make_doc(toml: &str) -> toml_edit::DocumentMut {
227 toml.parse().unwrap()
228 }
229
230 fn make_target(toml: &str) -> toml::Table {
231 toml::from_str(toml).unwrap()
232 }
233
234 #[test]
235 fn sync_table_adds_new_keys() {
236 let mut doc = make_doc("");
237 let target = make_target("name = \"shine\"\nversion = 1\n");
238 sync_table(doc.as_table_mut(), &target);
239 assert_eq!(doc["name"].as_str(), Some("shine"));
240 assert_eq!(doc["version"].as_integer(), Some(1));
241 }
242
243 #[test]
244 fn sync_table_removes_stale_keys() {
245 let mut doc = make_doc("old = \"gone\"\nkeep = true\n");
246 let target = make_target("keep = true");
247 sync_table(doc.as_table_mut(), &target);
248 assert!(doc.get("old").is_none());
249 assert!(doc.get("keep").is_some());
250 }
251
252 #[test]
253 fn sync_table_updates_changed_value() {
254 let mut doc = make_doc("version = 1");
255 let target = make_target("version = 2");
256 sync_table(doc.as_table_mut(), &target);
257 assert_eq!(doc["version"].as_integer(), Some(2));
258 }
259
260 #[test]
261 fn sync_table_preserves_unchanged_value() {
262 let mut doc = make_doc(r#"name = "shine" # important comment"#);
263 let target = make_target(r#"name = "shine""#);
264 let before = doc.to_string();
265 sync_table(doc.as_table_mut(), &target);
266 assert_eq!(doc.to_string(), before);
267 }
268
269 #[test]
270 fn sync_table_updates_inline_env_value_and_preserves_description() {
271 let mut doc = make_doc(
272 r#"[env]
273TOKEN = { value = "old", description = "API token" }
274"#,
275 );
276 let target = make_target("[env]\nTOKEN = \"new\"\n");
277
278 sync_table(doc.as_table_mut(), &target);
279
280 let inline = doc["env"]["TOKEN"].as_inline_table().unwrap();
281 assert_eq!(inline["value"].as_str(), Some("new"));
282 assert_eq!(inline["description"].as_str(), Some("API token"));
283 }
284
285 #[test]
286 fn sync_table_recurses_into_nested_tables() {
287 let mut doc = make_doc("[db]\nhost = \"old\"\nport = 5432\n");
288 let target = make_target("[db]\nhost = \"new\"\nport = 5432\n");
289 sync_table(doc.as_table_mut(), &target);
290 assert_eq!(doc["db"]["host"].as_str(), Some("new"));
291 assert_eq!(doc["db"]["port"].as_integer(), Some(5432));
292 }
293
294 #[test]
295 fn sync_table_removes_nested_stale_keys() {
296 let mut doc = make_doc("[section]\nkeep = 1\nstale = 2\n");
297 let target = make_target("[section]\nkeep = 1\n");
298 sync_table(doc.as_table_mut(), &target);
299 assert!(doc["section"].get("stale").is_none());
300 assert_eq!(doc["section"]["keep"].as_integer(), Some(1));
301 }
302
303 #[test]
304 fn sync_table_empty_target_clears_doc() {
305 let mut doc = make_doc("a = 1\nb = 2\n");
306 let target = make_target("");
307 sync_table(doc.as_table_mut(), &target);
308 assert!(doc.get("a").is_none());
309 assert!(doc.get("b").is_none());
310 }
311
312 #[test]
313 fn sync_table_empty_doc_fills_from_target() {
314 let mut doc = make_doc("");
315 let target = make_target("x = true\ny = 3.14\n");
316 sync_table(doc.as_table_mut(), &target);
317 assert_eq!(doc["x"].as_bool(), Some(true));
318 }
319
320 #[test]
321 fn test_values_equal() {
322 use toml::Value as TargetValue;
323 use toml_edit::Value as EditValue;
324
325 assert!(values_equal(
326 &EditValue::from("hello"),
327 &TargetValue::from("hello")
328 ));
329 assert!(values_equal(
330 &EditValue::from(42i64),
331 &TargetValue::from(42i64)
332 ));
333 assert!(values_equal(
334 &EditValue::from(3.14f64),
335 &TargetValue::from(3.14f64)
336 ));
337 assert!(values_equal(
338 &EditValue::from(true),
339 &TargetValue::from(true)
340 ));
341
342 let edit_array = EditValue::Array(
343 vec![
344 EditValue::from("a"),
345 EditValue::from(1i64),
346 EditValue::from(false),
347 ]
348 .into_iter()
349 .collect(),
350 );
351 let target_array = TargetValue::Array(vec![
352 TargetValue::from("a"),
353 TargetValue::from(1i64),
354 TargetValue::from(false),
355 ]);
356
357 assert!(values_equal(&edit_array, &target_array));
358
359 let different_string = TargetValue::from("world");
360 assert!(!values_equal(&EditValue::from("hello"), &different_string));
361
362 let different_int = TargetValue::from(7i64);
363 assert!(!values_equal(&EditValue::from(42i64), &different_int));
364
365 let different_type = TargetValue::from(42i64);
366 assert!(!values_equal(&EditValue::from("42"), &different_type));
367
368 let shorter_array =
369 TargetValue::Array(vec![TargetValue::from("a"), TargetValue::from(1i64)]);
370 assert!(!values_equal(&edit_array, &shorter_array));
371
372 let nested_edit_array = EditValue::Array(
373 vec![
374 EditValue::Array(vec![EditValue::from("x")].into_iter().collect()),
375 EditValue::from(false),
376 ]
377 .into_iter()
378 .collect(),
379 );
380 let nested_target_array = TargetValue::Array(vec![
381 TargetValue::Array(vec![TargetValue::from("x")]),
382 TargetValue::from(false),
383 ]);
384 assert!(values_equal(&nested_edit_array, &nested_target_array));
385 }
386}