From 8621ab27ae908e7270aaac932ba87ee0bbb7573e Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 14 May 2018 16:59:24 +0200 Subject: [PATCH 1/3] Add the preserve_order feature --- .travis.yml | 2 + Cargo.toml | 11 +- src/lib.rs | 3 + src/map.rs | 595 ++++++++++++++++++++++++++++++++++++ src/value.rs | 21 +- test-suite/tests/display.rs | 5 +- test-suite/tests/serde.rs | 5 +- 7 files changed, 628 insertions(+), 14 deletions(-) create mode 100644 src/map.rs diff --git a/.travis.yml b/.travis.yml index 0715ef1..dc09706 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,9 @@ before_script: - pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH script: - cargo test + - cargo test --features preserve_order - cargo test --manifest-path test-suite/Cargo.toml + - cargo test --manifest-path test-suite/Cargo.toml --features preserve_order - rustdoc --test README.md -L target - test "$TRAVIS_RUST_VERSION" != "1.15.0" && cargo doc --no-deps || echo "skipping cargo doc" after_success: diff --git a/Cargo.toml b/Cargo.toml index 9904e2f..39dae2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toml" -version = "0.4.10" +version = "0.5.0" authors = ["Alex Crichton "] license = "MIT/Apache-2.0" readme = "README.md" @@ -20,7 +20,16 @@ travis-ci = { repository = "alexcrichton/toml-rs" } [dependencies] serde = "1.0" +linked-hash-map = { version = "0.5", optional = true } [dev-dependencies] serde_derive = "1.0" serde_json = "1.0" + +[features] +default = [] + +# Use LinkedHashMap rather than BTreeMap as the map type of toml::Value. +# This allows data to be read into a Value and written back to a TOML string +# while preserving the order of map keys in the input. +preserve_order = ["linked-hash-map"] diff --git a/src/lib.rs b/src/lib.rs index e36ff52..4a32b13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -153,7 +153,10 @@ #[macro_use] extern crate serde; +#[cfg(feature = "preserve_order")] +extern crate linked_hash_map; +pub mod map; pub mod value; #[doc(no_inline)] pub use value::Value; diff --git a/src/map.rs b/src/map.rs new file mode 100644 index 0000000..59ae24e --- /dev/null +++ b/src/map.rs @@ -0,0 +1,595 @@ +// Copyright 2017 Serde Developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! A map of String to toml::Value. +//! +//! By default the map is backed by a [`BTreeMap`]. Enable the `preserve_order` +//! feature of toml-rs to use [`LinkedHashMap`] instead. +//! +//! [`BTreeMap`]: https://doc.rust-lang.org/std/collections/struct.BTreeMap.html +//! [`LinkedHashMap`]: https://docs.rs/linked-hash-map/*/linked_hash_map/struct.LinkedHashMap.html + +use serde::{de, ser}; +use std::fmt::{self, Debug}; +use value::Value; +use std::hash::Hash; +use std::iter::FromIterator; +use std::borrow::Borrow; +use std::ops; + +#[cfg(not(feature = "preserve_order"))] +use std::collections::{btree_map, BTreeMap}; + +#[cfg(feature = "preserve_order")] +use linked_hash_map::{self, LinkedHashMap}; + +/// Represents a JSON key/value type. +pub struct Map { + map: MapImpl, +} + +#[cfg(not(feature = "preserve_order"))] +type MapImpl = BTreeMap; +#[cfg(feature = "preserve_order")] +type MapImpl = LinkedHashMap; + +impl Map { + /// Makes a new empty Map. + #[inline] + pub fn new() -> Self { + Map { + map: MapImpl::new(), + } + } + + #[cfg(not(feature = "preserve_order"))] + /// Makes a new empty Map with the given initial capacity. + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + // does not support with_capacity + let _ = capacity; + Map { + map: BTreeMap::new(), + } + } + + #[cfg(feature = "preserve_order")] + /// Makes a new empty Map with the given initial capacity. + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + Map { + map: LinkedHashMap::with_capacity(capacity), + } + } + + /// Clears the map, removing all values. + #[inline] + pub fn clear(&mut self) { + self.map.clear() + } + + /// Returns a reference to the value corresponding to the key. + /// + /// The key may be any borrowed form of the map's key type, but the ordering + /// on the borrowed form *must* match the ordering on the key type. + #[inline] + pub fn get(&self, key: &Q) -> Option<&Value> + where + String: Borrow, + Q: Ord + Eq + Hash, + { + self.map.get(key) + } + + /// Returns true if the map contains a value for the specified key. + /// + /// The key may be any borrowed form of the map's key type, but the ordering + /// on the borrowed form *must* match the ordering on the key type. + #[inline] + pub fn contains_key(&self, key: &Q) -> bool + where + String: Borrow, + Q: Ord + Eq + Hash, + { + self.map.contains_key(key) + } + + /// Returns a mutable reference to the value corresponding to the key. + /// + /// The key may be any borrowed form of the map's key type, but the ordering + /// on the borrowed form *must* match the ordering on the key type. + #[inline] + pub fn get_mut(&mut self, key: &Q) -> Option<&mut Value> + where + String: Borrow, + Q: Ord + Eq + Hash, + { + self.map.get_mut(key) + } + + /// Inserts a key-value pair into the map. + /// + /// If the map did not have this key present, `None` is returned. + /// + /// If the map did have this key present, the value is updated, and the old + /// value is returned. The key is not updated, though; this matters for + /// types that can be `==` without being identical. + #[inline] + pub fn insert(&mut self, k: String, v: Value) -> Option { + self.map.insert(k, v) + } + + /// Removes a key from the map, returning the value at the key if the key + /// was previously in the map. + /// + /// The key may be any borrowed form of the map's key type, but the ordering + /// on the borrowed form *must* match the ordering on the key type. + #[inline] + pub fn remove(&mut self, key: &Q) -> Option + where + String: Borrow, + Q: Ord + Eq + Hash, + { + self.map.remove(key) + } + + /// Gets the given key's corresponding entry in the map for in-place + /// manipulation. + pub fn entry(&mut self, key: S) -> Entry + where + S: Into, + { + #[cfg(not(feature = "preserve_order"))] + use std::collections::btree_map::Entry as EntryImpl; + #[cfg(feature = "preserve_order")] + use linked_hash_map::Entry as EntryImpl; + + match self.map.entry(key.into()) { + EntryImpl::Vacant(vacant) => Entry::Vacant(VacantEntry { vacant: vacant }), + EntryImpl::Occupied(occupied) => Entry::Occupied(OccupiedEntry { occupied: occupied }), + } + } + + /// Returns the number of elements in the map. + #[inline] + pub fn len(&self) -> usize { + self.map.len() + } + + /// Returns true if the map contains no elements. + #[inline] + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + + /// Gets an iterator over the entries of the map. + #[inline] + pub fn iter(&self) -> Iter { + Iter { + iter: self.map.iter(), + } + } + + /// Gets a mutable iterator over the entries of the map. + #[inline] + pub fn iter_mut(&mut self) -> IterMut { + IterMut { + iter: self.map.iter_mut(), + } + } + + /// Gets an iterator over the keys of the map. + #[inline] + pub fn keys(&self) -> Keys { + Keys { + iter: self.map.keys(), + } + } + + /// Gets an iterator over the values of the map. + #[inline] + pub fn values(&self) -> Values { + Values { + iter: self.map.values(), + } + } +} + +impl Default for Map { + #[inline] + fn default() -> Self { + Map { + map: MapImpl::new(), + } + } +} + +impl Clone for Map { + #[inline] + fn clone(&self) -> Self { + Map { + map: self.map.clone(), + } + } +} + +impl PartialEq for Map { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.map.eq(&other.map) + } +} + +/// Access an element of this map. Panics if the given key is not present in the +/// map. +impl<'a, Q: ?Sized> ops::Index<&'a Q> for Map +where + String: Borrow, + Q: Ord + Eq + Hash, +{ + type Output = Value; + + fn index(&self, index: &Q) -> &Value { + self.map.index(index) + } +} + +/// Mutably access an element of this map. Panics if the given key is not +/// present in the map. +impl<'a, Q: ?Sized> ops::IndexMut<&'a Q> for Map +where + String: Borrow, + Q: Ord + Eq + Hash, +{ + fn index_mut(&mut self, index: &Q) -> &mut Value { + self.map.get_mut(index).expect("no entry found for key") + } +} + +impl Debug for Map { + #[inline] + fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> { + self.map.fmt(formatter) + } +} + +impl ser::Serialize for Map { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + use serde::ser::SerializeMap; + let mut map = try!(serializer.serialize_map(Some(self.len()))); + for (k, v) in self { + try!(map.serialize_key(k)); + try!(map.serialize_value(v)); + } + map.end() + } +} + +impl<'de> de::Deserialize<'de> for Map { + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = Map; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } + + #[inline] + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(Map::new()) + } + + #[inline] + fn visit_map(self, mut visitor: V) -> Result + where + V: de::MapAccess<'de>, + { + let mut values = Map::new(); + + while let Some((key, value)) = try!(visitor.next_entry()) { + values.insert(key, value); + } + + Ok(values) + } + } + + deserializer.deserialize_map(Visitor) + } +} + +impl FromIterator<(String, Value)> for Map { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + Map { + map: FromIterator::from_iter(iter), + } + } +} + +impl Extend<(String, Value)> for Map { + fn extend(&mut self, iter: T) + where + T: IntoIterator, + { + self.map.extend(iter); + } +} + +macro_rules! delegate_iterator { + (($name:ident $($generics:tt)*) => $item:ty) => { + impl $($generics)* Iterator for $name $($generics)* { + type Item = $item; + #[inline] + fn next(&mut self) -> Option { + self.iter.next() + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } + } + + impl $($generics)* DoubleEndedIterator for $name $($generics)* { + #[inline] + fn next_back(&mut self) -> Option { + self.iter.next_back() + } + } + + impl $($generics)* ExactSizeIterator for $name $($generics)* { + #[inline] + fn len(&self) -> usize { + self.iter.len() + } + } + } +} + +////////////////////////////////////////////////////////////////////////////// + +/// A view into a single entry in a map, which may either be vacant or occupied. +/// This enum is constructed from the [`entry`] method on [`Map`]. +/// +/// [`entry`]: struct.Map.html#method.entry +/// [`Map`]: struct.Map.html +pub enum Entry<'a> { + /// A vacant Entry. + Vacant(VacantEntry<'a>), + /// An occupied Entry. + Occupied(OccupiedEntry<'a>), +} + +/// A vacant Entry. It is part of the [`Entry`] enum. +/// +/// [`Entry`]: enum.Entry.html +pub struct VacantEntry<'a> { + vacant: VacantEntryImpl<'a>, +} + +/// An occupied Entry. It is part of the [`Entry`] enum. +/// +/// [`Entry`]: enum.Entry.html +pub struct OccupiedEntry<'a> { + occupied: OccupiedEntryImpl<'a>, +} + +#[cfg(not(feature = "preserve_order"))] +type VacantEntryImpl<'a> = btree_map::VacantEntry<'a, String, Value>; +#[cfg(feature = "preserve_order")] +type VacantEntryImpl<'a> = linked_hash_map::VacantEntry<'a, String, Value>; + +#[cfg(not(feature = "preserve_order"))] +type OccupiedEntryImpl<'a> = btree_map::OccupiedEntry<'a, String, Value>; +#[cfg(feature = "preserve_order")] +type OccupiedEntryImpl<'a> = linked_hash_map::OccupiedEntry<'a, String, Value>; + +impl<'a> Entry<'a> { + /// Returns a reference to this entry's key. + pub fn key(&self) -> &String { + match *self { + Entry::Vacant(ref e) => e.key(), + Entry::Occupied(ref 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 { + Entry::Vacant(entry) => entry.insert(default), + Entry::Occupied(entry) => entry.into_mut(), + } + } + + /// 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(self, default: F) -> &'a mut Value + where + F: FnOnce() -> Value, + { + match self { + Entry::Vacant(entry) => entry.insert(default()), + Entry::Occupied(entry) => entry.into_mut(), + } + } +} + +impl<'a> VacantEntry<'a> { + /// Gets a reference to the key that would be used when inserting a value + /// through the VacantEntry. + #[inline] + pub fn key(&self) -> &String { + self.vacant.key() + } + + /// Sets the value of the entry with the VacantEntry's key, and returns a + /// mutable reference to it. + #[inline] + pub fn insert(self, value: Value) -> &'a mut Value { + self.vacant.insert(value) + } +} + +impl<'a> OccupiedEntry<'a> { + /// Gets a reference to the key in the entry. + #[inline] + pub fn key(&self) -> &String { + self.occupied.key() + } + + /// Gets a reference to the value in the entry. + #[inline] + pub fn get(&self) -> &Value { + self.occupied.get() + } + + /// Gets a mutable reference to the value in the entry. + #[inline] + pub fn get_mut(&mut self) -> &mut Value { + self.occupied.get_mut() + } + + /// Converts the entry into a mutable reference to its value. + #[inline] + pub fn into_mut(self) -> &'a mut Value { + self.occupied.into_mut() + } + + /// Sets the value of the entry with the `OccupiedEntry`'s key, and returns + /// the entry's old value. + #[inline] + pub fn insert(&mut self, value: Value) -> Value { + self.occupied.insert(value) + } + + /// Takes the value of the entry out of the map, and returns it. + #[inline] + pub fn remove(self) -> Value { + self.occupied.remove() + } +} + +////////////////////////////////////////////////////////////////////////////// + +impl<'a> IntoIterator for &'a Map { + type Item = (&'a String, &'a Value); + type IntoIter = Iter<'a>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + Iter { + iter: self.map.iter(), + } + } +} + +/// An iterator over a toml::Map's entries. +pub struct Iter<'a> { + iter: IterImpl<'a>, +} + +#[cfg(not(feature = "preserve_order"))] +type IterImpl<'a> = btree_map::Iter<'a, String, Value>; +#[cfg(feature = "preserve_order")] +type IterImpl<'a> = linked_hash_map::Iter<'a, String, Value>; + +delegate_iterator!((Iter<'a>) => (&'a String, &'a Value)); + +////////////////////////////////////////////////////////////////////////////// + +impl<'a> IntoIterator for &'a mut Map { + type Item = (&'a String, &'a mut Value); + type IntoIter = IterMut<'a>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + IterMut { + iter: self.map.iter_mut(), + } + } +} + +/// A mutable iterator over a toml::Map's entries. +pub struct IterMut<'a> { + iter: IterMutImpl<'a>, +} + +#[cfg(not(feature = "preserve_order"))] +type IterMutImpl<'a> = btree_map::IterMut<'a, String, Value>; +#[cfg(feature = "preserve_order")] +type IterMutImpl<'a> = linked_hash_map::IterMut<'a, String, Value>; + +delegate_iterator!((IterMut<'a>) => (&'a String, &'a mut Value)); + +////////////////////////////////////////////////////////////////////////////// + +impl IntoIterator for Map { + type Item = (String, Value); + type IntoIter = IntoIter; + #[inline] + fn into_iter(self) -> Self::IntoIter { + IntoIter { + iter: self.map.into_iter(), + } + } +} + +/// An owning iterator over a toml::Map's entries. +pub struct IntoIter { + iter: IntoIterImpl, +} + +#[cfg(not(feature = "preserve_order"))] +type IntoIterImpl = btree_map::IntoIter; +#[cfg(feature = "preserve_order")] +type IntoIterImpl = linked_hash_map::IntoIter; + +delegate_iterator!((IntoIter) => (String, Value)); + +////////////////////////////////////////////////////////////////////////////// + +/// An iterator over a toml::Map's keys. +pub struct Keys<'a> { + iter: KeysImpl<'a>, +} + +#[cfg(not(feature = "preserve_order"))] +type KeysImpl<'a> = btree_map::Keys<'a, String, Value>; +#[cfg(feature = "preserve_order")] +type KeysImpl<'a> = linked_hash_map::Keys<'a, String, Value>; + +delegate_iterator!((Keys<'a>) => &'a String); + +////////////////////////////////////////////////////////////////////////////// + +/// An iterator over a toml::Map's values. +pub struct Values<'a> { + iter: ValuesImpl<'a>, +} + +#[cfg(not(feature = "preserve_order"))] +type ValuesImpl<'a> = btree_map::Values<'a, String, Value>; +#[cfg(feature = "preserve_order")] +type ValuesImpl<'a> = linked_hash_map::Values<'a, String, Value>; + +delegate_iterator!((Values<'a>) => &'a Value); diff --git a/src/value.rs b/src/value.rs index ee952b6..07378cf 100644 --- a/src/value.rs +++ b/src/value.rs @@ -14,6 +14,9 @@ use serde::ser; use datetime::{self, DatetimeFromString}; pub use datetime::{Datetime, DatetimeParseError}; +pub use map::Map; + + /// Representation of a TOML value. #[derive(PartialEq, Clone, Debug)] pub enum Value { @@ -36,8 +39,10 @@ pub enum Value { /// Type representing a TOML array, payload of the `Value::Array` variant pub type Array = Vec; -/// Type representing a TOML table, payload of the `Value::Table` variant -pub type Table = BTreeMap; +/// Type representing a TOML table, payload of the `Value::Table` variant. +/// By default it is backed by a BTreeMap, enable the `preserve_order` feature +/// to use a LinkedHashMap instead. +pub type Table = Map; impl Value { /// Convert a `T` into `toml::Value` which is an enum that can represent @@ -525,10 +530,10 @@ impl<'de> de::Deserialize<'de> for Value { let date: DatetimeFromString = visitor.next_value()?; return Ok(Value::Datetime(date.value)); } - None => return Ok(Value::Table(BTreeMap::new())), + None => return Ok(Value::Table(Map::new())), Some(false) => {} } - let mut map = BTreeMap::new(); + let mut map = Map::new(); map.insert(key, visitor.next_value()?); while let Some(key) = visitor.next_key()? { if map.contains_key(&key) { @@ -663,12 +668,12 @@ impl<'de> de::SeqAccess<'de> for SeqDeserializer { } struct MapDeserializer { - iter: as IntoIterator>::IntoIter, + iter: as IntoIterator>::IntoIter, value: Option<(String, Value)>, } impl MapDeserializer { - fn new(map: BTreeMap) -> Self { + fn new(map: Map) -> Self { MapDeserializer { iter: map.into_iter(), value: None, @@ -881,7 +886,7 @@ impl ser::Serializer for Serializer { fn serialize_map(self, _len: Option) -> Result { Ok(SerializeMap { - map: BTreeMap::new(), + map: Map::new(), next_key: None, }) } @@ -910,7 +915,7 @@ struct SerializeVec { } struct SerializeMap { - map: BTreeMap, + map: Map, next_key: Option, } diff --git a/test-suite/tests/display.rs b/test-suite/tests/display.rs index ca4fdd8..0174de1 100644 --- a/test-suite/tests/display.rs +++ b/test-suite/tests/display.rs @@ -1,11 +1,10 @@ extern crate toml; -use std::collections::BTreeMap; - use toml::Value::{String, Integer, Float, Boolean, Array, Table}; +use toml::map::Map; macro_rules! map( ($($k:expr => $v:expr),*) => ({ - let mut _m = BTreeMap::new(); + let mut _m = Map::new(); $(_m.insert($k.to_string(), $v);)* _m }) ); diff --git a/test-suite/tests/serde.rs b/test-suite/tests/serde.rs index c1b6d78..6bcd145 100644 --- a/test-suite/tests/serde.rs +++ b/test-suite/tests/serde.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Deserializer}; use toml::Value; use toml::Value::{Table, Integer, Array, Float}; +use toml::map::Map; macro_rules! t { ($e:expr) => (match $e { @@ -63,7 +64,7 @@ macro_rules! error { } macro_rules! map( ($($k:ident: $v:expr),*) => ({ - let mut _m = BTreeMap::new(); + let mut _m = Map::new(); $(_m.insert(stringify!($k).to_string(), $v);)* _m }) ); @@ -97,7 +98,7 @@ fn smoke_hyphen() { a_b: isize, } - let mut m = BTreeMap::new(); + let mut m = Map::new(); m.insert("a-b".to_string(), Integer(2)); equivalent! { Foo2 { a_b: 2 }, From 8b1cff0fce4b13c713876c0bcaea433c48bbc6f4 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 14 May 2018 17:36:27 +0200 Subject: [PATCH 2/3] test-suite is not tested with preserve_order --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dc09706..88ce22f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ script: - cargo test - cargo test --features preserve_order - cargo test --manifest-path test-suite/Cargo.toml - - cargo test --manifest-path test-suite/Cargo.toml --features preserve_order - rustdoc --test README.md -L target - test "$TRAVIS_RUST_VERSION" != "1.15.0" && cargo doc --no-deps || echo "skipping cargo doc" after_success: From b66f6108d32ebf6ec0bfb649265fe780fb7a241d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renault?= Date: Fri, 28 Dec 2018 15:39:48 +0100 Subject: [PATCH 3/3] Update the serde test-suite map! macro --- test-suite/tests/serde.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-suite/tests/serde.rs b/test-suite/tests/serde.rs index 6bcd145..9bbf7e0 100644 --- a/test-suite/tests/serde.rs +++ b/test-suite/tests/serde.rs @@ -65,7 +65,7 @@ macro_rules! error { macro_rules! map( ($($k:ident: $v:expr),*) => ({ let mut _m = Map::new(); - $(_m.insert(stringify!($k).to_string(), $v);)* + $(_m.insert(stringify!($k).to_string(), t!(Value::try_from($v)));)* _m }) );