commit 9f7a403e8872fe92d300c922441f0702708b7157 Author: upstream Date: Wed Sep 2 11:42:16 2026 +0200 Upstream toml-rs/toml state of the touched files before 4a7e36a53d diff --git a/crates/toml_edit/src/inline_table.rs b/crates/toml_edit/src/inline_table.rs new file mode 100644 index 0000000..10e7ffe --- /dev/null +++ b/crates/toml_edit/src/inline_table.rs @@ -0,0 +1,750 @@ +use std::iter::FromIterator; + +use crate::key::Key; +use crate::repr::Decor; +use crate::table::{Iter, IterMut, KeyValuePairs, TableLike}; +use crate::{Item, KeyMut, RawString, Table, Value}; + +/// A TOML [`Value`] that contains a collection of [`Key`]/[`Value`] pairs +#[derive(Debug, Default, Clone)] +pub struct InlineTable { + // `trailing` represents whitespaces, newlines + // and comments in an empty array or after the trailing comma + trailing: RawString, + trailing_comma: bool, + // Whether to hide an empty table + pub(crate) implicit: bool, + // prefix before `{` and suffix after `}` + decor: Decor, + pub(crate) span: Option>, + // whether this is a proxy for dotted keys + dotted: bool, + pub(crate) items: KeyValuePairs, +} + +/// Constructors +/// +/// See also `FromIterator` +impl InlineTable { + /// Creates an empty table. + pub fn new() -> Self { + Default::default() + } + + pub(crate) fn with_pairs(items: KeyValuePairs) -> Self { + Self { + items, + ..Default::default() + } + } + + /// Convert to a table + pub fn into_table(self) -> Table { + let mut t = Table::with_pairs(self.items); + t.fmt(); + t + } +} + +/// Formatting +impl InlineTable { + /// Get key/values for values that are visually children of this table + /// + /// For example, this will return dotted keys + pub fn get_values(&self) -> Vec<(Vec<&Key>, &Value)> { + let mut values = Vec::new(); + let mut root = Vec::new(); + self.append_values(&mut root, &mut values); + values + } + + /// Helper for `get_values()`. + /// + /// `path` is the parent for this table. path is mutable to reuse allocations but no mutations + /// should be observable. + pub(crate) fn append_values<'s>( + &'s self, + path: &mut Vec<&'s Key>, + values: &mut Vec<(Vec<&'s Key>, &'s Value)>, + ) { + for (key, value) in self.items.iter() { + path.push(key); + match value { + Item::Value(Value::InlineTable(table)) if table.is_dotted() => { + table.append_values(path, values); + } + Item::Value(value) => { + values.push((path.clone(), value)); + } + Item::Table(table) => { + table.append_all_values(path, values); + } + _ => {} + } + path.pop(); + } + } + + /// Auto formats the table. + pub fn fmt(&mut self) { + decorate_inline_table(self); + } + + /// Set whether the array will use a trailing comma + pub fn set_trailing_comma(&mut self, yes: bool) { + self.trailing_comma = yes; + } + + /// Whether the array will use a trailing comma + pub fn trailing_comma(&self) -> bool { + self.trailing_comma + } + + /// Set whitespace after last element + pub fn set_trailing(&mut self, trailing: impl Into) { + self.trailing = trailing.into(); + } + + /// Whitespace after last element + pub fn trailing(&self) -> &RawString { + &self.trailing + } + + /// Sorts [Key]/[Value]-pairs of the table + /// + ///
+ /// + /// This is not recursive. + /// + ///
+ pub fn sort_values(&mut self) { + // Assuming standard tables have their position set and this won't negatively impact them + self.items.sort_keys(); + for value in self.items.values_mut() { + match value { + Item::Value(Value::InlineTable(table)) if table.is_dotted() => { + table.sort_values(); + } + _ => {} + } + } + } + + /// Sort [Key]/[Value]-pairs of the table using the using the comparison function `compare` + /// + /// The comparison function receives two key and value pairs to compare (you can sort by keys or + /// values or their combination as needed). + /// + ///
+ /// + /// This is not recursive. + /// + ///
+ pub fn sort_values_by(&mut self, mut compare: F) + where + F: FnMut(&Key, &Value, &Key, &Value) -> std::cmp::Ordering, + { + self.sort_values_by_internal(&mut compare); + } + + fn sort_values_by_internal(&mut self, compare: &mut F) + where + F: FnMut(&Key, &Value, &Key, &Value) -> std::cmp::Ordering, + { + let modified_cmp = + |key1: &Key, val1: &Item, key2: &Key, val2: &Item| -> std::cmp::Ordering { + match (val1.as_value(), val2.as_value()) { + (Some(v1), Some(v2)) => compare(key1, v1, key2, v2), + (Some(_), None) => std::cmp::Ordering::Greater, + (None, Some(_)) => std::cmp::Ordering::Less, + (None, None) => std::cmp::Ordering::Equal, + } + }; + + self.items.sort_by(modified_cmp); + for value in self.items.values_mut() { + match value { + Item::Value(Value::InlineTable(table)) if table.is_dotted() => { + table.sort_values_by_internal(compare); + } + _ => {} + } + } + } + + /// If a table has no key/value pairs and implicit, it will not be displayed. + /// + /// # Examples + /// + /// ```notrust + /// [target."x86_64/windows.json".dependencies] + /// ``` + /// + /// In the document above, tables `target` and `target."x86_64/windows.json"` are implicit. + /// + /// ``` + /// # #[cfg(feature = "parse")] { + /// # #[cfg(feature = "display")] { + /// use toml_edit::DocumentMut; + /// let mut doc = "[a]\n[a.b]\n".parse::().expect("invalid toml"); + /// + /// doc["a"].as_table_mut().unwrap().set_implicit(true); + /// assert_eq!(doc.to_string(), "[a.b]\n"); + /// # } + /// # } + /// ``` + pub(crate) fn set_implicit(&mut self, implicit: bool) { + self.implicit = implicit; + } + + /// If a table has no key/value pairs and implicit, it will not be displayed. + pub(crate) fn is_implicit(&self) -> bool { + self.implicit + } + + /// Change this table's dotted status + pub fn set_dotted(&mut self, yes: bool) { + self.dotted = yes; + } + + /// Check if this is a wrapper for dotted keys, rather than a standard table + pub fn is_dotted(&self) -> bool { + self.dotted + } + + /// Returns the surrounding whitespace + pub fn decor_mut(&mut self) -> &mut Decor { + &mut self.decor + } + + /// Returns the surrounding whitespace + pub fn decor(&self) -> &Decor { + &self.decor + } + + /// Returns an accessor to a key's formatting + pub fn key(&self, key: &str) -> Option<&'_ Key> { + self.items.get_full(key).map(|(_, key, _)| key) + } + + /// Returns an accessor to a key's formatting + pub fn key_mut(&mut self, key: &str) -> Option> { + use indexmap::map::MutableKeys; + self.items + .get_full_mut2(key) + .map(|(_, key, _)| key.as_mut()) + } + + /// The location within the original document + /// + /// This generally requires a [`Document`][crate::Document]. + pub fn span(&self) -> Option> { + self.span.clone() + } + + pub(crate) fn despan(&mut self, input: &str) { + use indexmap::map::MutableKeys; + self.span = None; + self.decor.despan(input); + self.trailing.despan(input); + for (key, value) in self.items.iter_mut2() { + key.despan(input); + value.despan(input); + } + } +} + +impl InlineTable { + /// Returns an iterator over key/value pairs. + pub fn iter(&self) -> InlineTableIter<'_> { + Box::new( + self.items + .iter() + .filter(|(_, value)| !value.is_none()) + .map(|(key, value)| (key.get(), value.as_value().unwrap())), + ) + } + + /// Returns an iterator over key/value pairs. + pub fn iter_mut(&mut self) -> InlineTableIterMut<'_> { + use indexmap::map::MutableKeys; + Box::new( + self.items + .iter_mut2() + .filter(|(_, value)| value.is_value()) + .map(|(key, value)| (key.as_mut(), value.as_value_mut().unwrap())), + ) + } + + /// Returns the number of key/value pairs. + pub fn len(&self) -> usize { + self.iter().count() + } + + /// Returns true if the table is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Clears the table, removing all key-value pairs. Keeps the allocated memory for reuse. + pub fn clear(&mut self) { + self.items.clear(); + } + + /// Gets the given key's corresponding entry in the Table for in-place manipulation. + pub fn entry(&'_ mut self, key: impl Into) -> InlineEntry<'_> { + match self.items.entry(key.into().into()) { + indexmap::map::Entry::Occupied(mut entry) => { + // Ensure it is a `Value` to simplify `InlineOccupiedEntry`'s code. + let scratch = std::mem::take(entry.get_mut()); + let scratch = Item::Value( + scratch + .into_value() + // HACK: `Item::None` is a corner case of a corner case, let's just pick a + // "safe" value + .unwrap_or_else(|_| Value::InlineTable(Default::default())), + ); + *entry.get_mut() = scratch; + + InlineEntry::Occupied(InlineOccupiedEntry { entry }) + } + indexmap::map::Entry::Vacant(entry) => InlineEntry::Vacant(InlineVacantEntry { entry }), + } + } + + /// Gets the given key's corresponding entry in the Table for in-place manipulation. + pub fn entry_format<'a>(&'a mut self, key: &Key) -> InlineEntry<'a> { + // Accept a `&Key` to be consistent with `entry` + match self.items.entry(key.clone()) { + indexmap::map::Entry::Occupied(mut entry) => { + // Ensure it is a `Value` to simplify `InlineOccupiedEntry`'s code. + let scratch = std::mem::take(entry.get_mut()); + let scratch = Item::Value( + scratch + .into_value() + // HACK: `Item::None` is a corner case of a corner case, let's just pick a + // "safe" value + .unwrap_or_else(|_| Value::InlineTable(Default::default())), + ); + *entry.get_mut() = scratch; + + InlineEntry::Occupied(InlineOccupiedEntry { entry }) + } + indexmap::map::Entry::Vacant(entry) => InlineEntry::Vacant(InlineVacantEntry { entry }), + } + } + /// Return an optional reference to the value at the given the key. + pub fn get(&self, key: &str) -> Option<&Value> { + self.items.get(key).and_then(|value| value.as_value()) + } + + /// Return an optional mutable reference to the value at the given the key. + pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> { + self.items + .get_mut(key) + .and_then(|value| value.as_value_mut()) + } + + /// Return references to the key-value pair stored for key, if it is present, else None. + pub fn get_key_value<'a>(&'a self, key: &str) -> Option<(&'a Key, &'a Item)> { + self.items.get_full(key).and_then(|(_, key, value)| { + if !value.is_none() { + Some((key, value)) + } else { + None + } + }) + } + + /// Return mutable references to the key-value pair stored for key, if it is present, else None. + pub fn get_key_value_mut<'a>(&'a mut self, key: &str) -> Option<(KeyMut<'a>, &'a mut Item)> { + use indexmap::map::MutableKeys; + self.items.get_full_mut2(key).and_then(|(_, key, value)| { + if !value.is_none() { + Some((key.as_mut(), value)) + } else { + None + } + }) + } + + /// Returns true if the table contains given key. + pub fn contains_key(&self, key: &str) -> bool { + if let Some(value) = self.items.get(key) { + value.is_value() + } else { + false + } + } + + /// Inserts a key/value pair if the table does not contain the key. + /// Returns a mutable reference to the corresponding value. + pub fn get_or_insert>( + &mut self, + key: impl Into, + value: V, + ) -> &mut Value { + let key = key.into(); + self.items + .entry(Key::new(key)) + .or_insert(Item::Value(value.into())) + .as_value_mut() + .expect("non-value type in inline table") + } + + /// Inserts a key-value pair into the map. + pub fn insert(&mut self, key: impl Into, value: Value) -> Option { + use indexmap::map::MutableEntryKey; + let key = Key::new(key); + let value = Item::Value(value); + match self.items.entry(key.clone()) { + indexmap::map::Entry::Occupied(mut entry) => { + entry.key_mut().fmt(); + let old = std::mem::replace(entry.get_mut(), value); + old.into_value().ok() + } + indexmap::map::Entry::Vacant(entry) => { + entry.insert(value); + None + } + } + } + + /// Inserts a key-value pair into the map. + pub fn insert_formatted(&mut self, key: &Key, value: Value) -> Option { + use indexmap::map::MutableEntryKey; + let value = Item::Value(value); + match self.items.entry(key.clone()) { + indexmap::map::Entry::Occupied(mut entry) => { + *entry.key_mut() = key.clone(); + let old = std::mem::replace(entry.get_mut(), value); + old.into_value().ok() + } + indexmap::map::Entry::Vacant(entry) => { + entry.insert(value); + None + } + } + } + + /// Removes an item given the key. + pub fn remove(&mut self, key: &str) -> Option { + self.items + .shift_remove(key) + .and_then(|value| value.into_value().ok()) + } + + /// Removes a key from the map, returning the stored key and value if the key was previously in the map. + pub fn remove_entry(&mut self, key: &str) -> Option<(Key, Value)> { + self.items + .shift_remove_entry(key) + .and_then(|(key, value)| Some((key, value.into_value().ok()?))) + } + + /// Retains only the elements specified by the `keep` predicate. + /// + /// In other words, remove all pairs `(key, value)` for which + /// `keep(&key, &mut value)` returns `false`. + /// + /// The elements are visited in iteration order. + pub fn retain(&mut self, mut keep: F) + where + F: FnMut(&str, &mut Value) -> bool, + { + self.items.retain(|key, item| { + item.as_value_mut() + .map(|value| keep(key, value)) + .unwrap_or(false) + }); + } +} + +#[cfg(feature = "display")] +impl std::fmt::Display for InlineTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + crate::encode::encode_table(self, f, None, ("", "")) + } +} + +impl, V: Into> Extend<(K, V)> for InlineTable { + fn extend>(&mut self, iter: T) { + for (key, value) in iter { + let key = key.into(); + let value = Item::Value(value.into()); + self.items.insert(key, value); + } + } +} + +impl, V: Into> FromIterator<(K, V)> for InlineTable { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + let mut table = Self::new(); + table.extend(iter); + table + } +} + +impl IntoIterator for InlineTable { + type Item = (String, Value); + type IntoIter = InlineTableIntoIter; + + fn into_iter(self) -> Self::IntoIter { + Box::new( + self.items + .into_iter() + .filter(|(_, value)| value.is_value()) + .map(|(key, value)| (key.into(), value.into_value().unwrap())), + ) + } +} + +impl<'s> IntoIterator for &'s InlineTable { + type Item = (&'s str, &'s Value); + type IntoIter = InlineTableIter<'s>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +fn decorate_inline_table(table: &mut InlineTable) { + use indexmap::map::MutableKeys; + for (mut key, value) in table + .items + .iter_mut2() + .filter(|(_, value)| value.is_value()) + .map(|(key, value)| (key.as_mut(), value.as_value_mut().unwrap())) + { + key.leaf_decor_mut().clear(); + key.dotted_decor_mut().clear(); + value.decor_mut().clear(); + } +} + +/// An owned iterator type over an [`InlineTable`]'s [`Key`]/[`Value`] pairs +pub type InlineTableIntoIter = Box>; +/// An iterator type over [`InlineTable`]'s [`Key`]/[`Value`] pairs +pub type InlineTableIter<'a> = Box + 'a>; +/// A mutable iterator type over [`InlineTable`]'s [`Key`]/[`Value`] pairs +pub type InlineTableIterMut<'a> = Box, &'a mut Value)> + 'a>; + +impl TableLike for InlineTable { + fn iter(&self) -> Iter<'_> { + Box::new(self.items.iter().map(|(key, value)| (key.get(), value))) + } + fn iter_mut(&mut self) -> IterMut<'_> { + use indexmap::map::MutableKeys; + Box::new( + self.items + .iter_mut2() + .map(|(key, value)| (key.as_mut(), value)), + ) + } + fn clear(&mut self) { + self.clear(); + } + fn entry<'a>(&'a mut self, key: &str) -> crate::Entry<'a> { + // Accept a `&str` rather than an owned type to keep `String`, well, internal + match self.items.entry(key.into()) { + indexmap::map::Entry::Occupied(entry) => { + crate::Entry::Occupied(crate::OccupiedEntry { entry }) + } + indexmap::map::Entry::Vacant(entry) => { + crate::Entry::Vacant(crate::VacantEntry { entry }) + } + } + } + fn entry_format<'a>(&'a mut self, key: &Key) -> crate::Entry<'a> { + // Accept a `&Key` to be consistent with `entry` + match self.items.entry(key.get().into()) { + indexmap::map::Entry::Occupied(entry) => { + crate::Entry::Occupied(crate::OccupiedEntry { entry }) + } + indexmap::map::Entry::Vacant(entry) => { + crate::Entry::Vacant(crate::VacantEntry { entry }) + } + } + } + fn get<'s>(&'s self, key: &str) -> Option<&'s Item> { + self.items.get(key) + } + fn get_mut<'s>(&'s mut self, key: &str) -> Option<&'s mut Item> { + self.items.get_mut(key) + } + fn get_key_value<'a>(&'a self, key: &str) -> Option<(&'a Key, &'a Item)> { + self.get_key_value(key) + } + fn get_key_value_mut<'a>(&'a mut self, key: &str) -> Option<(KeyMut<'a>, &'a mut Item)> { + self.get_key_value_mut(key) + } + fn contains_key(&self, key: &str) -> bool { + self.contains_key(key) + } + fn insert(&mut self, key: &str, value: Item) -> Option { + self.insert(key, value.into_value().unwrap()) + .map(Item::Value) + } + fn remove(&mut self, key: &str) -> Option { + self.remove(key).map(Item::Value) + } + + fn get_values(&self) -> Vec<(Vec<&Key>, &Value)> { + self.get_values() + } + fn fmt(&mut self) { + self.fmt(); + } + fn sort_values(&mut self) { + self.sort_values(); + } + fn set_dotted(&mut self, yes: bool) { + self.set_dotted(yes); + } + fn is_dotted(&self) -> bool { + self.is_dotted() + } + + fn key(&self, key: &str) -> Option<&'_ Key> { + self.key(key) + } + fn key_mut(&mut self, key: &str) -> Option> { + self.key_mut(key) + } +} + +// `{ key1 = value1, ... }` +pub(crate) const DEFAULT_INLINE_KEY_DECOR: (&str, &str) = (" ", " "); + +/// A view into a single location in an [`InlineTable`], which may be vacant or occupied. +pub enum InlineEntry<'a> { + /// An occupied Entry. + Occupied(InlineOccupiedEntry<'a>), + /// A vacant Entry. + Vacant(InlineVacantEntry<'a>), +} + +impl<'a> InlineEntry<'a> { + /// Returns the entry key + /// + /// # Examples + /// + /// ``` + /// use toml_edit::Table; + /// + /// let mut map = Table::new(); + /// + /// assert_eq!("hello", map.entry("hello").key()); + /// ``` + pub fn key(&self) -> &str { + match self { + InlineEntry::Occupied(e) => e.key(), + InlineEntry::Vacant(e) => e.key(), + } + } + + /// Ensures a value is in the entry by inserting the default if empty, and returns + /// a mutable reference to the value in the entry. + pub fn or_insert(self, default: Value) -> &'a mut Value { + match self { + InlineEntry::Occupied(entry) => entry.into_mut(), + InlineEntry::Vacant(entry) => entry.insert(default), + } + } + + /// Ensures a value is in the entry by inserting the result of the default function if empty, + /// and returns a mutable reference to the value in the entry. + pub fn or_insert_with Value>(self, default: F) -> &'a mut Value { + match self { + InlineEntry::Occupied(entry) => entry.into_mut(), + InlineEntry::Vacant(entry) => entry.insert(default()), + } + } +} + +/// A view into a single occupied location in an [`InlineTable`]. +pub struct InlineOccupiedEntry<'a> { + entry: indexmap::map::OccupiedEntry<'a, Key, Item>, +} + +impl<'a> InlineOccupiedEntry<'a> { + /// Gets a reference to the entry key + /// + /// # Examples + /// + /// ``` + /// use toml_edit::Table; + /// + /// let mut map = Table::new(); + /// + /// assert_eq!("foo", map.entry("foo").key()); + /// ``` + pub fn key(&self) -> &str { + self.entry.key().get() + } + + /// Gets a mutable reference to the entry key + pub fn key_mut(&mut self) -> KeyMut<'_> { + use indexmap::map::MutableEntryKey; + self.entry.key_mut().as_mut() + } + + /// Gets a reference to the value in the entry. + pub fn get(&self) -> &Value { + self.entry.get().as_value().unwrap() + } + + /// Gets a mutable reference to the value in the entry. + pub fn get_mut(&mut self) -> &mut Value { + self.entry.get_mut().as_value_mut().unwrap() + } + + /// Converts the `OccupiedEntry` into a mutable reference to the value in the entry + /// with a lifetime bound to the map itself + pub fn into_mut(self) -> &'a mut Value { + self.entry.into_mut().as_value_mut().unwrap() + } + + /// Sets the value of the entry, and returns the entry's old value + pub fn insert(&mut self, value: Value) -> Value { + let value = Item::Value(value); + self.entry.insert(value).into_value().unwrap() + } + + /// Takes the value out of the entry, and returns it + pub fn remove(self) -> Value { + self.entry.shift_remove().into_value().unwrap() + } +} + +/// A view into a single empty location in an [`InlineTable`]. +pub struct InlineVacantEntry<'a> { + entry: indexmap::map::VacantEntry<'a, Key, Item>, +} + +impl<'a> InlineVacantEntry<'a> { + /// Gets a reference to the entry key + /// + /// # Examples + /// + /// ``` + /// use toml_edit::Table; + /// + /// let mut map = Table::new(); + /// + /// assert_eq!("foo", map.entry("foo").key()); + /// ``` + pub fn key(&self) -> &str { + self.entry.key().get() + } + + /// Sets the value of the entry with the `VacantEntry`'s key, + /// and returns a mutable reference to it + pub fn insert(self, value: Value) -> &'a mut Value { + let entry = self.entry; + let value = Item::Value(value); + entry.insert(value).as_value_mut().unwrap() + } +} diff --git a/crates/toml_edit/src/table.rs b/crates/toml_edit/src/table.rs new file mode 100644 index 0000000..c9c1f2c --- /dev/null +++ b/crates/toml_edit/src/table.rs @@ -0,0 +1,821 @@ +use std::iter::FromIterator; + +use indexmap::map::IndexMap; + +use crate::key::Key; +use crate::repr::Decor; +use crate::value::DEFAULT_VALUE_DECOR; +use crate::{InlineTable, Item, KeyMut, Value}; + +/// A TOML table, a top-level collection of key/[`Value`] pairs under a header and logical +/// sub-tables +#[derive(Clone, Debug, Default)] +pub struct Table { + // Comments/spaces before and after the header + pub(crate) decor: Decor, + // Whether to hide an empty table + pub(crate) implicit: bool, + // Whether this is a proxy for dotted keys + pub(crate) dotted: bool, + // Used for putting tables back in their original order when serialising. + // + // `None` for user created tables (can be overridden with `set_position`) + doc_position: Option, + pub(crate) span: Option>, + pub(crate) items: KeyValuePairs, +} + +/// Constructors +/// +/// See also `FromIterator` +impl Table { + /// Creates an empty table. + pub fn new() -> Self { + Default::default() + } + + pub(crate) fn with_pos(doc_position: Option) -> Self { + Self { + doc_position, + ..Default::default() + } + } + + pub(crate) fn with_pairs(items: KeyValuePairs) -> Self { + Self { + items, + ..Default::default() + } + } + + /// Convert to an inline table + pub fn into_inline_table(mut self) -> InlineTable { + for (_, value) in self.items.iter_mut() { + value.make_value(); + } + let mut t = InlineTable::with_pairs(self.items); + t.fmt(); + t + } +} + +/// Formatting +impl Table { + /// Get key/values for values that are visually children of this table + /// + /// For example, this will return dotted keys + pub fn get_values(&self) -> Vec<(Vec<&Key>, &Value)> { + let mut values = Vec::new(); + let mut root = Vec::new(); + self.append_values(&mut root, &mut values); + values + } + + /// Helper for `get_values()`. + /// + /// `path` is the parent for this table. path is mutable to reuse allocations but no mutations + /// should be observable. + fn append_values<'s>( + &'s self, + path: &mut Vec<&'s Key>, + values: &mut Vec<(Vec<&'s Key>, &'s Value)>, + ) { + for (key, value) in self.items.iter() { + path.push(key); + match value { + Item::Table(table) if table.is_dotted() => { + table.append_values(path, values); + } + Item::Value(value) => { + if let Some(table) = value.as_inline_table() { + if table.is_dotted() { + table.append_values(path, values); + } else { + values.push((path.clone(), value)); + } + } else { + values.push((path.clone(), value)); + } + } + _ => {} + } + path.pop(); + } + } + + /// Helper for `get_values()`. + /// + /// `path` is the parent for this table. path is mutable to reuse allocations but no mutations + /// should be observable. + pub(crate) fn append_all_values<'s>( + &'s self, + path: &mut Vec<&'s Key>, + values: &mut Vec<(Vec<&'s Key>, &'s Value)>, + ) { + for (key, value) in self.items.iter() { + path.push(key); + match value { + Item::Table(table) => { + table.append_all_values(path, values); + } + Item::Value(value) => { + if let Some(table) = value.as_inline_table() { + if table.is_dotted() { + table.append_values(path, values); + } else { + values.push((path.clone(), value)); + } + } else { + values.push((path.clone(), value)); + } + } + _ => {} + } + path.pop(); + } + } + + /// Auto formats the table. + pub fn fmt(&mut self) { + decorate_table(self); + } + + /// Sorts [Key]/[Value]-pairs of the table + /// + ///
+ /// + /// This sorts the syntactic table (everything under the `[header]`) and not the logical map of + /// key-value pairs. + /// This does not affect the order of [sub-tables][Table] or [sub-arrays][crate::ArrayOfTables]. + /// This is not recursive. + /// + ///
+ pub fn sort_values(&mut self) { + // Assuming standard tables have their doc_position set and this won't negatively impact them + self.items.sort_keys(); + for value in self.items.values_mut() { + match value { + Item::Table(table) if table.is_dotted() => { + table.sort_values(); + } + _ => {} + } + } + } + + /// Sort [Key]/[Value]-pairs of the table using the using the comparison function `compare` + /// + /// The comparison function receives two key and value pairs to compare (you can sort by keys or + /// values or their combination as needed). + /// + ///
+ /// + /// This sorts the syntactic table (everything under the `[header]`) and not the logical map of + /// key-value pairs. + /// This does not affect the order of [sub-tables][Table] or [sub-arrays][crate::ArrayOfTables]. + /// This is not recursive. + /// + ///
+ pub fn sort_values_by(&mut self, mut compare: F) + where + F: FnMut(&Key, &Item, &Key, &Item) -> std::cmp::Ordering, + { + self.sort_values_by_internal(&mut compare); + } + + fn sort_values_by_internal(&mut self, compare: &mut F) + where + F: FnMut(&Key, &Item, &Key, &Item) -> std::cmp::Ordering, + { + let modified_cmp = + |key1: &Key, val1: &Item, key2: &Key, val2: &Item| -> std::cmp::Ordering { + compare(key1, val1, key2, val2) + }; + + self.items.sort_by(modified_cmp); + + for value in self.items.values_mut() { + match value { + Item::Table(table) if table.is_dotted() => { + table.sort_values_by_internal(compare); + } + _ => {} + } + } + } + + /// If a table has no key/value pairs and implicit, it will not be displayed. + /// + /// # Examples + /// + /// ```notrust + /// [target."x86_64/windows.json".dependencies] + /// ``` + /// + /// In the document above, tables `target` and `target."x86_64/windows.json"` are implicit. + /// + /// ``` + /// # #[cfg(feature = "parse")] { + /// # #[cfg(feature = "display")] { + /// use toml_edit::DocumentMut; + /// let mut doc = "[a]\n[a.b]\n".parse::().expect("invalid toml"); + /// + /// doc["a"].as_table_mut().unwrap().set_implicit(true); + /// assert_eq!(doc.to_string(), "[a.b]\n"); + /// # } + /// # } + /// ``` + pub fn set_implicit(&mut self, implicit: bool) { + self.implicit = implicit; + } + + /// If a table has no key/value pairs and implicit, it will not be displayed. + pub fn is_implicit(&self) -> bool { + self.implicit + } + + /// Change this table's dotted status + pub fn set_dotted(&mut self, yes: bool) { + self.dotted = yes; + } + + /// Check if this is a wrapper for dotted keys, rather than a standard table + pub fn is_dotted(&self) -> bool { + self.dotted + } + + /// Sets the position of the `Table` within the [`DocumentMut`][crate::DocumentMut]. + /// + /// Use `None` for having an unspecified location + pub fn set_position(&mut self, doc_position: Option) { + self.doc_position = doc_position; + } + + /// The position of the `Table` within the [`DocumentMut`][crate::DocumentMut]. + /// + /// Returns `None` if the `Table` was created manually (i.e. not via parsing) + /// in which case its position is set automatically. This can be overridden with + /// [`Table::set_position`]. + pub fn position(&self) -> Option { + self.doc_position + } + + /// Returns the surrounding whitespace + pub fn decor_mut(&mut self) -> &mut Decor { + &mut self.decor + } + + /// Returns the decor associated with a given key of the table. + pub fn decor(&self) -> &Decor { + &self.decor + } + + /// Returns an accessor to a key's formatting + pub fn key(&self, key: &str) -> Option<&'_ Key> { + self.items.get_full(key).map(|(_, key, _)| key) + } + + /// Returns an accessor to a key's formatting + pub fn key_mut(&mut self, key: &str) -> Option> { + use indexmap::map::MutableKeys; + self.items + .get_full_mut2(key) + .map(|(_, key, _)| key.as_mut()) + } + + /// The location within the original document + /// + /// This generally requires a [`Document`][crate::Document]. + pub fn span(&self) -> Option> { + self.span.clone() + } + + pub(crate) fn despan(&mut self, input: &str) { + use indexmap::map::MutableKeys; + self.span = None; + self.decor.despan(input); + for (key, value) in self.items.iter_mut2() { + key.despan(input); + value.despan(input); + } + } +} + +impl Table { + /// Returns an iterator over all key/value pairs, including empty. + pub fn iter(&self) -> Iter<'_> { + Box::new( + self.items + .iter() + .filter(|(_, value)| !value.is_none()) + .map(|(key, value)| (key.get(), value)), + ) + } + + /// Returns an mutable iterator over all key/value pairs, including empty. + pub fn iter_mut(&mut self) -> IterMut<'_> { + use indexmap::map::MutableKeys; + Box::new( + self.items + .iter_mut2() + .filter(|(_, value)| !value.is_none()) + .map(|(key, value)| (key.as_mut(), value)), + ) + } + + /// Returns the number of non-empty items in the table. + pub fn len(&self) -> usize { + self.iter().count() + } + + /// Returns true if the table is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Clears the table, removing all key-value pairs. Keeps the allocated memory for reuse. + pub fn clear(&mut self) { + self.items.clear(); + } + + /// Gets the given key's corresponding entry in the Table for in-place manipulation. + pub fn entry<'a>(&'a mut self, key: &str) -> Entry<'a> { + // Accept a `&str` rather than an owned type to keep `String`, well, internal + match self.items.entry(key.into()) { + indexmap::map::Entry::Occupied(entry) => Entry::Occupied(OccupiedEntry { entry }), + indexmap::map::Entry::Vacant(entry) => Entry::Vacant(VacantEntry { entry }), + } + } + + /// Gets the given key's corresponding entry in the Table for in-place manipulation. + pub fn entry_format<'a>(&'a mut self, key: &Key) -> Entry<'a> { + // Accept a `&Key` to be consistent with `entry` + match self.items.entry(key.clone()) { + indexmap::map::Entry::Occupied(entry) => Entry::Occupied(OccupiedEntry { entry }), + indexmap::map::Entry::Vacant(entry) => Entry::Vacant(VacantEntry { entry }), + } + } + + /// Returns an optional reference to an item given the key. + pub fn get<'a>(&'a self, key: &str) -> Option<&'a Item> { + self.items.get(key).filter(|value| !value.is_none()) + } + + /// Returns an optional mutable reference to an item given the key. + pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Item> { + self.items.get_mut(key).filter(|value| !value.is_none()) + } + + /// Return references to the key-value pair stored for key, if it is present, else None. + pub fn get_key_value<'a>(&'a self, key: &str) -> Option<(&'a Key, &'a Item)> { + self.items.get_full(key).and_then(|(_, key, value)| { + if !value.is_none() { + Some((key, value)) + } else { + None + } + }) + } + + /// Return mutable references to the key-value pair stored for key, if it is present, else None. + pub fn get_key_value_mut<'a>(&'a mut self, key: &str) -> Option<(KeyMut<'a>, &'a mut Item)> { + use indexmap::map::MutableKeys; + self.items.get_full_mut2(key).and_then(|(_, key, value)| { + if !value.is_none() { + Some((key.as_mut(), value)) + } else { + None + } + }) + } + + /// Returns true if the table contains an item with the given key. + pub fn contains_key(&self, key: &str) -> bool { + if let Some(value) = self.items.get(key) { + !value.is_none() + } else { + false + } + } + + /// Returns true if the table contains a table with the given key. + pub fn contains_table(&self, key: &str) -> bool { + if let Some(value) = self.items.get(key) { + value.is_table() + } else { + false + } + } + + /// Returns true if the table contains a value with the given key. + pub fn contains_value(&self, key: &str) -> bool { + if let Some(value) = self.items.get(key) { + value.is_value() + } else { + false + } + } + + /// Returns true if the table contains an array of tables with the given key. + pub fn contains_array_of_tables(&self, key: &str) -> bool { + if let Some(value) = self.items.get(key) { + value.is_array_of_tables() + } else { + false + } + } + + /// Inserts a key-value pair into the map. + pub fn insert(&mut self, key: &str, item: Item) -> Option { + use indexmap::map::MutableEntryKey; + let key = Key::new(key); + match self.items.entry(key.clone()) { + indexmap::map::Entry::Occupied(mut entry) => { + entry.key_mut().fmt(); + let old = std::mem::replace(entry.get_mut(), item); + Some(old) + } + indexmap::map::Entry::Vacant(entry) => { + entry.insert(item); + None + } + } + } + + /// Inserts a key-value pair into the map. + pub fn insert_formatted(&mut self, key: &Key, item: Item) -> Option { + use indexmap::map::MutableEntryKey; + match self.items.entry(key.clone()) { + indexmap::map::Entry::Occupied(mut entry) => { + *entry.key_mut() = key.clone(); + let old = std::mem::replace(entry.get_mut(), item); + Some(old) + } + indexmap::map::Entry::Vacant(entry) => { + entry.insert(item); + None + } + } + } + + /// Removes an item given the key. + pub fn remove(&mut self, key: &str) -> Option { + self.items.shift_remove(key) + } + + /// Removes a key from the map, returning the stored key and value if the key was previously in the map. + pub fn remove_entry(&mut self, key: &str) -> Option<(Key, Item)> { + self.items.shift_remove_entry(key) + } + + /// Retains only the elements specified by the `keep` predicate. + /// + /// In other words, remove all pairs `(key, item)` for which + /// `keep(&key, &mut item)` returns `false`. + /// + /// The elements are visited in iteration order. + pub fn retain(&mut self, mut keep: F) + where + F: FnMut(&str, &mut Item) -> bool, + { + self.items.retain(|key, value| keep(key, value)); + } +} + +#[cfg(feature = "display")] +impl std::fmt::Display for Table { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let children = self.get_values(); + // print table body + for (key_path, value) in children { + crate::encode::encode_key_path_ref(&key_path, f, None, DEFAULT_KEY_DECOR)?; + write!(f, "=")?; + crate::encode::encode_value(value, f, None, DEFAULT_VALUE_DECOR)?; + writeln!(f)?; + } + Ok(()) + } +} + +impl, V: Into> Extend<(K, V)> for Table { + fn extend>(&mut self, iter: T) { + for (key, value) in iter { + let key = key.into(); + let value = value.into(); + self.items.insert(key, value); + } + } +} + +impl, V: Into> FromIterator<(K, V)> for Table { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + let mut table = Self::new(); + table.extend(iter); + table + } +} + +impl IntoIterator for Table { + type Item = (String, Item); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + Box::new(self.items.into_iter().map(|(k, value)| (k.into(), value))) + } +} + +impl<'s> IntoIterator for &'s Table { + type Item = (&'s str, &'s Item); + type IntoIter = Iter<'s>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +pub(crate) type KeyValuePairs = IndexMap; + +fn decorate_table(table: &mut Table) { + use indexmap::map::MutableKeys; + for (mut key, value) in table + .items + .iter_mut2() + .filter(|(_, value)| value.is_value()) + .map(|(key, value)| (key.as_mut(), value.as_value_mut().unwrap())) + { + key.leaf_decor_mut().clear(); + key.dotted_decor_mut().clear(); + value.decor_mut().clear(); + } +} + +// `key1 = value1` +pub(crate) const DEFAULT_ROOT_DECOR: (&str, &str) = ("", ""); +pub(crate) const DEFAULT_KEY_DECOR: (&str, &str) = ("", " "); +pub(crate) const DEFAULT_TABLE_DECOR: (&str, &str) = ("\n", ""); +pub(crate) const DEFAULT_KEY_PATH_DECOR: (&str, &str) = ("", ""); + +/// An owned iterator type over [`Table`]'s [`Key`]/[`Item`] pairs +pub type IntoIter = Box>; +/// An iterator type over [`Table`]'s [`Key`]/[`Item`] pairs +pub type Iter<'a> = Box + 'a>; +/// A mutable iterator type over [`Table`]'s [`Key`]/[`Item`] pairs +pub type IterMut<'a> = Box, &'a mut Item)> + 'a>; + +/// This trait represents either a `Table`, or an `InlineTable`. +pub trait TableLike: crate::private::Sealed { + /// Returns an iterator over key/value pairs. + fn iter(&self) -> Iter<'_>; + /// Returns an mutable iterator over all key/value pairs, including empty. + fn iter_mut(&mut self) -> IterMut<'_>; + /// Returns the number of nonempty items. + fn len(&self) -> usize { + self.iter().filter(|&(_, v)| !v.is_none()).count() + } + /// Returns true if the table is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Clears the table, removing all key-value pairs. Keeps the allocated memory for reuse. + fn clear(&mut self); + /// Gets the given key's corresponding entry in the Table for in-place manipulation. + fn entry<'a>(&'a mut self, key: &str) -> Entry<'a>; + /// Gets the given key's corresponding entry in the Table for in-place manipulation. + fn entry_format<'a>(&'a mut self, key: &Key) -> Entry<'a>; + /// Returns an optional reference to an item given the key. + fn get<'s>(&'s self, key: &str) -> Option<&'s Item>; + /// Returns an optional mutable reference to an item given the key. + fn get_mut<'s>(&'s mut self, key: &str) -> Option<&'s mut Item>; + /// Return references to the key-value pair stored for key, if it is present, else None. + fn get_key_value<'a>(&'a self, key: &str) -> Option<(&'a Key, &'a Item)>; + /// Return mutable references to the key-value pair stored for key, if it is present, else None. + fn get_key_value_mut<'a>(&'a mut self, key: &str) -> Option<(KeyMut<'a>, &'a mut Item)>; + /// Returns true if the table contains an item with the given key. + fn contains_key(&self, key: &str) -> bool; + /// Inserts a key-value pair into the map. + fn insert(&mut self, key: &str, value: Item) -> Option; + /// Removes an item given the key. + fn remove(&mut self, key: &str) -> Option; + + /// Get key/values for values that are visually children of this table + /// + /// For example, this will return dotted keys + fn get_values(&self) -> Vec<(Vec<&Key>, &Value)>; + + /// Auto formats the table. + fn fmt(&mut self); + /// Sorts [Key]/[Value]-pairs of the table + /// + ///
+ /// + /// This sorts the syntactic table (everything under the `[header]`) and not the logical map of + /// key-value pairs. + /// This does not affect the order of [sub-tables][Table] or [sub-arrays][crate::ArrayOfTables]. + /// This is not recursive. + /// + ///
+ fn sort_values(&mut self); + /// Change this table's dotted status + fn set_dotted(&mut self, yes: bool); + /// Check if this is a wrapper for dotted keys, rather than a standard table + fn is_dotted(&self) -> bool; + + /// Returns an accessor to a key's formatting + fn key(&self, key: &str) -> Option<&'_ Key>; + /// Returns an accessor to a key's formatting + fn key_mut(&mut self, key: &str) -> Option>; +} + +impl TableLike for Table { + fn iter(&self) -> Iter<'_> { + self.iter() + } + fn iter_mut(&mut self) -> IterMut<'_> { + self.iter_mut() + } + fn clear(&mut self) { + self.clear(); + } + fn entry<'a>(&'a mut self, key: &str) -> Entry<'a> { + self.entry(key) + } + fn entry_format<'a>(&'a mut self, key: &Key) -> Entry<'a> { + self.entry_format(key) + } + fn get<'s>(&'s self, key: &str) -> Option<&'s Item> { + self.get(key) + } + fn get_mut<'s>(&'s mut self, key: &str) -> Option<&'s mut Item> { + self.get_mut(key) + } + fn get_key_value<'a>(&'a self, key: &str) -> Option<(&'a Key, &'a Item)> { + self.get_key_value(key) + } + fn get_key_value_mut<'a>(&'a mut self, key: &str) -> Option<(KeyMut<'a>, &'a mut Item)> { + self.get_key_value_mut(key) + } + fn contains_key(&self, key: &str) -> bool { + self.contains_key(key) + } + fn insert(&mut self, key: &str, value: Item) -> Option { + self.insert(key, value) + } + fn remove(&mut self, key: &str) -> Option { + self.remove(key) + } + + fn get_values(&self) -> Vec<(Vec<&Key>, &Value)> { + self.get_values() + } + fn fmt(&mut self) { + self.fmt(); + } + fn sort_values(&mut self) { + self.sort_values(); + } + fn is_dotted(&self) -> bool { + self.is_dotted() + } + fn set_dotted(&mut self, yes: bool) { + self.set_dotted(yes); + } + + fn key(&self, key: &str) -> Option<&'_ Key> { + self.key(key) + } + fn key_mut(&mut self, key: &str) -> Option> { + self.key_mut(key) + } +} + +/// A view into a single location in a [`Table`], which may be vacant or occupied. +pub enum Entry<'a> { + /// An occupied Entry. + Occupied(OccupiedEntry<'a>), + /// A vacant Entry. + Vacant(VacantEntry<'a>), +} + +impl<'a> Entry<'a> { + /// Returns the entry key + /// + /// # Examples + /// + /// ``` + /// use toml_edit::Table; + /// + /// let mut map = Table::new(); + /// + /// assert_eq!("hello", map.entry("hello").key()); + /// ``` + pub fn key(&self) -> &str { + match self { + Entry::Occupied(e) => e.key(), + Entry::Vacant(e) => e.key(), + } + } + + /// Ensures a value is in the entry by inserting the default if empty, and returns + /// a mutable reference to the value in the entry. + pub fn or_insert(self, default: Item) -> &'a mut Item { + match self { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => entry.insert(default), + } + } + + /// Ensures a value is in the entry by inserting the result of the default function if empty, + /// and returns a mutable reference to the value in the entry. + pub fn or_insert_with Item>(self, default: F) -> &'a mut Item { + match self { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => entry.insert(default()), + } + } +} + +/// A view into a single occupied location in a [`Table`]. +pub struct OccupiedEntry<'a> { + pub(crate) entry: indexmap::map::OccupiedEntry<'a, Key, Item>, +} + +impl<'a> OccupiedEntry<'a> { + /// Gets a reference to the entry key + /// + /// # Examples + /// + /// ``` + /// use toml_edit::Table; + /// + /// let mut map = Table::new(); + /// + /// assert_eq!("foo", map.entry("foo").key()); + /// ``` + pub fn key(&self) -> &str { + self.entry.key().get() + } + + /// Gets a mutable reference to the entry key + pub fn key_mut(&mut self) -> KeyMut<'_> { + use indexmap::map::MutableEntryKey; + self.entry.key_mut().as_mut() + } + + /// Gets a reference to the value in the entry. + pub fn get(&self) -> &Item { + self.entry.get() + } + + /// Gets a mutable reference to the value in the entry. + pub fn get_mut(&mut self) -> &mut Item { + self.entry.get_mut() + } + + /// Converts the `OccupiedEntry` into a mutable reference to the value in the entry + /// with a lifetime bound to the map itself + pub fn into_mut(self) -> &'a mut Item { + self.entry.into_mut() + } + + /// Sets the value of the entry, and returns the entry's old value + pub fn insert(&mut self, value: Item) -> Item { + self.entry.insert(value) + } + + /// Takes the value out of the entry, and returns it + pub fn remove(self) -> Item { + self.entry.shift_remove() + } +} + +/// A view into a single empty location in a [`Table`]. +pub struct VacantEntry<'a> { + pub(crate) entry: indexmap::map::VacantEntry<'a, Key, Item>, +} + +impl<'a> VacantEntry<'a> { + /// Gets a reference to the entry key + /// + /// # Examples + /// + /// ``` + /// use toml_edit::Table; + /// + /// let mut map = Table::new(); + /// + /// assert_eq!("foo", map.entry("foo").key()); + /// ``` + pub fn key(&self) -> &str { + self.entry.key().get() + } + + /// Sets the value of the entry with the `VacantEntry`'s key, + /// and returns a mutable reference to it + pub fn insert(self, value: Item) -> &'a mut Item { + let entry = self.entry; + entry.insert(value) + } +} diff --git a/crates/toml_edit/tests/testsuite/edit.rs b/crates/toml_edit/tests/testsuite/edit.rs new file mode 100644 index 0000000..4d7d1de --- /dev/null +++ b/crates/toml_edit/tests/testsuite/edit.rs @@ -0,0 +1,1901 @@ +use std::iter::FromIterator; + +use snapbox::assert_data_eq; +use snapbox::prelude::*; +use snapbox::str; +use toml_edit::{DocumentMut, Item, Key, Table, Value, array, table, value}; + +macro_rules! parse_key { + ($s:expr) => {{ + let key = $s.parse::(); + assert!(key.is_ok()); + key.unwrap() + }}; +} + +macro_rules! as_table { + ($e:ident) => {{ + assert!($e.is_table()); + $e.as_table_mut().unwrap() + }}; +} + +struct Test { + doc: DocumentMut, +} + +fn given(input: &str) -> Test { + let doc = input.parse::(); + assert!(doc.is_ok()); + Test { doc: doc.unwrap() } +} + +impl Test { + fn running(&mut self, func: F) -> &mut Self + where + F: Fn(&mut Table), + { + { + let root = self.doc.as_table_mut(); + func(root); + } + self + } + fn running_on_doc(&mut self, func: F) -> &mut Self + where + F: Fn(&mut DocumentMut), + { + { + func(&mut self.doc); + } + self + } + + #[track_caller] + fn produces_display(&self, expected: snapbox::data::Inline) -> &Self { + assert_data_eq!(self.doc.to_string(), expected.raw()); + self + } +} + +#[test] +fn assign_whitespace() { + let input = r#" + # top comment + [ grandparent . parent ] # table comment + # table-value sep comment + key . child = 'value' # key comment + key . inline-empty = { } # empty inline-table comment + key . inline = { inline . child = 'inline-value' } # inline-table comment + key . array-empty = [ # inside empty array comment + ] # after empty array comment + key . array = [ # start of array comment + 'one' , # one comment + 'two' , # two comment +] # after array comment + # table-table sep comment + [ another . table ] # table comment + # final comment +"#; + assert_data_eq!( + input.parse::().unwrap().to_debug(), + str![[r#" +DocumentMut { + root: Table( + Table { + decor: Decor { + prefix: "default", + suffix: "default", + }, + implicit: false, + dotted: false, + doc_position: None, + span: None, + items: { + Key { + key: "grandparent", + repr: Some( + "grandparent", + ), + leaf_decor: Decor { + prefix: "default", + suffix: "default", + }, + dotted_decor: Decor { + prefix: empty, + suffix: " ", + }, + }: Table( + Table { + decor: Decor { + prefix: "default", + suffix: "default", + }, + implicit: true, + dotted: false, + doc_position: None, + span: None, + items: { + Key { + key: "parent", + repr: Some( + "parent", + ), + leaf_decor: Decor { + prefix: " ", + suffix: " ", + }, + dotted_decor: Decor { + prefix: " ", + suffix: empty, + }, + }: Table( + Table { + decor: Decor { + prefix: "\n # top comment\n ", + suffix: " # table comment", + }, + implicit: false, + dotted: false, + doc_position: Some( + 1, + ), + span: None, + items: { + Key { + key: "key", + repr: Some( + "key", + ), + leaf_decor: Decor { + prefix: "default", + suffix: "default", + }, + dotted_decor: Decor { + prefix: empty, + suffix: " ", + }, + }: Table( + Table { + decor: Decor { + prefix: "default", + suffix: "default", + }, + implicit: true, + dotted: true, + doc_position: None, + span: None, + items: { + Key { + key: "child", + repr: Some( + "child", + ), + leaf_decor: Decor { + prefix: " # table-value sep comment\n ", + suffix: " ", + }, + dotted_decor: Decor { + prefix: " ", + suffix: empty, + }, + }: Value( + String( + Formatted { + value: "value", + repr: "'value'", + decor: Decor { + prefix: " ", + suffix: " # key comment", + }, + }, + ), + ), + Key { + key: "inline-empty", + repr: Some( + "inline-empty", + ), + leaf_decor: Decor { + prefix: " ", + suffix: " ", + }, + dotted_decor: Decor { + prefix: " ", + suffix: empty, + }, + }: Value( + InlineTable( + InlineTable { + trailing: " ", + trailing_comma: false, + implicit: false, + decor: Decor { + prefix: " ", + suffix: " # empty inline-table comment", + }, + span: None, + dotted: false, + items: {}, + }, + ), + ), + Key { + key: "inline", + repr: Some( + "inline", + ), + leaf_decor: Decor { + prefix: " ", + suffix: " ", + }, + dotted_decor: Decor { + prefix: " ", + suffix: empty, + }, + }: Value( + InlineTable( + InlineTable { + trailing: empty, + trailing_comma: false, + implicit: false, + decor: Decor { + prefix: " ", + suffix: " # inline-table comment", + }, + span: None, + dotted: false, + items: { + Key { + key: "inline", + repr: Some( + "inline", + ), + leaf_decor: Decor { + prefix: "default", + suffix: "default", + }, + dotted_decor: Decor { + prefix: empty, + suffix: " ", + }, + }: Value( + InlineTable( + InlineTable { + trailing: empty, + trailing_comma: false, + implicit: true, + decor: Decor { + prefix: "default", + suffix: "default", + }, + span: None, + dotted: true, + items: { + Key { + key: "child", + repr: Some( + "child", + ), + leaf_decor: Decor { + prefix: " ", + suffix: " ", + }, + dotted_decor: Decor { + prefix: " ", + suffix: empty, + }, + }: Value( + String( + Formatted { + value: "inline-value", + repr: "'inline-value'", + decor: Decor { + prefix: " ", + suffix: " ", + }, + }, + ), + ), + }, + }, + ), + ), + }, + }, + ), + ), + Key { + key: "array-empty", + repr: Some( + "array-empty", + ), + leaf_decor: Decor { + prefix: " ", + suffix: " ", + }, + dotted_decor: Decor { + prefix: " ", + suffix: empty, + }, + }: Value( + Array( + Array { + trailing: " # inside empty array comment\n ", + trailing_comma: false, + decor: Decor { + prefix: " ", + suffix: " # after empty array comment", + }, + span: None, + values: [], + }, + ), + ), + Key { + key: "array", + repr: Some( + "array", + ), + leaf_decor: Decor { + prefix: " ", + suffix: " ", + }, + dotted_decor: Decor { + prefix: " ", + suffix: empty, + }, + }: Value( + Array( + Array { + trailing: " # two comment\n", + trailing_comma: true, + decor: Decor { + prefix: " ", + suffix: " # after array comment", + }, + span: None, + values: [ + Value( + String( + Formatted { + value: "one", + repr: "'one'", + decor: Decor { + prefix: " # start of array comment\n ", + suffix: " ", + }, + }, + ), + ), + Value( + String( + Formatted { + value: "two", + repr: "'two'", + decor: Decor { + prefix: " # one comment\n ", + suffix: " ", + }, + }, + ), + ), + ], + }, + ), + ), + }, + }, + ), + }, + }, + ), + }, + }, + ), + Key { + key: "another", + repr: Some( + "another", + ), + leaf_decor: Decor { + prefix: "default", + suffix: "default", + }, + dotted_decor: Decor { + prefix: empty, + suffix: " ", + }, + }: Table( + Table { + decor: Decor { + prefix: "default", + suffix: "default", + }, + implicit: true, + dotted: false, + doc_position: None, + span: None, + items: { + Key { + key: "table", + repr: Some( + "table", + ), + leaf_decor: Decor { + prefix: " ", + suffix: " ", + }, + dotted_decor: Decor { + prefix: " ", + suffix: empty, + }, + }: Table( + Table { + decor: Decor { + prefix: " # table-table sep comment\n ", + suffix: " # table comment", + }, + implicit: false, + dotted: false, + doc_position: Some( + 2, + ), + span: None, + items: {}, + }, + ), + }, + }, + ), + }, + }, + ), + trailing: " # final comment\n", +} + +"#]] + .raw() + ); +} + +#[test] +fn test_add_root_decor() { + given( + r#"[package] +name = "hello" +version = "1.0.0" + +[[bin]] +name = "world" +path = "src/bin/world/main.rs" + +[dependencies] +nom = "4.0" # future is here + +[[bin]] +name = "delete me please" +path = "src/bin/dmp/main.rs""#, + ) + .running_on_doc(|document| { + document.decor_mut().set_prefix("# Some Header\n\n"); + document.decor_mut().set_suffix("# Some Footer"); + document.set_trailing("\n\ntrailing..."); + }) + .produces_display(str![[r#" +# Some Header + +[package] +name = "hello" +version = "1.0.0" + +[[bin]] +name = "world" +path = "src/bin/world/main.rs" + +[dependencies] +nom = "4.0" # future is here + +[[bin]] +name = "delete me please" +path = "src/bin/dmp/main.rs" +# Some Footer + +trailing... +"#]]); +} + +/// Tests that default decor is None for both suffix and prefix and that this means empty strings +#[test] +fn test_no_root_decor() { + given( + r#"[package] +name = "hello" +version = "1.0.0" + +[[bin]] +name = "world" +path = "src/bin/world/main.rs" + +[dependencies] +nom = "4.0" # future is here + +[[bin]] +name = "delete me please" +path = "src/bin/dmp/main.rs""#, + ) + .running_on_doc(|document| { + assert!(document.decor().prefix().is_none()); + assert!(document.decor().suffix().is_none()); + document.set_trailing("\n\ntrailing..."); + }) + .produces_display(str![[r#" +[package] +name = "hello" +version = "1.0.0" + +[[bin]] +name = "world" +path = "src/bin/world/main.rs" + +[dependencies] +nom = "4.0" # future is here + +[[bin]] +name = "delete me please" +path = "src/bin/dmp/main.rs" + + +trailing... +"#]]); +} + +// insertion + +#[test] +fn test_insert_leaf_table() { + given( + r#"[servers] + + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [other.table]"#, + ) + .running(|root| { + root["servers"]["beta"] = table(); + root["servers"]["beta"]["ip"] = value("10.0.0.2"); + root["servers"]["beta"]["dc"] = value("eqdc10"); + }) + .produces_display(str![[r#" +[servers] + + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + +[servers.beta] +ip = "10.0.0.2" +dc = "eqdc10" + + [other.table] + +"#]]); +} + +#[test] +fn test_inserted_leaf_table_goes_after_last_sibling() { + given( + r#" + [package] + [dependencies] + [[example]] + [dependencies.opencl] + [dev-dependencies]"#, + ) + .running(|root| { + root["dependencies"]["newthing"] = table(); + }) + .produces_display(str![[r#" + + [package] + [dependencies] + [[example]] + [dependencies.opencl] + +[dependencies.newthing] + [dev-dependencies] + +"#]]); +} + +#[test] +fn test_inserting_tables_from_different_parsed_docs() { + given("[a]") + .running(|root| { + let other = "[b]".parse::().unwrap(); + root["b"] = other["b"].clone(); + }) + .produces_display(str![[r#" +[a] +[b] + +"#]]); +} +#[test] +fn test_insert_nonleaf_table() { + given( + r#" + [other.table]"#, + ) + .running(|root| { + root["servers"] = table(); + root["servers"]["alpha"] = table(); + root["servers"]["alpha"]["ip"] = value("10.0.0.1"); + root["servers"]["alpha"]["dc"] = value("eqdc10"); + }) + .produces_display(str![[r#" + + [other.table] + +[servers] + +[servers.alpha] +ip = "10.0.0.1" +dc = "eqdc10" + +"#]]); +} + +#[test] +fn test_insert_array() { + given( + r#" + [package] + title = "withoutarray""#, + ) + .running(|root| { + root["bin"] = array(); + assert!(root["bin"].is_array_of_tables()); + let array = root["bin"].as_array_of_tables_mut().unwrap(); + { + let mut table = Table::new(); + table["hello"] = value("world"); + array.push(table); + } + array.push(Table::new()); + }) + .produces_display(str![[r#" + + [package] + title = "withoutarray" + +[[bin]] +hello = "world" + +[[bin]] + +"#]]); +} + +#[test] +fn test_insert_values() { + given( + r#" + [tbl.son]"#, + ) + .running(|root| { + root["tbl"]["key1"] = value("value1"); + root["tbl"]["key2"] = value(42); + root["tbl"]["key3"] = value(8.1415926); + }) + .produces_display(str![[r#" +[tbl] +key1 = "value1" +key2 = 42 +key3 = 8.1415926 + + [tbl.son] + +"#]]); +} + +#[test] +fn test_insert_key_with_quotes() { + given( + r#" + [package] + name = "foo" + + [target] + "#, + ) + .running(|root| { + root["target"]["cfg(target_os = \"linux\")"] = table(); + root["target"]["cfg(target_os = \"linux\")"]["dependencies"] = table(); + root["target"]["cfg(target_os = \"linux\")"]["dependencies"]["name"] = value("dep"); + }) + .produces_display(str![[r#" + + [package] + name = "foo" + + [target] + +[target.'cfg(target_os = "linux")'] + +[target.'cfg(target_os = "linux")'.dependencies] +name = "dep" + +"#]]); +} + +// removal + +#[test] +fn test_remove_leaf_table() { + given( + r#" + [servers] + + # Indentation (tabs and/or spaces) is allowed but not required +[servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10""#, + ) + .running(|root| { + let servers = root.get_mut("servers").unwrap(); + let servers = as_table!(servers); + assert!(servers.remove("alpha").is_some()); + }) + .produces_display(str![[r#" + + [servers] + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +"#]]); +} + +#[test] +fn test_remove_nonleaf_table() { + given( + r#" + title = "not relevant" + + # comment 1 + [a.b.c] # comment 1.1 + key1 = 1 # comment 1.2 + # comment 2 + [b] # comment 2.1 + key2 = 2 # comment 2.2 + + # comment 3 + [a] # comment 3.1 + key3 = 3 # comment 3.2 + [[a.'array']] + b = 1 + + [[a.b.c.trololololololo]] # ohohohohoho + c = 2 + key3 = 42 + + # comment on some other table + [some.other.table] + + + + + # comment 4 + [a.b] # comment 4.1 + key4 = 4 # comment 4.2 + key41 = 41 # comment 4.3 + + + "#, + ) + .running(|root| { + assert!(root.remove("a").is_some()); + }) + .produces_display(str![[r#" + + title = "not relevant" + # comment 2 + [b] # comment 2.1 + key2 = 2 # comment 2.2 + + # comment on some other table + [some.other.table] + + + "#]]); +} + +#[test] +fn test_remove_array_entry() { + given( + r#" + [package] + name = "hello" + version = "1.0.0" + + [[bin]] + name = "world" + path = "src/bin/world/main.rs" + + [dependencies] + nom = "4.0" # future is here + + [[bin]] + name = "delete me please" + path = "src/bin/dmp/main.rs""#, + ) + .running(|root| { + let dmp = root.get_mut("bin").unwrap(); + assert!(dmp.is_array_of_tables()); + let dmp = dmp.as_array_of_tables_mut().unwrap(); + assert_eq!(dmp.len(), 2); + dmp.remove(1); + assert_eq!(dmp.len(), 1); + }) + .produces_display(str![[r#" + + [package] + name = "hello" + version = "1.0.0" + + [[bin]] + name = "world" + path = "src/bin/world/main.rs" + + [dependencies] + nom = "4.0" # future is here + +"#]]); +} + +#[test] +fn test_remove_array() { + given( + r#" + [package] + name = "hello" + version = "1.0.0" + + [[bin]] + name = "world" + path = "src/bin/world/main.rs" + + [dependencies] + nom = "4.0" # future is here + + [[bin]] + name = "delete me please" + path = "src/bin/dmp/main.rs""#, + ) + .running(|root| { + assert!(root.remove("bin").is_some()); + }) + .produces_display(str![[r#" + + [package] + name = "hello" + version = "1.0.0" + + [dependencies] + nom = "4.0" # future is here + +"#]]); +} + +#[test] +fn test_remove_value() { + given( + r#" + name = "hello" + # delete this + version = "1.0.0" # please + documentation = "https://docs.rs/hello""#, + ) + .running(|root| { + let value = root.remove("version"); + assert!(value.is_some()); + let value = value.unwrap(); + assert!(value.is_value()); + let value = value.as_value().unwrap(); + assert!(value.is_str()); + let value = value.as_str().unwrap(); + assert_data_eq!(value, str!["1.0.0"].raw()); + }) + .produces_display(str![[r#" + + name = "hello" + documentation = "https://docs.rs/hello" + +"#]]); +} + +#[test] +fn test_remove_last_value_from_implicit() { + given( + r#" + [a] + b = 1"#, + ) + .running(|root| { + let a = root.get_mut("a").unwrap(); + assert!(a.is_table()); + let a = as_table!(a); + a.set_implicit(true); + let value = a.remove("b"); + assert!(value.is_some()); + let value = value.unwrap(); + assert!(value.is_value()); + let value = value.as_value().unwrap(); + assert_eq!(value.as_integer(), Some(1)); + }) + .produces_display(str![]); +} + +// values + +#[test] +fn test_sort_values() { + given( + r#" + [a.z] + + [a] + # this comment is attached to b + b = 2 # as well as this + a = 1 + c = 3 + + [a.y]"#, + ) + .running(|root| { + let a = root.get_mut("a").unwrap(); + let a = as_table!(a); + a.sort_values(); + }) + .produces_display(str![[r#" + + [a.z] + + [a] + a = 1 + # this comment is attached to b + b = 2 # as well as this + c = 3 + + [a.y] + +"#]]); +} + +#[test] +fn test_sort_values_by() { + given( + r#" + [a.z] + + [a] + # this comment is attached to b + b = 2 # as well as this + a = 1 + "c" = 3 + + [a.y]"#, + ) + .running(|root| { + let a = root.get_mut("a").unwrap(); + let a = as_table!(a); + // Sort by the representation, not the value. So "\"c\"" sorts before "a" because '"' sorts + // before 'a'. + a.sort_values_by(|k1, _, k2, _| k1.display_repr().cmp(&k2.display_repr())); + }) + .produces_display(str![[r#" + + [a.z] + + [a] + "c" = 3 + a = 1 + # this comment is attached to b + b = 2 # as well as this + + [a.y] + +"#]]); +} + +#[test] +fn test_set_position() { + given( + r#" + [package] + [dependencies] + [dependencies.opencl] + [dev-dependencies]"#, + ) + .running(|root| { + for (header, table) in root.iter_mut() { + if header == "dependencies" { + let tab = as_table!(table); + tab.set_position(Some(0)); + let (_, segmented) = tab.iter_mut().next().unwrap(); + as_table!(segmented).set_position(Some(5)); + } + } + }) + .produces_display(str![[r#" + [dependencies] + + [package] + [dev-dependencies] + [dependencies.opencl] + +"#]]); +} + +#[test] +fn test_set_position_negative() { + given( + r#" + edition = "2015" + [dependencies] + [dependencies.opencl] + [dev-dependencies]"#, + ) + .running(|root| { + let package = root.entry("package").or_insert_with(|| { + let mut t = Table::new(); + t.set_position(Some(-1)); + t.into() + }); + package["edition"] = "2024".into(); + }) + .produces_display(str![[r#" + + edition = "2015" + +[package] +edition = "2024" + [dependencies] + [dependencies.opencl] + [dev-dependencies] + +"#]]); +} + +#[test] +fn test_multiple_zero_positions() { + given( + r#" + [package] + [dependencies] + [dependencies.opencl] + a="" + [dev-dependencies]"#, + ) + .running(|root| { + for (_, table) in root.iter_mut() { + as_table!(table).set_position(Some(0)); + } + }) + .produces_display(str![[r#" + + [package] + [dependencies] + [dev-dependencies] + [dependencies.opencl] + a="" + +"#]]); +} + +#[test] +fn test_multiple_max_usize_positions() { + given( + r#" + [package] + [dependencies] + [dependencies.opencl] + a="" + [dev-dependencies]"#, + ) + .running(|root| { + for (_, table) in root.iter_mut() { + as_table!(table).set_position(Some(isize::MAX)); + } + }) + .produces_display(str![[r#" + [dependencies.opencl] + a="" + + [package] + [dependencies] + [dev-dependencies] + +"#]]); +} + +macro_rules! as_array { + ($entry:ident) => {{ + assert!($entry.is_value()); + let a = $entry.as_value_mut().unwrap(); + assert!(a.is_array()); + a.as_array_mut().unwrap() + }}; +} + +#[test] +fn test_insert_replace_into_array() { + given( + r#" + a = [1,2,3] + b = []"#, + ) + .running(|root| { + { + let a = root.get_mut("a").unwrap(); + let a = as_array!(a); + assert_eq!(a.len(), 3); + assert!(a.get(2).is_some()); + a.push(4); + assert_eq!(a.len(), 4); + a.fmt(); + } + let b = root.get_mut("b").unwrap(); + let b = as_array!(b); + assert!(b.is_empty()); + b.push("hello"); + assert_eq!(b.len(), 1); + + b.push_formatted(Value::from("world").decorated("\n", "\n")); + b.push_formatted(Value::from("test").decorated("", "")); + + b.insert(1, "beep"); + b.insert_formatted(2, Value::from("boop").decorated(" ", " ")); + + // This should preserve formatting. + assert_eq!(b.replace(2, "zoink").as_str(), Some("boop")); + // This should replace formatting. + assert_eq!( + b.replace_formatted(4, Value::from("yikes").decorated(" ", "")) + .as_str(), + Some("test") + ); + dbg!(root); + }) + .produces_display(str![[r#" + + a = [1, 2, 3, 4] + b = ["hello", "beep", "zoink" , +"world" +, "yikes"] + +"#]]); +} + +#[test] +fn test_remove_from_array() { + given( + r#" + a = [1, 2, 3, 4] + b = ["hello"]"#, + ) + .running(|root| { + { + let a = root.get_mut("a").unwrap(); + let a = as_array!(a); + assert_eq!(a.len(), 4); + assert!(a.remove(3).is_integer()); + assert_eq!(a.len(), 3); + } + let b = root.get_mut("b").unwrap(); + let b = as_array!(b); + assert_eq!(b.len(), 1); + assert!(b.remove(0).is_str()); + assert!(b.is_empty()); + }) + .produces_display(str![[r#" + + a = [1, 2, 3] + b = [] + +"#]]); +} + +#[test] +fn test_format_array() { + given( + r#" + a = [ + 1, + "2", + 3.0, + ] + "#, + ) + .running(|root| { + for (_, v) in root.iter_mut() { + if let Item::Value(Value::Array(array)) = v { + array.fmt(); + } + } + }) + .produces_display(str![[r#" + + a = [1, "2", 3.0] + "#]]); +} + +macro_rules! as_inline_table { + ($entry:ident) => {{ + assert!($entry.is_value()); + let a = $entry.as_value_mut().unwrap(); + assert!(a.is_inline_table()); + a.as_inline_table_mut().unwrap() + }}; +} + +#[test] +fn test_insert_into_inline_table() { + given( + r#" + a = {a=2, c = 3} + b = {}"#, + ) + .running(|root| { + { + let a = root.get_mut("a").unwrap(); + let a = as_inline_table!(a); + assert_eq!(a.len(), 2); + assert!(a.contains_key("a") && a.get("c").is_some() && a.get_mut("c").is_some()); + a.get_or_insert("b", 42); + assert_eq!(a.len(), 3); + a.fmt(); + } + let b = root.get_mut("b").unwrap(); + let b = as_inline_table!(b); + assert!(b.is_empty()); + b.get_or_insert("hello", "world"); + assert_eq!(b.len(), 1); + b.fmt(); + }) + .produces_display(str![[r#" + + a = { a = 2, c = 3, b = 42 } + b = { hello = "world" } + +"#]]); +} + +#[test] +fn test_remove_from_inline_table() { + given( + r#" + a = {a=2, c = 3, b = 42} + b = {'hello' = "world"}"#, + ) + .running(|root| { + { + let a = root.get_mut("a").unwrap(); + let a = as_inline_table!(a); + assert_eq!(a.len(), 3); + assert!(a.remove("c").is_some()); + assert_eq!(a.len(), 2); + } + let b = root.get_mut("b").unwrap(); + let b = as_inline_table!(b); + assert_eq!(b.len(), 1); + assert!(b.remove("hello").is_some()); + assert!(b.is_empty()); + }) + .produces_display(str![[r#" + + a = {a=2, b = 42} + b = {} + +"#]]); +} + +#[test] +fn test_as_table_like() { + given( + r#" + a = {a=2, c = 3, b = 42} + x = {} + [[bin]] + [b] + x = "y" + [empty]"#, + ) + .running(|root| { + let a = root["a"].as_table_like(); + assert!(a.is_some()); + let a = a.unwrap(); + assert_eq!(a.iter().count(), 3); + assert_eq!(a.len(), 3); + assert_eq!(a.get("a").and_then(Item::as_integer), Some(2)); + + let b = root["b"].as_table_like(); + assert!(b.is_some()); + let b = b.unwrap(); + assert_eq!(b.iter().count(), 1); + assert_eq!(b.len(), 1); + assert_eq!(b.get("x").and_then(Item::as_str), Some("y")); + + assert_eq!(root["x"].as_table_like().map(|t| t.iter().count()), Some(0)); + assert_eq!( + root["empty"].as_table_like().map(|t| t.is_empty()), + Some(true) + ); + + assert!(root["bin"].as_table_like().is_none()); + }); +} + +#[test] +fn test_inline_table_append() { + let mut a = Value::from_iter(vec![ + (parse_key!("a"), 1), + (parse_key!("b"), 2), + (parse_key!("c"), 3), + ]); + let a = a.as_inline_table_mut().unwrap(); + + let mut b = Value::from_iter(vec![ + (parse_key!("c"), 4), + (parse_key!("d"), 5), + (parse_key!("e"), 6), + ]); + let b = b.as_inline_table_mut().unwrap(); + + a.extend(b.iter()); + assert_eq!(a.len(), 5); + assert!(a.contains_key("e")); + assert_eq!(b.len(), 3); +} + +#[test] +fn test_insert_dotted_into_std_table() { + given("") + .running(|root| { + root["nixpkgs"] = table(); + + root["nixpkgs"]["src"] = table(); + root["nixpkgs"]["src"] + .as_table_like_mut() + .unwrap() + .set_dotted(true); + root["nixpkgs"]["src"]["git"] = value("https://github.com/nixos/nixpkgs"); + }) + .produces_display(str![[r#" +[nixpkgs] +src.git = "https://github.com/nixos/nixpkgs" + +"#]]); +} + +#[test] +fn test_insert_dotted_into_implicit_table() { + given("") + .running(|root| { + root["nixpkgs"] = table(); + + root["nixpkgs"]["src"]["git"] = value("https://github.com/nixos/nixpkgs"); + root["nixpkgs"]["src"] + .as_table_like_mut() + .unwrap() + .set_dotted(true); + }) + .produces_display(str![[r#" +[nixpkgs] +src.git = "https://github.com/nixos/nixpkgs" + +"#]]); +} + +#[test] +fn sorting_with_references() { + let values = vec!["foo", "qux", "bar"]; + let mut array = toml_edit::Array::from_iter(values); + array.sort_by(|lhs, rhs| lhs.as_str().cmp(&rhs.as_str())); +} + +#[test] +fn table_str_key_whitespace() { + let mut document = "bookmark = 1010".parse::().unwrap(); + + let key: &str = "bookmark"; + + document.insert(key, array()); + let table = document[key].as_array_of_tables_mut().unwrap(); + + let mut bookmark_table = Table::new(); + bookmark_table["name"] = value("test.swf".to_owned()); + table.push(bookmark_table); + + assert_data_eq!( + document.to_string(), + str![[r#" +[[bookmark]] +name = "test.swf" + +"#]] + ); +} + +#[test] +fn table_key_decor_whitespace() { + let mut document = "bookmark = 1010".parse::().unwrap(); + + let key = Key::parse(" bookmark ").unwrap().remove(0); + + document.insert_formatted(&key, array()); + let table = document[&key].as_array_of_tables_mut().unwrap(); + + let mut bookmark_table = Table::new(); + bookmark_table["name"] = value("test.swf".to_owned()); + table.push(bookmark_table); + + assert_data_eq!( + document.to_string(), + str![[r#" +[[ bookmark ]] +name = "test.swf" + +"#]] + ); +} + +#[test] +fn table_into_inline() { + let toml = r#" +[table] +string = "value" +array = [1, 2, 3] +inline = { "1" = 1, "2" = 2 } + +[table.child] +other = "world" +"#; + let mut doc = toml.parse::().unwrap(); + + doc.get_mut("table").unwrap().make_value(); + + let actual = doc.to_string(); + // `table=` is because we didn't re-format the table key, only the value + assert_data_eq!(actual, str![[r#" +table= { string = "value", array = [1, 2, 3], inline = { "1" = 1, "2" = 2 }, child = { other = "world" } } + +"#]].raw()); +} + +#[test] +fn inline_table_to_table() { + let toml = r#"table = { string = "value", array = [1, 2, 3], inline = { "1" = 1, "2" = 2 }, child = { other = "world" } } +"#; + let mut doc = toml.parse::().unwrap(); + + let t = doc.remove("table").unwrap(); + let t = match t { + Item::Value(Value::InlineTable(t)) => t, + _ => unreachable!("Unexpected {:?}", t), + }; + let t = t.into_table(); + doc.insert("table", Item::Table(t)); + + let actual = doc.to_string(); + assert_data_eq!( + actual, + str![[r#" +[table] +string = "value" +array = [1, 2, 3] +inline = { "1" = 1, "2" = 2 } +child = { other = "world" } + +"#]] + .raw() + ); +} + +#[test] +fn array_of_tables_to_array() { + let toml = r#" +[[table]] +string = "value" +array = [1, 2, 3] +inline = { "1" = 1, "2" = 2 } + +[table.child] +other = "world" + +[[table]] +string = "value" +array = [1, 2, 3] +inline = { "1" = 1, "2" = 2 } + +[table.child] +other = "world" +"#; + let mut doc = toml.parse::().unwrap(); + + doc.get_mut("table").unwrap().make_value(); + + let actual = doc.to_string(); + // `table=` is because we didn't re-format the table key, only the value + assert_data_eq!(actual, str![[r#" +table= [{ string = "value", array = [1, 2, 3], inline = { "1" = 1, "2" = 2 }, child = { other = "world" } }, { string = "value", array = [1, 2, 3], inline = { "1" = 1, "2" = 2 }, child = { other = "world" } }] + +"#]].raw()); +} + +#[test] +fn test_key_from_str() { + macro_rules! test_key { + ($s:expr, $expected:expr) => {{ + let key = $s.parse::(); + match key { + Ok(key) => assert_eq!($expected, key.get(), ""), + Err(err) => panic!("failed with {err}"), + } + }}; + } + + test_key!("a", "a"); + test_key!(r#"'hello key'"#, "hello key"); + test_key!( + r#""Jos\u00E9\U000A0000\n\t\r\f\b\"""#, + "Jos\u{00E9}\u{A0000}\n\t\r\u{c}\u{8}\"" + ); + test_key!("\"\"", ""); + test_key!("\"'hello key'bla\"", "'hello key'bla"); + test_key!( + "'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\cargo-edit-test.YizxPxxElXn9'", + "C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\cargo-edit-test.YizxPxxElXn9" + ); +} + +#[test] +fn despan_keys() { + let mut doc = r#"aaaaaa = 1"#.parse::().unwrap(); + let key = "bbb".parse::().unwrap(); + let table = doc.as_table_mut(); + table.insert_formatted( + &key, + Item::Value(Value::Integer(toml_edit::Formatted::new(2))), + ); + + assert_eq!(doc.to_string(), "aaaaaa = 1\nbbb = 2\n"); +} + +#[test] +fn key_repr_roundtrip() { + assert_key_repr_roundtrip(r#""""#, str![[r#""""#]]); + assert_key_repr_roundtrip(r#""a""#, str![[r#""a""#]]); + + assert_key_repr_roundtrip(r#""tab \t tab""#, str![[r#""tab \t tab""#]]); + assert_key_repr_roundtrip(r#""lf \n lf""#, str![[r#""lf \n lf""#]]); + assert_key_repr_roundtrip(r#""crlf \r\n crlf""#, str![[r#""crlf \r\n crlf""#]]); + assert_key_repr_roundtrip(r#""bell \b bell""#, str![[r#""bell \b bell""#]]); + assert_key_repr_roundtrip(r#""feed \f feed""#, str![[r#""feed \f feed""#]]); + assert_key_repr_roundtrip( + r#""backslash \\ backslash""#, + str![[r#""backslash \\ backslash""#]], + ); + + assert_key_repr_roundtrip(r#""squote ' squote""#, str![[r#""squote ' squote""#]]); + assert_key_repr_roundtrip( + r#""triple squote ''' triple squote""#, + str![[r#""triple squote ''' triple squote""#]], + ); + assert_key_repr_roundtrip(r#""end squote '""#, str![[r#""end squote '""#]]); + + assert_key_repr_roundtrip(r#""quote \" quote""#, str![[r#""quote \" quote""#]]); + assert_key_repr_roundtrip( + r#""triple quote \"\"\" triple quote""#, + str![[r#""triple quote \"\"\" triple quote""#]], + ); + assert_key_repr_roundtrip(r#""end quote \"""#, str![[r#""end quote \"""#]]); + assert_key_repr_roundtrip( + r#""quoted \"content\" quoted""#, + str![[r#""quoted \"content\" quoted""#]], + ); + assert_key_repr_roundtrip( + r#""squoted 'content' squoted""#, + str![[r#""squoted 'content' squoted""#]], + ); + assert_key_repr_roundtrip( + r#""mixed quoted \"start\" 'end'' mixed quote""#, + str![[r#""mixed quoted \"start\" 'end'' mixed quote""#]], + ); +} + +#[track_caller] +fn assert_key_repr_roundtrip(input: &str, expected: impl IntoData) { + let value: Key = input.parse().unwrap(); + let actual = value.to_string(); + let _: Key = actual.parse().unwrap_or_else(|_err| { + panic!( + "invalid `Key`: +``` +{actual} +``` +" + ) + }); + assert_data_eq!(actual, expected.raw()); +} + +#[test] +fn key_value_roundtrip() { + assert_key_value_roundtrip(r#""""#, str![[r#""""#]]); + assert_key_value_roundtrip(r#""a""#, str!["a"]); + + assert_key_value_roundtrip(r#""tab \t tab""#, str![[r#""tab \t tab""#]]); + assert_key_value_roundtrip(r#""lf \n lf""#, str![[r#""lf \n lf""#]]); + assert_key_value_roundtrip(r#""crlf \r\n crlf""#, str![[r#""crlf \r\n crlf""#]]); + assert_key_value_roundtrip(r#""bell \b bell""#, str![[r#""bell \b bell""#]]); + assert_key_value_roundtrip(r#""feed \f feed""#, str![[r#""feed \f feed""#]]); + assert_key_value_roundtrip( + r#""backslash \\ backslash""#, + str![[r#"'backslash \ backslash'"#]], + ); + + assert_key_value_roundtrip(r#""squote ' squote""#, str![[r#""squote ' squote""#]]); + assert_key_value_roundtrip( + r#""triple squote ''' triple squote""#, + str![[r#""triple squote ''' triple squote""#]], + ); + assert_key_value_roundtrip(r#""end squote '""#, str![[r#""end squote '""#]]); + + assert_key_value_roundtrip(r#""quote \" quote""#, str![[r#"'quote " quote'"#]]); + assert_key_value_roundtrip( + r#""triple quote \"\"\" triple quote""#, + str![[r#"'triple quote """ triple quote'"#]], + ); + assert_key_value_roundtrip(r#""end quote \"""#, str![[r#"'end quote "'"#]]); + assert_key_value_roundtrip( + r#""quoted \"content\" quoted""#, + str![[r#"'quoted "content" quoted'"#]], + ); + assert_key_value_roundtrip( + r#""squoted 'content' squoted""#, + str![[r#""squoted 'content' squoted""#]], + ); + assert_key_value_roundtrip( + r#""mixed quoted \"start\" 'end'' mixed quote""#, + str![[r#""mixed quoted \"start\" 'end'' mixed quote""#]], + ); +} + +#[track_caller] +fn assert_key_value_roundtrip(input: &str, expected: impl IntoData) { + let value: Key = input.parse().unwrap(); + let value = Key::new(value.get()); // Remove repr + let actual = value.to_string(); + let _: Key = actual.parse().unwrap_or_else(|_err| { + panic!( + "invalid `Key`: +``` +{actual} +``` +" + ) + }); + assert_data_eq!(actual, expected.raw()); +} + +#[test] +fn table_under_inline() { + let mut doc = DocumentMut::new(); + doc["tool"]["typst-test"] = table(); + doc["tool"]["typst-test"]["tests"] = value("tests"); + + assert_data_eq!( + doc.to_string(), + str![[r#" +tool = { typst-test.tests = "tests" } + +"#]] + ); +} + +#[test] +fn array_of_tables_insert_at_beginning() { + given( + r#"[[fruit]] +name = "apple" + +[[fruit]] +name = "banana" +"#, + ) + .running(|root| { + let array = root["fruit"].as_array_of_tables_mut().unwrap(); + let mut new_table = Table::new(); + new_table.insert("name", value("cherry")); + array.insert(0, new_table); + // The previously-first table (now at index 1) had no leading blank line + // from parsing; fix it up so it separates properly. + array.get_mut(1).unwrap().decor_mut().set_prefix("\n"); + }) + .produces_display(str![[r#" +[[fruit]] +name = "cherry" + +[[fruit]] +name = "apple" + +[[fruit]] +name = "banana" + +"#]]); +} + +#[test] +fn array_of_tables_insert_in_middle() { + given( + r#"[[fruit]] +name = "apple" + +[[fruit]] +name = "banana" +"#, + ) + .running(|root| { + let array = root["fruit"].as_array_of_tables_mut().unwrap(); + let mut new_table = Table::new(); + new_table.insert("name", value("cherry")); + // Give the new table a leading blank line to match the existing style. + new_table.decor_mut().set_prefix("\n"); + array.insert(1, new_table); + }) + .produces_display(str![[r#" +[[fruit]] +name = "apple" + +[[fruit]] +name = "cherry" + +[[fruit]] +name = "banana" + +"#]]); +} + +#[test] +fn array_of_tables_insert_at_end() { + given( + r#"[[fruit]] +name = "apple" + +[[fruit]] +name = "banana" +"#, + ) + .running(|root| { + let array = root["fruit"].as_array_of_tables_mut().unwrap(); + let mut new_table = Table::new(); + new_table.insert("name", value("cherry")); + // Give the new table a leading blank line to match the existing style. + new_table.decor_mut().set_prefix("\n"); + array.insert(2, new_table); + }) + .produces_display(str![[r#" +[[fruit]] +name = "apple" + +[[fruit]] +name = "banana" + +[[fruit]] +name = "cherry" + +"#]]); +} + +#[test] +#[should_panic] +fn array_of_tables_insert_out_of_bounds() { + let mut array = toml_edit::ArrayOfTables::new(); + let t = Table::new(); + array.insert(1, t); +} + +#[test] +fn inline_table_to_table_with_comment() { + given( + r#" +# hello i'm a comment +foo = { bar = 1 } +"#, + ) + .running(|root| { + let mut new_foo = Table::new(); + new_foo.insert("bar", Item::Value(1.into())); + new_foo.insert("baz", Item::Value(2.into())); + *root.get_mut("foo").unwrap() = Item::Table(new_foo); + }) + .produces_display(str![[r#" + +# hello i'm a comment +[foo] +bar = 1 +baz = 2 + +"#]]); +} + +#[test] +fn inline_table_to_array_of_tables_with_comment() { + given( + r#" +# hello i'm a comment +foo = [{ bar = 1 }] +"#, + ) + .running(|root| { + let mut new_foo_entry = Table::new(); + new_foo_entry.insert("bar", Item::Value(1.into())); + new_foo_entry.insert("baz", Item::Value(2.into())); + let mut arr = toml_edit::ArrayOfTables::new(); + arr.push(new_foo_entry); + *root.get_mut("foo").unwrap() = Item::ArrayOfTables(arr); + }) + .produces_display(str![[r#" + +# hello i'm a comment +[[foo]] +bar = 1 +baz = 2 + +"#]]); +} + +#[test] +fn array_of_tables_replace() { + given( + r#"[[fruit]] +name = "apple" + +# tropical +[[fruit]] +name = "banana" +"#, + ) + .running(|root| { + let array = root["fruit"].as_array_of_tables_mut().unwrap(); + let mut new_table = Table::new(); + new_table.insert("name", value("cherry")); + let old = array.replace(1, new_table); + assert_eq!(old["name"].as_str(), Some("banana")); + }) + .produces_display(str![[r#" +[[fruit]] +name = "apple" + +[[fruit]] +name = "cherry" + +"#]]); +} + +#[test] +#[should_panic] +fn array_of_tables_replace_out_of_bounds() { + let mut array = toml_edit::ArrayOfTables::new(); + let t = Table::new(); + array.replace(0, t); +}