Repository: jonasbb/serde_with
Pull request #33 (2019-02-16 13:44:30)

Add documentation for HashMap and BTreeMap as tuple lists
This addresses the first two wishes from #32.

@upsuper: Ping, since you originally proposed this for addition to serde.
It would be nice to know if it works for you, since I generalized your implementation.

codecov[bot]: # [Codecov](https://codecov.io/gh/jonasbb/serde_with/pull/33?src=pr&el=h1) Report
> Merging [#33](https://codecov.io/gh/jonasbb/serde_with/pull/33?src=pr&el=desc) into [master](https://codecov.io/gh/jonasbb/serde_with/commit/4319090cdcbb0da2e51a44c0c6a257901cf2a8f5?src=pr&el=desc) will **increase** coverage by `5.02%`.
> The diff coverage is `96.39%`.

[![Impacted file tree graph](https://codecov.io/gh/jonasbb/serde_with/pull/33/graphs/tree.svg?width=650&token=DoaDtevgcY&height=150&src=pr)](https://codecov.io/gh/jonasbb/serde_with/pull/33?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #33      +/-   ##
==========================================
+ Coverage   67.35%   72.38%   +5.02%     
==========================================
  Files          10       10              
  Lines         530      641     +111     
==========================================
+ Hits          357      464     +107     
- Misses        173      177       +4
```


| [Impacted Files](https://codecov.io/gh/jonasbb/serde_with/pull/33?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/rust.rs](https://codecov.io/gh/jonasbb/serde_with/pull/33/diff?src=pr&el=tree#diff-dGVzdHMvcnVzdC5ycw==) | `98.1% <100%> (+1.2%)` | :arrow_up: |
| [src/rust.rs](https://codecov.io/gh/jonasbb/serde_with/pull/33/diff?src=pr&el=tree#diff-c3JjL3J1c3QucnM=) | `54.2% <86.2%> (+11.89%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/jonasbb/serde_with/pull/33?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/jonasbb/serde_with/pull/33?src=pr&el=footer). Last update [4319090...752a5b5](https://codecov.io/gh/jonasbb/serde_with/pull/33?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index a89ddc9..c49e643 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,10 +32,12 @@ serde = "1.0.75"
 serde_json = { version = "1.0.1", optional = true }
 
 [dev-dependencies]
+fnv = "1.0.6"
+pretty_assertions = "0.5.1"
 ron = ">=0.3.0,<0.6"
 serde_derive = "1.0.75"
 serde_json = "1.0.1"
-pretty_assertions = "0.5.1"
+version-sync = "0.7.0"
 
 [package.metadata.docs.rs]
 all-features = true
diff --git a/src/rust.rs b/src/rust.rs
index 65c5d17..0dc1f5c 100644
--- a/src/rust.rs
+++ b/src/rust.rs
@@ -2,10 +2,13 @@
 
 use serde::{
     de::{Deserialize, DeserializeOwned, Deserializer, Error, MapAccess, SeqAccess, Visitor},
-    ser::{Serialize, Serializer},
+    ser::{Serialize, SerializeSeq, Serializer},
 };
 use std::{
+    cmp::Eq,
+    collections::{BTreeMap, HashMap},
     fmt::{self, Display},
+    hash::{BuildHasher, Hash},
     iter::FromIterator,
     marker::PhantomData,
     str::FromStr,
@@ -921,3 +924,228 @@ pub mod string_empty_as_none {
         }
     }
 }
+
+/// De/Serialize a [`HashMap`] into a list of tuples
+///
+/// Some formats, like JSON, have limitations on the type of keys for maps.
+/// In case of JSON, keys are restricted to strings.
+/// Rust features more powerfull keys, for example tuple, which can not be serialized to JSON.
+///
+/// This helper serializes the [`HashMap`] into a list of tuples, which does not have the same type restrictions.
+///
+/// If you need to de/serialize a [`BTreeMap`] then use [`btreemap_as_tuple_list`].
+///
+/// # Examples
+///
+/// ```
+/// # extern crate serde;
+/// # #[macro_use]
+/// # extern crate serde_derive;
+/// # #[macro_use]
+/// # extern crate serde_json;
+/// # extern crate serde_with;
+/// #
+/// # use std::collections::HashMap;
+/// #
+/// #[derive(Deserialize, Serialize)]
+/// struct A {
+///     #[serde(with = "serde_with::rust::hashmap_as_tuple_list")]
+///     s: HashMap<(String, u32), u32>,
+/// }
+///
+/// # fn main() {
+/// let v: A = serde_json::from_value(json!({
+///     "s": [
+///         [["Hello", 123], 0],
+///         [["World", 456], 1]
+///     ]
+/// })).unwrap();
+///
+/// assert_eq!(2, v.s.len());
+/// assert_eq!(1, v.s[&("World".to_string(), 456)]);
+/// # }
+/// ```
+///
+/// The helper is generic over the hasher type of the [`HashMap`] and works with different variants, such as `FnvHashMap`.
+///
+/// ```
+/// # extern crate fnv;
+/// # extern crate serde;
+/// # #[macro_use]
+/// # extern crate serde_derive;
+/// # #[macro_use]
+/// # extern crate serde_json;
+/// # extern crate serde_with;
+/// #
+/// use fnv::FnvHashMap;
+///
+/// #[derive(Deserialize, Serialize)]
+/// struct A {
+///     #[serde(with = "serde_with::rust::hashmap_as_tuple_list")]
+///     s: FnvHashMap<u32, bool>,
+/// }
+///
+/// # fn main() {
+/// let v: A = serde_json::from_value(json!({
+///     "s": [
+///         [0, false],
+///         [1, true]
+///     ]
+/// })).unwrap();
+///
+/// assert_eq!(2, v.s.len());
+/// assert_eq!(true, v.s[&1]);
+/// # }
+/// ```
+pub mod hashmap_as_tuple_list {
+    use super::SerializeSeq; // Needed to remove the unused import warning in the parent scope
+    use super::*;
+
+    /// Serialize the [`HashMap`] as a list of tuples
+    pub fn serialize<K, V, S, BH>(map: &HashMap<K, V, BH>, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+        K: Eq + Hash + Serialize,
+        V: Serialize,
+        BH: BuildHasher,
+    {
+        let mut seq = serializer.serialize_seq(Some(map.len()))?;
+        for item in map.iter() {
+            seq.serialize_element(&item)?;
+        }
+        seq.end()
+    }
+
+    /// Deserialize a [`HashMap`] from a list of tuples
+    pub fn deserialize<'de, K, V, BH, D>(deserializer: D) -> Result<HashMap<K, V, BH>, D::Error>
+    where
+        D: Deserializer<'de>,
+        K: Eq + Hash + Deserialize<'de>,
+        V: Deserialize<'de>,
+        BH: BuildHasher + Default,
+    {
+        deserializer.deserialize_seq(HashMapVisitor(PhantomData))
+    }
+
+    #[cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
+    struct HashMapVisitor<K, V, BH>(PhantomData<fn() -> HashMap<K, V, BH>>);
+
+    impl<'de, K, V, BH> Visitor<'de> for HashMapVisitor<K, V, BH>
+    where
+        K: Deserialize<'de> + Eq + Hash,
+        V: Deserialize<'de>,
+        BH: BuildHasher + Default,
+    {
+        type Value = HashMap<K, V, BH>;
+
+        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+            formatter.write_str("a list of key-value pairs")
+        }
+
+        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+        where
+            A: SeqAccess<'de>,
+        {
+            let mut map =
+                HashMap::with_capacity_and_hasher(seq.size_hint().unwrap_or(0), BH::default());
+            while let Some((key, value)) = seq.next_element()? {
+                map.insert(key, value);
+            }
+            Ok(map)
+        }
+    }
+}
+
+/// De/Serialize a [`BTreeMap`] into a list of tuples
+///
+/// Some formats, like JSON, have limitations on the type of keys for maps.
+/// In case of JSON, keys are restricted to strings.
+/// Rust features more powerfull keys, for example tuple, which can not be serialized to JSON.
+///
+/// This helper serializes the [`BTreeMap`] into a list of tuples, which does not have the same type restrictions.
+///
+/// If you need to de/serialize a [`HashMap`] then use [`hashmap_as_tuple_list`].
+///
+/// # Examples
+///
+/// ```
+/// # extern crate serde;
+/// # #[macro_use]
+/// # extern crate serde_derive;
+/// # #[macro_use]
+/// # extern crate serde_json;
+/// # extern crate serde_with;
+/// #
+/// # use std::collections::BTreeMap;
+/// #
+/// #[derive(Deserialize, Serialize)]
+/// struct A {
+///     #[serde(with = "serde_with::rust::btreemap_as_tuple_list")]
+///     s: BTreeMap<(String, u32), u32>,
+/// }
+///
+/// # fn main() {
+/// let v: A = serde_json::from_value(json!({
+///     "s": [
+///         [["Hello", 123], 0],
+///         [["World", 456], 1]
+///     ]
+/// })).unwrap();
+///
+/// assert_eq!(2, v.s.len());
+/// assert_eq!(1, v.s[&("World".to_string(), 456)]);
+/// # }
+/// ```
+pub mod btreemap_as_tuple_list {
+    use super::*;
+
+    /// Serialize the [`BTreeMap`] as a list of tuples
+    pub fn serialize<K, V, S>(map: &BTreeMap<K, V>, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+        K: Eq + Hash + Serialize,
+        V: Serialize,
+    {
+        let mut seq = serializer.serialize_seq(Some(map.len()))?;
+        for item in map.iter() {
+            seq.serialize_element(&item)?;
+        }
+        seq.end()
+    }
+
+    /// Deserialize a [`BTreeMap`] from a list of tuples
+    pub fn deserialize<'de, K, V, D>(deserializer: D) -> Result<BTreeMap<K, V>, D::Error>
+    where
+        D: Deserializer<'de>,
+        K: Deserialize<'de> + Ord,
+        V: Deserialize<'de>,
+    {
+        deserializer.deserialize_seq(BTreeMapVisitor(PhantomData))
+    }
+
+    #[cfg_attr(feature = "cargo-clippy", allow(type_complexity))]
+    struct BTreeMapVisitor<K, V>(PhantomData<fn() -> BTreeMap<K, V>>);
+
+    impl<'de, K, V> Visitor<'de> for BTreeMapVisitor<K, V>
+    where
+        K: Deserialize<'de> + Ord,
+        V: Deserialize<'de>,
+    {
+        type Value = BTreeMap<K, V>;
+
+        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+            formatter.write_str("a list of key-value pairs")
+        }
+
+        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+        where
+            A: SeqAccess<'de>,
+        {
+            let mut map = BTreeMap::default();
+            while let Some((key, value)) = seq.next_element()? {
+                map.insert(key, value);
+            }
+            Ok(map)
+        }
+    }
+}


Test patch:
diff --git a/tests/rust.rs b/tests/rust.rs
index eed22ae..2b7c5f8 100644
--- a/tests/rust.rs
+++ b/tests/rust.rs
@@ -1,3 +1,4 @@
+extern crate fnv;
 extern crate serde;
 #[macro_use]
 extern crate serde_derive;
@@ -6,7 +7,9 @@ extern crate serde_with;
 #[macro_use]
 extern crate pretty_assertions;
 
+use fnv::FnvHashMap;
 use serde_with::CommaSeparator;
+use std::collections::{BTreeMap, HashMap};
 
 #[test]
 fn string_collection() {
@@ -242,3 +245,183 @@ fn duplicate_key_first_wins_btreemap() {
     v.map.insert(2, 2);
     assert_eq!(v, serde_json::from_str(s).unwrap());
 }
+
+#[test]
+fn test_hashmap_as_tuple_list() {
+    #[derive(Debug, Deserialize, Serialize, PartialEq, Default)]
+    struct S {
+        #[serde(with = "serde_with::rust::hashmap_as_tuple_list")]
+        s: HashMap<String, u8>,
+    }
+    let from = r#"{
+        "s": [
+            ["ABC", 1],
+            ["Hello", 0],
+            ["World", 20]
+        ]
+    }"#;
+    let mut expected = S::default();
+    expected.s.insert("ABC".to_string(), 1);
+    expected.s.insert("Hello".to_string(), 0);
+    expected.s.insert("World".to_string(), 20);
+
+    let res: S = serde_json::from_str(from).unwrap();
+    assert_eq!(expected, res);
+
+    let from = r#"{
+  "s": [
+    [
+      "Hello",
+      0
+    ]
+  ]
+}"#;
+    let mut expected = S::default();
+    expected.s.insert("Hello".to_string(), 0);
+
+    let res: S = serde_json::from_str(from).unwrap();
+    assert_eq!(expected, res);
+    // We can only do this with a HashMap of size 1 as otherwise the iteration order is unspecified
+    assert_eq!(from, serde_json::to_string_pretty(&expected).unwrap());
+
+    let from = r#"{
+  "s": []
+}"#;
+    let expected = S::default();
+    let res: S = serde_json::from_str(from).unwrap();
+    assert!(res.s.is_empty());
+    assert_eq!(from, serde_json::to_string_pretty(&expected).unwrap());
+
+    // Test parse error
+    let from = r#"{
+  "s": [ [1] ]
+}"#;
+    let res: Result<S, _> = serde_json::from_str(from);
+    assert!(res.is_err());
+    let err = res.unwrap_err();
+    println!("{:?}", err);
+    assert!(err.is_data());
+}
+
+#[test]
+fn test_hashmap_as_tuple_list_fnv() {
+    #[derive(Debug, Deserialize, Serialize, PartialEq, Default)]
+    struct S {
+        #[serde(with = "serde_with::rust::hashmap_as_tuple_list")]
+        s: FnvHashMap<String, u8>,
+    }
+    let from = r#"{
+        "s": [
+            ["ABC", 1],
+            ["Hello", 0],
+            ["World", 20]
+        ]
+    }"#;
+    let mut expected = S::default();
+    expected.s.insert("ABC".to_string(), 1);
+    expected.s.insert("Hello".to_string(), 0);
+    expected.s.insert("World".to_string(), 20);
+
+    let res: S = serde_json::from_str(from).unwrap();
+    assert_eq!(expected, res);
+
+    let from = r#"{
+  "s": [
+    [
+      "Hello",
+      0
+    ]
+  ]
+}"#;
+    let mut expected = S::default();
+    expected.s.insert("Hello".to_string(), 0);
+
+    let res: S = serde_json::from_str(from).unwrap();
+    assert_eq!(expected, res);
+    // We can only do this with a HashMap of size 1 as otherwise the iteration order is unspecified
+    assert_eq!(from, serde_json::to_string_pretty(&expected).unwrap());
+
+    let from = r#"{
+  "s": []
+}"#;
+    let expected = S::default();
+    let res: S = serde_json::from_str(from).unwrap();
+    assert!(res.s.is_empty());
+    assert_eq!(from, serde_json::to_string_pretty(&expected).unwrap());
+
+    // Test parse error
+    let from = r#"{
+  "s": [ [1] ]
+}"#;
+    let res: Result<S, _> = serde_json::from_str(from);
+    assert!(res.is_err());
+    let err = res.unwrap_err();
+    println!("{:?}", err);
+    assert!(err.is_data());
+}
+
+#[test]
+fn test_btreemap_as_tuple_list() {
+    #[derive(Debug, Deserialize, Serialize, PartialEq, Default)]
+    struct S {
+        #[serde(with = "serde_with::rust::btreemap_as_tuple_list")]
+        s: BTreeMap<String, u8>,
+    }
+    let from = r#"{
+  "s": [
+    [
+      "ABC",
+      1
+    ],
+    [
+      "Hello",
+      0
+    ],
+    [
+      "World",
+      20
+    ]
+  ]
+}"#;
+    let mut expected = S::default();
+    expected.s.insert("ABC".to_string(), 1);
+    expected.s.insert("Hello".to_string(), 0);
+    expected.s.insert("World".to_string(), 20);
+
+    let res: S = serde_json::from_str(from).unwrap();
+    assert_eq!(expected, res);
+    assert_eq!(from, serde_json::to_string_pretty(&expected).unwrap());
+
+    let from = r#"{
+  "s": [
+    [
+      "Hello",
+      0
+    ]
+  ]
+}"#;
+    let mut expected = S::default();
+    expected.s.insert("Hello".to_string(), 0);
+
+    let res: S = serde_json::from_str(from).unwrap();
+    assert_eq!(expected, res);
+    assert_eq!(from, serde_json::to_string_pretty(&expected).unwrap());
+
+    let from = r#"{
+  "s": []
+}"#;
+    let expected = S::default();
+    let res: S = serde_json::from_str(from).unwrap();
+    assert!(res.s.is_empty());
+    assert_eq!(from, serde_json::to_string_pretty(&expected).unwrap());
+
+    // Test parse error
+    let from = r#"{
+  "s": [ [1] ]
+}"#;
+    let res: Result<S, _> = serde_json::from_str(from);
+    assert!(res.is_err());
+    let err = res.unwrap_err();
+    println!("{:?}", err);
+    assert!(err.is_data());
+}