Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Preserve dotted-key ordering #808

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 5 additions & 61 deletions crates/toml_edit/src/inline_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use crate::key::Key;
use crate::repr::Decor;
use crate::table::{Iter, IterMut, KeyValuePairs, TableLike};
use crate::internal_table::{GetTableValues, SortTable, SortTableBy};
use crate::{InternalString, Item, KeyMut, RawString, Table, Value};

/// Type representing a TOML inline table,
Expand Down Expand Up @@ -51,30 +52,7 @@
///
/// For example, this will return dotted keys
pub fn get_values(&self) -> Vec<(Vec<&Key>, &Value)> {
let mut values = Vec::new();
let root = Vec::new();
self.append_values(&root, &mut values);
values
}

pub(crate) fn append_values<'s>(
&'s self,
parent: &[&'s Key],
values: &mut Vec<(Vec<&'s Key>, &'s Value)>,
) {
for (key, value) in self.items.iter() {
let mut path = parent.to_vec();
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, value));
}
_ => {}
}
}
GetTableValues::get_values(self)
}

/// Auto formats the table.
Expand All @@ -84,52 +62,18 @@

/// Sorts the key/value pairs by key.
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();
}
_ => {}
}
}
SortTable::sort_values(self)

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting
}

/// 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).
pub fn sort_values_by<F>(&mut self, mut compare: F)
pub fn sort_values_by<F>(&mut self, compare: F)
where
F: FnMut(&Key, &Value, &Key, &Value) -> std::cmp::Ordering,
{
self.sort_values_by_internal(&mut compare);
}

fn sort_values_by_internal<F>(&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);
}
_ => {}
}
}
SortTableBy::<Value>::sort_values_by(self, compare)

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting
}

/// If a table has no key/value pairs and implicit, it will not be displayed.
Expand Down
203 changes: 203 additions & 0 deletions crates/toml_edit/src/internal_table.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
use crate::{InlineTable, Item, Key, Table, Value};
/// a place for helper methods supporting the table-like impls

use crate::table::KeyValuePairs;

/// GetTableValues provides the logic for displaying a table's items in their parsed order

Check failure

Code scanning / clippy

item in documentation is missing backticks Error

item in documentation is missing backticks

Check failure

Code scanning / clippy

item in documentation is missing backticks Error

item in documentation is missing backticks
pub(crate) trait GetTableValues {
Comment on lines +4 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is a roundabout way to share code for manipulating KeyValuePairs. Seems like the functionality should just be writtenm for KeyValuePairs and shared

imo if I were to do this, I would

  • Move KeyValuePairs into here
  • Either rename the file to be about KeyValuePairs or rename the type (though not a fan of InternalTable though InnerTable might work)
  • Either make get_values and sort_values either non-member functions that take KeyValuePairs or make KeyValuePairs a newtype (which might add its own complexity)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to split this refactor out into its own commit. It'll likely make it easier to review and get merged which can speed up reviewing both PRs.

Also, please consider if there are intermediate steps to the above that should be broken out into their own commit (e.g. adding a newtype of we go that route)

fn items(&self) -> &KeyValuePairs;

fn get_values(&self) -> Vec<(Vec<&Key>, &Value)> {
let mut values = Vec::new();
let root = Vec::new();
self.append_values(&root, &mut values);
values
}

fn append_values<'s>(
&'s self,
parent: &[&'s Key],
values: &mut Vec<(Vec<&'s Key>, &'s Value)>,
) {
for (key, item) in self.items().iter() {
let mut path = parent.to_vec();
path.push(key);
match item {
Item::Table(table) if table.is_dotted() => {
GetTableValues::append_values(table, &path, values)

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting
}
Item::Value(Value::InlineTable(table)) if table.is_dotted() => {
GetTableValues::append_values(table, &path, values)

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting
}
Item::Value(value) => {
values.push((path, value))

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting
}
_ => {}
}
}
sort_values_by_position(values);
}
}

/// SortTable provides the logic for sorting a table's items by its keys

Check failure

Code scanning / clippy

item in documentation is missing backticks Error

item in documentation is missing backticks

Check failure

Code scanning / clippy

