#![forbid(unsafe_code)]
#![warn(missing_docs, clippy::pedantic)]
pub use toml_edit;
use toml_edit::{Datetime, InlineTable, Item, Key, Table, Value};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum NewKey {
Text,
Integer,
Float,
Boolean,
Datetime,
Table,
}
impl NewKey {
pub const SCALARS: [Self; 5] = [
Self::Text,
Self::Integer,
Self::Float,
Self::Boolean,
Self::Datetime,
];
pub const ALL: [Self; 6] = [
Self::Text,
Self::Integer,
Self::Float,
Self::Boolean,
Self::Datetime,
Self::Table,
];
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Text => "text",
Self::Integer => "integer",
Self::Float => "float",
Self::Boolean => "boolean",
Self::Datetime => "date and time",
Self::Table => "table",
}
}
fn as_value(self) -> Option<Value> {
match self.item() {
Item::Value(v) => Some(v),
_ => None,
}
}
fn item(self) -> Item {
match self {
Self::Text => Item::Value(Value::from("")),
Self::Integer => Item::Value(Value::from(0_i64)),
Self::Float => Item::Value(Value::from(0.0_f64)),
Self::Boolean => Item::Value(Value::from(false)),
Self::Datetime => Item::Value(Value::from(
"1970-01-01T00:00:00Z"
.parse::<Datetime>()
.expect("the epoch is a datetime"),
)),
Self::Table => Item::Table(Table::new()),
}
}
}
pub fn add_key(t: &mut Table, name: &str, kind: NewKey) -> 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: NewKey) -> 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 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, NewKey};
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", NewKey::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", NewKey::Integer));
assert!(!add_key(d.as_table_mut(), "", NewKey::Integer));
assert!(d.to_string().contains("first = \"one\""));
}
#[test]
fn every_kind_of_new_key_is_valid_toml() {
for kind in NewKey::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, NewKey};
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", NewKey::Integer));
assert!(!add_inline_key(owner(&mut d), "nested", NewKey::Table));
assert!(!add_inline_key(owner(&mut d), "name", NewKey::Text));
assert!(!NewKey::SCALARS.contains(&NewKey::Table));
let written = d.to_string();
assert!(written.contains("since = 0"), "{written}");
written.parse::<DocumentMut>().expect("still parses");
}
}
#[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"
);
}
}