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