item in documentation is missing backticks Error

item in documentation is missing backticks
pub(crate) trait SortTable {
fn items_mut(&mut self) -> &mut KeyValuePairs;

fn sort_values(&mut self) {
// Assuming standard tables have their doc_position set and this won't negatively impact them
self.items_mut().sort_keys();
assign_sequential_key_positions(self.items_mut(), |item| {
match item {
Item::Table(table) if table.is_dotted() => {
SortTable::sort_values(table)

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting
}
Item::Value(Value::InlineTable(table)) if table.is_dotted() => {
SortTable::sort_values(table)

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting
}
_ => {}
}
});
}
}

/// SortTableBy provides the logic for sorting a table by a custom comparison

Check failure

Code scanning / clippy

item in documentation is missing backticks Error

item in documentation is missing backticks

Check failure

Code scanning / clippy

item in documentation is missing backticks Error

item in documentation is missing backticks
pub(crate) trait SortTableBy<It> : SortTable
where
It: for<'a> TryFrom<&'a Item>
{
fn sort_values_by<F>(&mut self, compare: F)
where
F: FnMut(&Key, &It, &Key, &It) -> std::cmp::Ordering,
{
// intended for `InlineTable`s, where some `Item`s might not be `Value`s,
// in the case of dotted keys mostly I expect.
// but for `Table`s the `(Some,Some)` will be the only one used.
self.sort_vals_by_direct(
&mut Self::generalize(compare)
)

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting
}

/// no modification to the comparing Fn in this one,
/// allows for slightly improved recursion that does not continuously
/// re-modify the comparison function.
fn sort_vals_by_direct<F>(&mut self, compare: &mut F)
where
F: FnMut(&Key, &Item, &Key, &Item) -> std::cmp::Ordering
{
self.items_mut().sort_by(|key1, val1, key2, val2| {
compare(key1, val1, key2, val2)
});

assign_sequential_key_positions(self.items_mut(), |value| {
match value {
Item::Table(table) if table.is_dotted() => {
SortTableBy::<Item>::sort_values_by(
table,
|k1, i1, k2, i2| {
compare(k1, i1, k2, i2)
}
)

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting
},
Item::Value(Value::InlineTable(table)) if table.is_dotted() => {
SortTableBy::<Value>::sort_values_by(
table,
|k1, i1, k2, i2| {
let s1 = &Item::from(i1);
let s2 = &Item::from(i2);
compare(k1, s1, k2, s2)
}
)

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting

Check failure

Code scanning / clippy

consider adding a ; to the last statement for consistent formatting Error

consider adding a ; to the last statement for consistent formatting
},
_ => {}
};
});
}

fn generalize<'a, F>(mut compare: F) -> Box<dyn FnMut(&Key, &Item, &Key, &Item) -> std::cmp::Ordering + 'a>

Check failure

Code scanning / clippy

very complex type used. Consider factoring parts into type definitions Error

very complex type used. Consider factoring parts into type definitions

Check failure

Code scanning / clippy

very complex type used. Consider factoring parts into type definitions Error

very complex type used. Consider factoring parts into type definitions
where
F: FnMut(&Key, &It, &Key, &It) -> std::cmp::Ordering + 'a,
{
Box::new(move |key1, s1, key2, s2| {
match (It::try_from(s1).ok(), It::try_from(s2).ok()) {
(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,
}
})
}
}

fn assign_sequential_key_positions<F>(items: &mut KeyValuePairs, mut recursive_step: F)
where
F: FnMut(&mut Item),
{
use indexmap::map::MutableKeys;
for (pos, (key, value)) in items.iter_mut2().enumerate() {
key.set_position(Some(pos));
recursive_step(value);
}
}

fn sort_values_by_position<'s>(values: &mut [(Vec<&'s Key>, &'s Value)]) {
/*
`Vec::sort_by_key` works because we add the position to _every_ item's key during parsing,
so keys without positions would be either:
1. non-leaf keys (i.e. "foo" or "bar" in dotted key "foo.bar.baz")
2. custom keys added to the doc without an explicit position
In the case of (1), we'd never see it since we only look at the last
key in a dotted-key. So, we can safely return a constant value for these cases.

To support the most intuitive behavior, we return the maximum usize, placing
position=None items at the end, so when you insert it without position, it
appends it to the end.
*/
values.sort_by_key(|(key_path, _)| {
return match key_path.last().map(|x| x.position) {
// unwrap "last()" -> unwrap "position"
Some(Some(pos)) => pos,
// either last() = None, or position = None
_ => usize::MAX
};
});
}

