1#![forbid(unsafe_code)]
12#![warn(missing_docs, clippy::pedantic)]
13
14pub use toml_edit;
15
16mod array;
17mod comment;
18mod document;
19mod kind;
20
21pub use array::{convert_element, push_element, remove_element, set_element};
22pub use comment::{
23 comment_beside, comments_before, set_comment_beside, set_comments_before,
24 set_trailing_comments, trailing_comments,
25};
26pub use document::{Document, Error, Newline};
27pub use kind::{convert, Kind};
28
29use toml_edit::{InlineTable, Item, Key, Table, Value};
30
31pub fn add_key(t: &mut Table, name: &str, kind: Kind) -> bool {
37 if name.is_empty() || t.contains_key(name) {
38 return false;
39 }
40 t.insert(name, kind.item());
41 true
42}
43
44pub fn remove_key(t: &mut Table, name: &str) -> bool {
46 t.remove(name).is_some()
47}
48
49pub fn rename_key(t: &mut Table, from: &str, to: &str) -> bool {
57 if to.is_empty() || from == to || !t.contains_key(from) || t.contains_key(to) {
58 return false;
59 }
60
61 let names: Vec<String> = t.iter().map(|(k, _)| k.to_owned()).collect();
62 let mut taken: Vec<(Key, Item)> = Vec::with_capacity(names.len());
63 for name in &names {
64 if let Some(entry) = t.remove_entry(name) {
65 taken.push(entry);
66 }
67 }
68
69 for (key, item) in taken {
70 if key.get() == from {
71 let renamed = Key::new(to)
72 .with_leaf_decor(key.leaf_decor().clone())
73 .with_dotted_decor(key.dotted_decor().clone());
74 t.insert_formatted(&renamed, item);
75 } else {
76 t.insert_formatted(&key, item);
77 }
78 }
79 true
80}
81
82pub fn add_inline_key(t: &mut InlineTable, name: &str, kind: Kind) -> bool {
87 if name.is_empty() || t.contains_key(name) {
88 return false;
89 }
90 match kind.as_value() {
91 Some(value) => {
92 t.insert(name, value);
93 true
94 }
95 None => false,
96 }
97}
98
99pub fn remove_inline_key(t: &mut InlineTable, name: &str) -> bool {
101 t.remove(name).is_some()
102}
103
104pub fn rename_inline_key(t: &mut InlineTable, from: &str, to: &str) -> bool {
110 if to.is_empty() || from == to || !t.contains_key(from) || t.contains_key(to) {
111 return false;
112 }
113
114 let names: Vec<String> = t.iter().map(|(k, _)| k.to_owned()).collect();
115 let mut taken: Vec<(Key, Value)> = Vec::with_capacity(names.len());
116 for name in &names {
117 if let Some(entry) = t.remove_entry(name) {
118 taken.push(entry);
119 }
120 }
121
122 for (key, value) in taken {
123 if key.get() == from {
124 let renamed = Key::new(to)
125 .with_leaf_decor(key.leaf_decor().clone())
126 .with_dotted_decor(key.dotted_decor().clone());
127 t.insert_formatted(&renamed, value);
128 } else {
129 t.insert_formatted(&key, value);
130 }
131 }
132 true
133}
134
135pub fn convert_key(t: &mut Table, name: &str, to: Kind) -> bool {
145 let Some(item) = t.get_mut(name) else {
146 return false;
147 };
148 match (item, to) {
149 (Item::Value(v), to) if to != Kind::Table => match convert(v, to) {
150 Some(new) => {
151 set_value(v, new);
152 true
153 }
154 None => false,
155 },
156 (Item::Value(Value::InlineTable(_)), Kind::Table) | (Item::Table(_), Kind::InlineTable) => {
157 relayout(t, name)
158 }
159 _ => false,
160 }
161}
162
163fn relayout(t: &mut Table, name: &str) -> bool {
168 let names: Vec<String> = t.iter().map(|(k, _)| k.to_owned()).collect();
169 let mut taken: Vec<(Key, Item)> = Vec::with_capacity(names.len());
170 for n in &names {
171 if let Some(entry) = t.remove_entry(n) {
172 taken.push(entry);
173 }
174 }
175 for (key, item) in taken {
176 if key.get() != name {
177 t.insert_formatted(&key, item);
178 continue;
179 }
180 let (key, item) = match item {
186 Item::Table(table) => {
187 let mut key = Key::new(name);
188 if let Some(above) = table.decor().prefix().cloned() {
189 key.leaf_decor_mut().set_prefix(above);
190 }
191 (
192 key,
193 Item::Value(Value::InlineTable(table.into_inline_table())),
194 )
195 }
196 Item::Value(Value::InlineTable(inline)) => {
197 let mut table = inline.into_table();
198 if let Some(above) = key.leaf_decor().prefix().cloned() {
199 table.decor_mut().set_prefix(above);
200 }
201 (Key::new(name), Item::Table(table))
202 }
203 other => (key, other),
204 };
205 t.insert_formatted(&key, item);
206 }
207 true
208}
209
210pub fn convert_inline_key(t: &mut InlineTable, name: &str, to: Kind) -> bool {
213 if to == Kind::Table {
214 return false;
215 }
216 let Some(v) = t.get_mut(name) else {
217 return false;
218 };
219 match convert(v, to) {
220 Some(new) => {
221 set_value(v, new);
222 true
223 }
224 None => false,
225 }
226}
227
228pub fn set_value(slot: &mut Value, new: Value) {
234 let decor = slot.decor().clone();
235 *slot = new;
236 *slot.decor_mut() = decor;
237}
238
239#[cfg(test)]
240mod structure_tests {
241 use super::{add_key, remove_key, rename_key, Kind};
242 use toml_edit::DocumentMut;
243
244 const DOC: &str = "\
245# above first
246first = \"one\" # beside first
247second = 2
248
249[third]
250inner = true
251";
252
253 fn doc() -> DocumentMut {
254 DOC.parse().expect("valid TOML")
255 }
256
257 fn keys(d: &DocumentMut) -> Vec<String> {
258 d.as_table().iter().map(|(k, _)| k.to_owned()).collect()
259 }
260
261 #[test]
264 fn a_rename_keeps_its_place_and_its_comments() {
265 let mut d = doc();
266 assert!(rename_key(d.as_table_mut(), "first", "primary"));
267
268 assert_eq!(keys(&d), ["primary", "second", "third"]);
269
270 let written = d.to_string();
271 assert!(written.contains("# above first"), "{written}");
272 assert!(written.contains("# beside first"), "{written}");
273 assert!(written.contains("primary = \"one\""), "{written}");
274 assert!(!written.contains("first ="), "{written}");
275 }
276
277 #[test]
279 fn a_table_can_be_renamed() {
280 let mut d = doc();
281 assert!(rename_key(d.as_table_mut(), "third", "provenance"));
282
283 assert_eq!(keys(&d), ["first", "second", "provenance"]);
284 assert!(d.to_string().contains("inner = true"));
285 }
286
287 #[test]
291 fn a_rename_onto_an_existing_key_is_refused() {
292 let mut d = doc();
293 assert!(!rename_key(d.as_table_mut(), "first", "second"));
294 assert!(!rename_key(d.as_table_mut(), "first", ""));
295 assert!(!rename_key(d.as_table_mut(), "absent", "anything"));
296
297 assert_eq!(keys(&d), ["first", "second", "third"]);
298 assert!(d.to_string().contains("second = 2"));
299 }
300
301 #[test]
309 fn a_key_added_to_a_document_with_tables_stays_at_the_root() {
310 let mut d = doc();
311 assert!(add_key(d.as_table_mut(), "author", Kind::Text));
312 assert_eq!(keys(&d), ["first", "second", "third", "author"]);
313
314 let written = d.to_string();
315 assert!(written.contains("author = \"\""), "{written}");
316 assert!(
317 written.find("author").unwrap() < written.find("[third]").unwrap(),
318 "an added key must not fall inside the last table: {written}"
319 );
320
321 let back: DocumentMut = written.parse().expect("still parses");
323 assert!(back.as_table().contains_key("author"));
324 assert!(!back["third"]
325 .as_table()
326 .expect("a table")
327 .contains_key("author"));
328 }
329
330 #[test]
331 fn a_key_already_there_is_not_added_over() {
332 let mut d = doc();
333 assert!(!add_key(d.as_table_mut(), "first", Kind::Integer));
334 assert!(!add_key(d.as_table_mut(), "", Kind::Integer));
335 assert!(d.to_string().contains("first = \"one\""));
336 }
337
338 #[test]
340 fn every_kind_of_new_key_is_valid_toml() {
341 for kind in Kind::ALL {
342 let mut d = doc();
343 assert!(add_key(d.as_table_mut(), "added", kind), "{kind:?}");
344 let written = d.to_string();
345 written
346 .parse::<DocumentMut>()
347 .unwrap_or_else(|e| panic!("{kind:?} wrote something unparseable: {e}\n{written}"));
348 }
349 }
350
351 #[test]
352 fn removing_a_table_takes_what_is_under_it() {
353 let mut d = doc();
354 assert!(remove_key(d.as_table_mut(), "third"));
355 assert_eq!(keys(&d), ["first", "second"]);
356 assert!(!d.to_string().contains("inner"));
357 assert!(!remove_key(d.as_table_mut(), "third"));
358 }
359}
360
361#[cfg(test)]
362mod inline_tests {
363 use super::{add_inline_key, remove_inline_key, rename_inline_key, Kind};
364 use toml_edit::DocumentMut;
365
366 fn doc() -> DocumentMut {
367 "owner = { name = \"D. Anderson\", team = \"consulting\" }\n"
368 .parse()
369 .expect("valid TOML")
370 }
371
372 fn owner(d: &mut DocumentMut) -> &mut toml_edit::InlineTable {
373 d["owner"].as_inline_table_mut().expect("an inline table")
374 }
375
376 #[test]
380 fn a_key_inside_an_inline_table_can_be_removed() {
381 let mut d = doc();
382 assert!(remove_inline_key(owner(&mut d), "name"));
383 assert!(!remove_inline_key(owner(&mut d), "name"));
384
385 let written = d.to_string();
386 assert!(!written.contains("name"), "{written}");
387 assert!(written.contains("team = \"consulting\""), "{written}");
388 }
389
390 #[test]
393 fn a_key_inside_an_inline_table_keeps_its_place_when_renamed() {
394 let mut d = doc();
395 assert!(rename_inline_key(owner(&mut d), "name", "who"));
396
397 let written = d.to_string();
398 assert!(
399 written.find("who").unwrap() < written.find("team").unwrap(),
400 "{written}"
401 );
402 assert!(written.contains("who = \"D. Anderson\""), "{written}");
403
404 assert!(!rename_inline_key(owner(&mut d), "who", "team"));
406 assert!(!rename_inline_key(owner(&mut d), "who", ""));
407 }
408
409 #[test]
412 fn an_inline_table_takes_values_and_not_tables() {
413 let mut d = doc();
414 assert!(add_inline_key(owner(&mut d), "since", Kind::Integer));
415 assert!(!add_inline_key(owner(&mut d), "nested", Kind::Table));
416 assert!(!add_inline_key(owner(&mut d), "name", Kind::Text));
417
418 assert!(!Kind::VALUES.contains(&Kind::Table));
419
420 let written = d.to_string();
421 assert!(written.contains("since = 0"), "{written}");
422 written.parse::<DocumentMut>().expect("still parses");
423 }
424}
425
426#[cfg(test)]
427mod convert_tests {
428 use super::{convert_inline_key, convert_key, Kind};
429 use toml_edit::DocumentMut;
430
431 #[test]
435 fn a_table_and_an_inline_table_trade_places() {
436 let mut d: DocumentMut = "first = 1\n\n# who\n[owner]\nname = \"D\"\n\n[last]\nz = 0\n"
437 .parse()
438 .expect("valid TOML");
439 assert!(convert_key(d.as_table_mut(), "owner", Kind::InlineTable));
440 let keys: Vec<&str> = d.as_table().iter().map(|(k, _)| k).collect();
441 assert_eq!(keys, ["first", "owner", "last"]);
442 assert!(d["owner"].is_inline_table(), "{d}");
443 assert_eq!(
444 d.to_string(),
445 "first = 1\n\n# who\nowner = { name = \"D\" }\n\n[last]\nz = 0\n"
446 );
447
448 assert!(convert_key(d.as_table_mut(), "owner", Kind::Table));
449 assert!(d["owner"].is_table(), "{d}");
450 assert_eq!(
451 d.to_string(),
452 "first = 1\n\n# who\n[owner]\nname = \"D\"\n\n[last]\nz = 0\n"
453 );
454 }
455
456 #[test]
459 fn a_value_converts_or_is_left_alone() {
460 let mut d: DocumentMut = "n = \"44\" # beside\n".parse().expect("valid TOML");
461 assert!(convert_key(d.as_table_mut(), "n", Kind::Integer));
462 assert_eq!(d.to_string(), "n = 44 # beside\n");
463 assert!(!convert_key(d.as_table_mut(), "n", Kind::Boolean));
464 assert_eq!(d.to_string(), "n = 44 # beside\n");
465 assert!(!convert_key(d.as_table_mut(), "n", Kind::Table));
466 assert!(!convert_key(d.as_table_mut(), "absent", Kind::Text));
467 }
468
469 #[test]
471 fn an_inline_table_converts_its_values_and_holds_no_table() {
472 let mut d: DocumentMut = "t = { n = \"1\", m = { x = 1 } }\n"
473 .parse()
474 .expect("valid TOML");
475 let inline = d["t"].as_inline_table_mut().expect("an inline table");
476 assert!(convert_inline_key(inline, "n", Kind::Integer));
477 assert!(!convert_inline_key(inline, "m", Kind::Table));
478 assert!(!convert_inline_key(inline, "n", Kind::InlineTable));
479 assert_eq!(d.to_string(), "t = { n = 1, m = { x = 1 } }\n");
480 }
481}
482
483#[cfg(test)]
484mod value_tests {
485 use super::set_value;
486 use toml_edit::{DocumentMut, Value};
487
488 #[test]
495 fn a_changed_value_keeps_its_comment_and_its_spacing() {
496 let mut d: DocumentMut = "title = \"before\" # beside the title\n"
497 .parse()
498 .expect("valid TOML");
499 set_value(
500 d["title"].as_value_mut().expect("a value"),
501 Value::from("after"),
502 );
503 assert_eq!(
504 d.to_string(),
505 "title = \"after\" # beside the title\n"
506 );
507 }
508}