#![forbid(unsafe_code)]
#![warn(missing_docs, clippy::pedantic)]
pub use toml_edit;
mod array;
mod comment;
mod document;
mod kind;
pub use array::{convert_element, push_element, remove_element, set_element};
pub use comment::{
comment_beside, comments_before, set_comment_beside, set_comments_before,
set_trailing_comments, trailing_comments,
};
pub use document::{Document, Error, Newline};
pub use kind::{convert, Kind};
use toml_edit::{InlineTable, Item, Key, Table, Value};
pub fn add_key(t: &mut Table, name: &str, kind: Kind) -> bool {
if name.is_empty() || t.contains_key(name) {
return false;
}
t.insert(name, kind.item());
true
}
pub fn remove_key(t: &mut Table, name: &str) -> bool {
t.remove(name).is_some()
}
pub fn rename_key(t: &mut Table, from: &str, to: &str) -> bool {
if to.is_empty() || from == to || !t.contains_key(from) || t.contains_key(to) {
return false;
}
let names: Vec<String> = t.iter().map(|(k, _)| k.to_owned()).collect();
let mut taken: Vec<(Key, Item)> = Vec::with_capacity(names.len());
for name in &names {
if let Some(entry) = t.remove_entry(name) {
taken.push(entry);
}
}
for (key, item) in taken {
if key.get() == from {
let renamed = Key::new(to)
.with_leaf_decor(key.leaf_decor().clone())
.with_dotted_decor(key.dotted_decor().clone());
t.insert_formatted(&renamed, item);
} else {
t.insert_formatted(&key, item);
}
}
true
}
pub fn add_inline_key(t: &mut InlineTable, name: &str, kind: Kind) -> bool {
if name.is_empty() || t.contains_key(name) {
return false;
}
match kind.as_value() {
Some(value) => {
t.insert(name, value);
true
}
None => false,
}
}
pub fn remove_inline_key(t: &mut InlineTable, name: &str) -> bool {
t.remove(name).is_some()
}
pub fn rename_inline_key(t: &mut InlineTable, from: &str, to: &str) -> bool {
if to.is_empty() || from == to || !t.contains_key(from) || t.contains_key(to) {
return false;
}
let names: Vec<String> = t.iter().map(|(k, _)| k.to_owned()).collect();
let mut taken: Vec<(Key, Value)> = Vec::with_capacity(names.len());
for name in &names {
if let Some(entry) = t.remove_entry(name) {
taken.push(entry);
}
}
for (key, value) in taken {
if key.get() == from {
let renamed = Key::new(to)
.with_leaf_decor(key.leaf_decor().clone())
.with_dotted_decor(key.dotted_decor().clone());
t.insert_formatted(&renamed, value);
} else {
t.insert_formatted(&key, value);
}
}
true
}
pub fn convert_key(t: &mut Table, name: &str, to: Kind) -> bool {
let Some(item) = t.get_mut(name) else {
return false;
};
match (item, to) {
(Item::Value(v), to) if to != Kind::Table => match convert(v, to) {
Some(new) => {
set_value(v, new);
true
}
None => false,
},
(Item::Value(Value::InlineTable(_)), Kind::Table) | (Item::Table(_), Kind::InlineTable) => {
relayout(t, name)
}
_ => false,
}
}
fn relayout(t: &mut Table, name: &str) -> bool {
let names: Vec<String> = t.iter().map(|(k, _)| k.to_owned()).collect();
let mut taken: Vec<(Key, Item)> = Vec::with_capacity(names.len());
for n in &names {
if let Some(entry) = t.remove_entry(n) {
taken.push(entry);
}
}
for (key, item) in taken {
if key.get() != name {
t.insert_formatted(&key, item);
continue;
}
let (key, item) = match item {
Item::Table(table) => {
let mut key = Key::new(name);
if let Some(above) = table.decor().prefix().cloned() {
key.leaf_decor_mut().set_prefix(above);
}
(
key,
Item::Value(Value::InlineTable(table.into_inline_table())),
)
}
Item::Value(Value::InlineTable(inline)) => {
let mut table = inline.into_table();
if let Some(above) = key.leaf_decor().prefix().cloned() {
table.decor_mut().set_prefix(above);
}
(Key::new(name), Item::Table(table))
}
other => (key, other),
};
t.insert_formatted(&key, item);
}
true
}
pub fn convert_inline_key(t: &mut InlineTable, name: &str, to: Kind) -> bool {
if to == Kind::Table {
return false;
}
let Some(v) = t.get_mut(name) else {
return false;
};
match convert(v, to) {
Some(new) => {
set_value(v, new);
true
}
None => false,
}
}
pub fn set_value(slot: &mut Value, new: Value) {
let decor = slot.decor().clone();
*slot = new;
*slot.decor_mut() = decor;
}
#[cfg(test)]
mod structure_tests {
use super::{add_key, remove_key, rename_key, Kind};
use toml_edit::DocumentMut;
const DOC: &str = "\
# above first
first = \"one\" # beside first
second = 2
[third]
inner = true
";
fn doc() -> DocumentMut {
DOC.parse().expect("valid TOML")
}
fn keys(d: &DocumentMut) -> Vec<String> {
d.as_table().iter().map(|(k, _)| k.to_owned()).collect()
}
#[test]
fn a_rename_keeps_its_place_and_its_comments() {
let mut d = doc();
assert!(rename_key(d.as_table_mut(), "first", "primary"));
assert_eq!(keys(&d), ["primary", "second", "third"]);
let written = d.to_string();
assert!(written.contains("# above first"), "{written}");
assert!(written.contains("# beside first"), "{written}");
assert!(written.contains("primary = \"one\""), "{written}");
assert!(!written.contains("first ="), "{written}");
}
#[test]
fn a_table_can_be_renamed() {
let mut d = doc();
assert!(rename_key(d.as_table_mut(), "third", "provenance"));
assert_eq!(keys(&d), ["first", "second", "provenance"]);
assert!(d.to_string().contains("inner = true"));
}
#[test]
fn a_rename_onto_an_existing_key_is_refused() {
let mut d = doc();
assert!(!rename_key(d.as_table_mut(), "first", "second"));
assert!(!rename_key(d.as_table_mut(), "first", ""));
assert!(!rename_key(d.as_table_mut(), "absent", "anything"));
assert_eq!(keys(&d), ["first", "second", "third"]);
assert!(d.to_string().contains("second = 2"));
}
#[test]
fn a_key_added_to_a_document_with_tables_stays_at_the_root() {
let mut d = doc();
assert!(add_key(d.as_table_mut(), "author", Kind::Text));
assert_eq!(keys(&d), ["first", "second", "third", "author"]);
let written = d.to_string();
assert!(written.contains("author = \"\""), "{written}");
assert!(
written.find("author").unwrap() < written.find("[third]").unwrap(),
"an added key must not fall inside the last table: {written}"
);
let back: DocumentMut = written.parse().expect("still parses");
assert!(back.as_table().contains_key("author"));
assert!(!back["third"]
.as_table()
.expect("a table")
.contains_key("author"));
}
#[test]
fn a_key_already_there_is_not_added_over() {
let mut d = doc();
assert!(!add_key(d.as_table_mut(), "first", Kind::Integer));
assert!(!add_key(d.as_table_mut(), "", Kind::Integer));
assert!(d.to_string().contains("first = \"one\""));
}
#[test]
fn every_kind_of_new_key_is_valid_toml() {
for kind in Kind::ALL {
let mut d = doc();
assert!(add_key(d.as_table_mut(), "added", kind), "{kind:?}");
let written = d.to_string();
written
.parse::<DocumentMut>()
.unwrap_or_else(|e| panic!("{kind:?} wrote something unparseable: {e}\n{written}"));
}
}
#[test]
fn removing_a_table_takes_what_is_under_it() {
let mut d = doc();
assert!(remove_key(d.as_table_mut(), "third"));
assert_eq!(keys(&d), ["first", "second"]);
assert!(!d.to_string().contains("inner"));
assert!(!remove_key(d.as_table_mut(), "third"));
}
}
#[cfg(test)]
mod inline_tests {
use super::{add_inline_key, remove_inline_key, rename_inline_key, Kind};
use toml_edit::DocumentMut;
fn doc() -> DocumentMut {
"owner = { name = \"D. Anderson\", team = \"consulting\" }\n"
.parse()
.expect("valid TOML")
}
fn owner(d: &mut DocumentMut) -> &mut toml_edit::InlineTable {
d["owner"].as_inline_table_mut().expect("an inline table")
}
#[test]
fn a_key_inside_an_inline_table_can_be_removed() {
let mut d = doc();
assert!(remove_inline_key(owner(&mut d), "name"));
assert!(!remove_inline_key(owner(&mut d), "name"));
let written = d.to_string();
assert!(!written.contains("name"), "{written}");
assert!(written.contains("team = \"consulting\""), "{written}");
}
#[test]
fn a_key_inside_an_inline_table_keeps_its_place_when_renamed() {
let mut d = doc();
assert!(rename_inline_key(owner(&mut d), "name", "who"));
let written = d.to_string();
assert!(
written.find("who").unwrap() < written.find("team").unwrap(),
"{written}"
);
assert!(written.contains("who = \"D. Anderson\""), "{written}");
assert!(!rename_inline_key(owner(&mut d), "who", "team"));
assert!(!rename_inline_key(owner(&mut d), "who", ""));
}
#[test]
fn an_inline_table_takes_values_and_not_tables() {
let mut d = doc();
assert!(add_inline_key(owner(&mut d), "since", Kind::Integer));
assert!(!add_inline_key(owner(&mut d), "nested", Kind::Table));
assert!(!add_inline_key(owner(&mut d), "name", Kind::Text));
assert!(!Kind::VALUES.contains(&Kind::Table));
let written = d.to_string();
assert!(written.contains("since = 0"), "{written}");
written.parse::<DocumentMut>().expect("still parses");
}
}
#[cfg(test)]
mod convert_tests {
use super::{convert_inline_key, convert_key, Kind};
use toml_edit::DocumentMut;
#[test]
fn a_table_and_an_inline_table_trade_places() {
let mut d: DocumentMut = "first = 1\n\n# who\n[owner]\nname = \"D\"\n\n[last]\nz = 0\n"
.parse()
.expect("valid TOML");
assert!(convert_key(d.as_table_mut(), "owner", Kind::InlineTable));
let keys: Vec<&str> = d.as_table().iter().map(|(k, _)| k).collect();
assert_eq!(keys, ["first", "owner", "last"]);
assert!(d["owner"].is_inline_table(), "{d}");
assert_eq!(
d.to_string(),
"first = 1\n\n# who\nowner = { name = \"D\" }\n\n[last]\nz = 0\n"
);
assert!(convert_key(d.as_table_mut(), "owner", Kind::Table));
assert!(d["owner"].is_table(), "{d}");
assert_eq!(
d.to_string(),
"first = 1\n\n# who\n[owner]\nname = \"D\"\n\n[last]\nz = 0\n"
);
}
#[test]
fn a_value_converts_or_is_left_alone() {
let mut d: DocumentMut = "n = \"44\" # beside\n".parse().expect("valid TOML");
assert!(convert_key(d.as_table_mut(), "n", Kind::Integer));
assert_eq!(d.to_string(), "n = 44 # beside\n");
assert!(!convert_key(d.as_table_mut(), "n", Kind::Boolean));
assert_eq!(d.to_string(), "n = 44 # beside\n");
assert!(!convert_key(d.as_table_mut(), "n", Kind::Table));
assert!(!convert_key(d.as_table_mut(), "absent", Kind::Text));
}
#[test]
fn an_inline_table_converts_its_values_and_holds_no_table() {
let mut d: DocumentMut = "t = { n = \"1\", m = { x = 1 } }\n"
.parse()
.expect("valid TOML");
let inline = d["t"].as_inline_table_mut().expect("an inline table");
assert!(convert_inline_key(inline, "n", Kind::Integer));
assert!(!convert_inline_key(inline, "m", Kind::Table));
assert!(!convert_inline_key(inline, "n", Kind::InlineTable));
assert_eq!(d.to_string(), "t = { n = 1, m = { x = 1 } }\n");
}
}
#[cfg(test)]
mod value_tests {
use super::set_value;
use toml_edit::{DocumentMut, Value};
#[test]
fn a_changed_value_keeps_its_comment_and_its_spacing() {
let mut d: DocumentMut = "title = \"before\" # beside the title\n"
.parse()
.expect("valid TOML");
set_value(
d["title"].as_value_mut().expect("a value"),
Value::from("after"),
);
assert_eq!(
d.to_string(),
"title = \"after\" # beside the title\n"
);
}
}