1#![forbid(unsafe_code)]
12#![warn(missing_docs, clippy::pedantic)]
13
14pub use toml_edit;
15
16use toml_edit::{Datetime, InlineTable, Item, Key, Table, Value};
17
18#[derive(Clone, Copy, PartialEq, Eq, Debug)]
23pub enum NewKey {
24 Text,
26 Integer,
28 Float,
30 Boolean,
32 Datetime,
35 Table,
37}
38
39impl NewKey {
40 pub const SCALARS: [Self; 5] = [
46 Self::Text,
47 Self::Integer,
48 Self::Float,
49 Self::Boolean,
50 Self::Datetime,
51 ];
52
53 pub const ALL: [Self; 6] = [
55 Self::Text,
56 Self::Integer,
57 Self::Float,
58 Self::Boolean,
59 Self::Datetime,
60 Self::Table,
61 ];
62
63 #[must_use]
65 pub fn label(self) -> &'static str {
66 match self {
67 Self::Text => "text",
68 Self::Integer => "integer",
69 Self::Float => "float",
70 Self::Boolean => "boolean",
71 Self::Datetime => "date and time",
72 Self::Table => "table",
73 }
74 }
75
76 fn as_value(self) -> Option<Value> {
78 match self.item() {
79 Item::Value(v) => Some(v),
80 _ => None,
81 }
82 }
83
84 fn item(self) -> Item {
85 match self {
86 Self::Text => Item::Value(Value::from("")),
87 Self::Integer => Item::Value(Value::from(0_i64)),
88 Self::Float => Item::Value(Value::from(0.0_f64)),
89 Self::Boolean => Item::Value(Value::from(false)),
90 Self::Datetime => Item::Value(Value::from(
91 "1970-01-01T00:00:00Z"
92 .parse::<Datetime>()
93 .expect("the epoch is a datetime"),
94 )),
95 Self::Table => Item::Table(Table::new()),
96 }
97 }
98}
99
100pub fn add_key(t: &mut Table, name: &str, kind: NewKey) -> bool {
106 if name.is_empty() || t.contains_key(name) {
107 return false;
108 }
109 t.insert(name, kind.item());
110 true
111}
112
113pub fn remove_key(t: &mut Table, name: &str) -> bool {
115 t.remove(name).is_some()
116}
117
118pub fn rename_key(t: &mut Table, from: &str, to: &str) -> bool {
126 if to.is_empty() || from == to || !t.contains_key(from) || t.contains_key(to) {
127 return false;
128 }
129
130 let names: Vec<String> = t.iter().map(|(k, _)| k.to_owned()).collect();
131 let mut taken: Vec<(Key, Item)> = Vec::with_capacity(names.len());
132 for name in &names {
133 if let Some(entry) = t.remove_entry(name) {
134 taken.push(entry);
135 }
136 }
137
138 for (key, item) in taken {
139 if key.get() == from {
140 let renamed = Key::new(to)
141 .with_leaf_decor(key.leaf_decor().clone())
142 .with_dotted_decor(key.dotted_decor().clone());
143 t.insert_formatted(&renamed, item);
144 } else {
145 t.insert_formatted(&key, item);
146 }
147 }
148 true
149}
150
151pub fn add_inline_key(t: &mut InlineTable, name: &str, kind: NewKey) -> bool {
156 if name.is_empty() || t.contains_key(name) {
157 return false;
158 }
159 match kind.as_value() {
160 Some(value) => {
161 t.insert(name, value);
162 true
163 }
164 None => false,
165 }
166}
167
168pub fn remove_inline_key(t: &mut InlineTable, name: &str) -> bool {
170 t.remove(name).is_some()
171}
172
173pub fn rename_inline_key(t: &mut InlineTable, from: &str, to: &str) -> bool {
179 if to.is_empty() || from == to || !t.contains_key(from) || t.contains_key(to) {
180 return false;
181 }
182
183 let names: Vec<String> = t.iter().map(|(k, _)| k.to_owned()).collect();
184 let mut taken: Vec<(Key, Value)> = Vec::with_capacity(names.len());
185 for name in &names {
186 if let Some(entry) = t.remove_entry(name) {
187 taken.push(entry);
188 }
189 }
190
191 for (key, value) in taken {
192 if key.get() == from {
193 let renamed = Key::new(to)
194 .with_leaf_decor(key.leaf_decor().clone())
195 .with_dotted_decor(key.dotted_decor().clone());
196 t.insert_formatted(&renamed, value);
197 } else {
198 t.insert_formatted(&key, value);
199 }
200 }
201 true
202}
203
204pub fn set_value(slot: &mut Value, new: Value) {
210 let decor = slot.decor().clone();
211 *slot = new;
212 *slot.decor_mut() = decor;
213}
214
215#[cfg(test)]
216mod structure_tests {
217 use super::{add_key, remove_key, rename_key, NewKey};
218 use toml_edit::DocumentMut;
219
220 const DOC: &str = "\
221# above first
222first = \"one\" # beside first
223second = 2
224
225[third]
226inner = true
227";
228
229 fn doc() -> DocumentMut {
230 DOC.parse().expect("valid TOML")
231 }
232
233 fn keys(d: &DocumentMut) -> Vec<String> {
234 d.as_table().iter().map(|(k, _)| k.to_owned()).collect()
235 }
236
237 #[test]
240 fn a_rename_keeps_its_place_and_its_comments() {
241 let mut d = doc();
242 assert!(rename_key(d.as_table_mut(), "first", "primary"));
243
244 assert_eq!(keys(&d), ["primary", "second", "third"]);
245
246 let written = d.to_string();
247 assert!(written.contains("# above first"), "{written}");
248 assert!(written.contains("# beside first"), "{written}");
249 assert!(written.contains("primary = \"one\""), "{written}");
250 assert!(!written.contains("first ="), "{written}");
251 }
252
253 #[test]
255 fn a_table_can_be_renamed() {
256 let mut d = doc();
257 assert!(rename_key(d.as_table_mut(), "third", "provenance"));
258
259 assert_eq!(keys(&d), ["first", "second", "provenance"]);
260 assert!(d.to_string().contains("inner = true"));
261 }
262
263 #[test]
267 fn a_rename_onto_an_existing_key_is_refused() {
268 let mut d = doc();
269 assert!(!rename_key(d.as_table_mut(), "first", "second"));
270 assert!(!rename_key(d.as_table_mut(), "first", ""));
271 assert!(!rename_key(d.as_table_mut(), "absent", "anything"));
272
273 assert_eq!(keys(&d), ["first", "second", "third"]);
274 assert!(d.to_string().contains("second = 2"));
275 }
276
277 #[test]
285 fn a_key_added_to_a_document_with_tables_stays_at_the_root() {
286 let mut d = doc();
287 assert!(add_key(d.as_table_mut(), "author", NewKey::Text));
288 assert_eq!(keys(&d), ["first", "second", "third", "author"]);
289
290 let written = d.to_string();
291 assert!(written.contains("author = \"\""), "{written}");
292 assert!(
293 written.find("author").unwrap() < written.find("[third]").unwrap(),
294 "an added key must not fall inside the last table: {written}"
295 );
296
297 let back: DocumentMut = written.parse().expect("still parses");
299 assert!(back.as_table().contains_key("author"));
300 assert!(!back["third"]
301 .as_table()
302 .expect("a table")
303 .contains_key("author"));
304 }
305
306 #[test]
307 fn a_key_already_there_is_not_added_over() {
308 let mut d = doc();
309 assert!(!add_key(d.as_table_mut(), "first", NewKey::Integer));
310 assert!(!add_key(d.as_table_mut(), "", NewKey::Integer));
311 assert!(d.to_string().contains("first = \"one\""));
312 }
313
314 #[test]
316 fn every_kind_of_new_key_is_valid_toml() {
317 for kind in NewKey::ALL {
318 let mut d = doc();
319 assert!(add_key(d.as_table_mut(), "added", kind), "{kind:?}");
320 let written = d.to_string();
321 written
322 .parse::<DocumentMut>()
323 .unwrap_or_else(|e| panic!("{kind:?} wrote something unparseable: {e}\n{written}"));
324 }
325 }
326
327 #[test]
328 fn removing_a_table_takes_what_is_under_it() {
329 let mut d = doc();
330 assert!(remove_key(d.as_table_mut(), "third"));
331 assert_eq!(keys(&d), ["first", "second"]);
332 assert!(!d.to_string().contains("inner"));
333 assert!(!remove_key(d.as_table_mut(), "third"));
334 }
335}
336
337#[cfg(test)]
338mod inline_tests {
339 use super::{add_inline_key, remove_inline_key, rename_inline_key, NewKey};
340 use toml_edit::DocumentMut;
341
342 fn doc() -> DocumentMut {
343 "owner = { name = \"D. Anderson\", team = \"consulting\" }\n"
344 .parse()
345 .expect("valid TOML")
346 }
347
348 fn owner(d: &mut DocumentMut) -> &mut toml_edit::InlineTable {
349 d["owner"].as_inline_table_mut().expect("an inline table")
350 }
351
352 #[test]
356 fn a_key_inside_an_inline_table_can_be_removed() {
357 let mut d = doc();
358 assert!(remove_inline_key(owner(&mut d), "name"));
359 assert!(!remove_inline_key(owner(&mut d), "name"));
360
361 let written = d.to_string();
362 assert!(!written.contains("name"), "{written}");
363 assert!(written.contains("team = \"consulting\""), "{written}");
364 }
365
366 #[test]
369 fn a_key_inside_an_inline_table_keeps_its_place_when_renamed() {
370 let mut d = doc();
371 assert!(rename_inline_key(owner(&mut d), "name", "who"));
372
373 let written = d.to_string();
374 assert!(
375 written.find("who").unwrap() < written.find("team").unwrap(),
376 "{written}"
377 );
378 assert!(written.contains("who = \"D. Anderson\""), "{written}");
379
380 assert!(!rename_inline_key(owner(&mut d), "who", "team"));
382 assert!(!rename_inline_key(owner(&mut d), "who", ""));
383 }
384
385 #[test]
388 fn an_inline_table_takes_values_and_not_tables() {
389 let mut d = doc();
390 assert!(add_inline_key(owner(&mut d), "since", NewKey::Integer));
391 assert!(!add_inline_key(owner(&mut d), "nested", NewKey::Table));
392 assert!(!add_inline_key(owner(&mut d), "name", NewKey::Text));
393
394 assert!(!NewKey::SCALARS.contains(&NewKey::Table));
395
396 let written = d.to_string();
397 assert!(written.contains("since = 0"), "{written}");
398 written.parse::<DocumentMut>().expect("still parses");
399 }
400}
401
402#[cfg(test)]
403mod value_tests {
404 use super::set_value;
405 use toml_edit::{DocumentMut, Value};
406
407 #[test]
414 fn a_changed_value_keeps_its_comment_and_its_spacing() {
415 let mut d: DocumentMut = "title = \"before\" # beside the title\n"
416 .parse()
417 .expect("valid TOML");
418 set_value(
419 d["title"].as_value_mut().expect("a value"),
420 Value::from("after"),
421 );
422 assert_eq!(
423 d.to_string(),
424 "title = \"after\" # beside the title\n"
425 );
426 }
427}