impl TryFrom<&Item> for Value {
type Error = String;

fn try_from(value: &Item) -> Result<Self, Self::Error> {
let err = "cannot extract Value from Non-Value Item:";
match value {
Item::Value(v) => Ok((*v).clone()),
it => it.as_value().map(|v| v.clone()).ok_or(

Check failure

Code scanning / clippy

you are using an explicit closure for cloning elements Error

you are using an explicit closure for cloning elements

Check failure

Code scanning / clippy

you are using an explicit closure for cloning elements Error

you are using an explicit closure for cloning elements
format!("{err}: {it:?}")
),
}
}

}

impl GetTableValues for Table {
fn items(&self) -> &KeyValuePairs {
&self.items
}
}
impl GetTableValues for InlineTable {
fn items(&self) -> &KeyValuePairs {
&self.items
}
}

impl SortTable for Table {
fn items_mut(&mut self) -> &mut KeyValuePairs {
&mut self.items
}
}
impl SortTable for InlineTable {
fn items_mut(&mut self) -> &mut KeyValuePairs {
&mut self.items
}
}

impl SortTableBy<Item> for Table {}
impl SortTableBy<Value> for InlineTable {}
19 changes: 19 additions & 0 deletions crates/toml_edit/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct Key {
pub(crate) repr: Option<Repr>,
pub(crate) leaf_decor: Decor,
pub(crate) dotted_decor: Decor,
pub(crate) position: Option<usize>,
}

impl Key {
Expand All @@ -42,6 +43,7 @@ impl Key {
repr: None,
leaf_decor: Default::default(),
dotted_decor: Default::default(),
position: Default::default(),
}
}

Expand Down Expand Up @@ -76,6 +78,12 @@ impl Key {
self
}

/// While creating the `Key`, add a table position to it
pub fn with_position(mut self, position: Option<usize>) -> Self {
self.position = position;
self
}

/// Access a mutable proxy for the `Key`.
pub fn as_mut(&mut self) -> KeyMut<'_> {
KeyMut { key: self }
Expand Down Expand Up @@ -158,6 +166,16 @@ impl Key {
}
}

/// Get the position relative to other keys in parent table
pub fn position(&self) -> Option<usize> {
self.position
}

/// Set the position relative to other keys in parent table
pub fn set_position(&mut self, position: Option<usize>) {
self.position = position;
}

/// Auto formats the key.
pub fn fmt(&mut self) {
self.repr = None;
Expand Down Expand Up @@ -190,6 +208,7 @@ impl Clone for Key {
repr: self.repr.clone(),
leaf_decor: self.leaf_decor.clone(),
dotted_decor: self.dotted_decor.clone(),
position: self.position,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/toml_edit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ mod error;
mod index;
mod inline_table;
mod internal_string;
mod internal_table;
mod item;
mod key;
#[cfg(feature = "parse")]
Expand Down
4 changes: 3 additions & 1 deletion crates/toml_edit/src/parser/inline_table.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this commit type would be test

Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ fn table_from_pairs(
// Assuming almost all pairs will be directly in `root`
root.items.reserve(v.len());

for (path, (key, value)) in v {
for (position, (path, (mut key, value))) in v.into_iter().enumerate() {
key.set_position(Some(position));
let table = descend_path(&mut root, &path)?;

// "Likewise, using dotted keys to redefine tables already defined in [table] form is not allowed"
Expand Down Expand Up @@ -162,6 +163,7 @@ mod test {
r#"{a = 1e165}"#,
r#"{ hello = "world", a = 1}"#,
r#"{ hello.world = "a" }"#,
r#"{ hello.world = "a", goodbye = "b", hello.moon = "c" }"#,
];
for input in inputs {
dbg!(input);
Expand Down
Loading
Loading