Repository: jonasbb/serde_with (rust)

Issue:
`indexmap` v2 support
There is a v2 release of `indexmap`: https://github.com/bluss/indexmap/pull/267

Support for it can look mostly identical as the current v1 support. It would be behind a new `indexmap_2` feature.
One noticeable change is the increased MSRV to 1.64.

Pull request:
Add trait implementations for indexmap v2
Introduces a new feature flag `indexmap_2` that enables the support.
Tests are copied from the v1 support.

Closes #621

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 143d441..77660b6 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -61,7 +61,7 @@ jobs:
         # It is good to test more than the MSRV and stable since sometimes
         # breakage occurs in intermediate versions.
         # IMPORTANT: Synchronize the MSRV with the Cargo.toml values.
-        rust: ["1.61", "1.65", "1.70", "stable", "beta", "nightly"]
+        rust: ["1.64", "1.65", "1.70", "stable", "beta", "nightly"]
     runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v3
diff --git a/Cargo.lock b/Cargo.lock
index b919c83..a8c7d59 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -214,6 +214,12 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
 [[package]]
 name = "expect-test"
 version = "1.4.0"
@@ -251,6 +257,12 @@ version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -304,7 +316,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
 dependencies = [
  "autocfg",
- "hashbrown",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.0",
  "serde",
 ]
 
@@ -559,7 +582,7 @@ version = "1.0.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
 dependencies = [
- "indexmap",
+ "indexmap 1.9.2",
  "itoa",
  "ryu",
  "serde",
@@ -585,7 +608,8 @@ dependencies = [
  "fnv",
  "glob",
  "hex",
- "indexmap",
+ "indexmap 1.9.2",
+ "indexmap 2.0.0",
  "mime",
  "pretty_assertions",
  "regex",
@@ -636,7 +660,7 @@ version = "0.9.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567"
 dependencies = [
- "indexmap",
+ "indexmap 1.9.2",
  "itoa",
  "ryu",
  "serde",
diff --git a/serde_with/CHANGELOG.md b/serde_with/CHANGELOG.md
index 9eb00ac..b68ac9e 100644
--- a/serde_with/CHANGELOG.md
+++ b/serde_with/CHANGELOG.md
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 
 ## [Unreleased]
 
+### Added
+
+* Add optional support for indexmap v2 (#621)
+    Support for v1 is already available using the `indexmap_1` feature.
+    This adds identical support for v2 of indexmap using the `indexmap_2` feature.
+
+### Changed
+
+* Bump MSRV to 1.64, since that is required for the indexmap v2 dependency.
+
 ## [3.1.0] - 2023-07-17
 
 ### Added
diff --git a/serde_with/Cargo.toml b/serde_with/Cargo.toml
index 762e97d..49e1b99 100644
--- a/serde_with/Cargo.toml
+++ b/serde_with/Cargo.toml
@@ -5,7 +5,7 @@ authors = [
     "Marcin Kaźmierczak",
 ]
 name = "serde_with"
-rust-version = "1.61"
+rust-version = "1.64"
 version = "3.1.0"
 
 categories = ["encoding", "no-std", "no-std::no-alloc"]
@@ -43,6 +43,7 @@ std = [
     "chrono_0_4?/clock",
     "chrono_0_4?/std",
     "indexmap_1?/std",
+    "indexmap_2?/std",
     "time_0_3?/serde-well-known",
     "time_0_3?/std",
 ]
@@ -54,6 +55,7 @@ guide = ["dep:doc-comment", "macros", "std"]
 hex = ["dep:hex", "alloc"]
 indexmap = ["indexmap_1"]
 indexmap_1 = ["dep:indexmap_1", "alloc"]
+indexmap_2 = ["dep:indexmap_2", "alloc"]
 json = ["dep:serde_json", "alloc"]
 macros = ["dep:serde_with_macros"]
 time_0_3 = ["dep:time_0_3"]
@@ -65,6 +67,7 @@ chrono_0_4 = {package = "chrono", version = "0.4.20", optional = true, default-f
 doc-comment = {version = "0.3.3", optional = true}
 hex = {version = "0.4.3", optional = true, default-features = false}
 indexmap_1 = {package = "indexmap", version = "1.8", optional = true, default-features = false, features = ["serde-1"]}
+indexmap_2 = {package = "indexmap", version = "2.0", optional = true, default-features = false, features = ["serde"]}
 # The derive feature is needed for the flattened_maybe macro.
 # https://github.com/jonasbb/serde_with/blob/eb1965a74a3be073ecd13ec05f02a01bc1c44309/serde_with/src/flatten_maybe.rs#L67
 serde = {version = "1.0.157", default-features = false, features = ["derive"] }
@@ -113,6 +116,11 @@ name = "indexmap_1"
 path = "tests/indexmap_1.rs"
 required-features = ["indexmap_1", "macros"]
 
+[[test]]
+name = "indexmap_2"
+path = "tests/indexmap_2.rs"
+required-features = ["indexmap_2", "macros"]
+
 [[test]]
 name = "json"
 path = "tests/json.rs"
diff --git a/serde_with/src/de/duplicates.rs b/serde_with/src/de/duplicates.rs
index a03dfba..246b997 100644
--- a/serde_with/src/de/duplicates.rs
+++ b/serde_with/src/de/duplicates.rs
@@ -9,6 +9,8 @@ use crate::{
 };
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
+#[cfg(feature = "indexmap_2")]
+use indexmap_2::{IndexMap as IndexMap2, IndexSet as IndexSet2};
 
 struct SetPreventDuplicatesVisitor<SET, T, TAs>(PhantomData<(SET, T, TAs)>);
 
diff --git a/serde_with/src/de/impls.rs b/serde_with/src/de/impls.rs
index 7ea590f..2886737 100644
--- a/serde_with/src/de/impls.rs
+++ b/serde_with/src/de/impls.rs
@@ -1,6 +1,8 @@
 use crate::{formats::*, prelude::*};
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
+#[cfg(feature = "indexmap_2")]
+use indexmap_2::{IndexMap as IndexMap2, IndexSet as IndexSet2};
 
 ///////////////////////////////////////////////////////////////////////////////
 // Helper macro used internally
@@ -16,6 +18,8 @@ macro_rules! foreach_map {
         $m!(HashMap<K: Eq + Hash, V, H: Sized>);
         #[cfg(all(feature = "std", feature = "indexmap_1"))]
         $m!(IndexMap<K: Eq + Hash, V, H: Sized>);
+        #[cfg(all(feature = "std", feature = "indexmap_2"))]
+        $m!(IndexMap2<K: Eq + Hash, V, H: Sized>);
     };
 }
 pub(crate) use foreach_map;
@@ -34,6 +38,11 @@ macro_rules! foreach_map_create {
             IndexMap<K: Eq + Hash, V, S: BuildHasher + Default>,
             (|size| IndexMap::with_capacity_and_hasher(size, Default::default()))
         );
+        #[cfg(feature = "indexmap_2")]
+        $m!(
+            IndexMap2<K: Eq + Hash, V, S: BuildHasher + Default>,
+            (|size| IndexMap2::with_capacity_and_hasher(size, Default::default()))
+        );
     };
 }
 pub(crate) use foreach_map_create;
@@ -46,6 +55,8 @@ macro_rules! foreach_set {
         $m!(HashSet<(K, V): Eq + Hash>);
         #[cfg(all(feature = "std", feature = "indexmap_1"))]
         $m!(IndexSet<(K, V): Eq + Hash>);
+        #[cfg(all(feature = "std", feature = "indexmap_2"))]
+        $m!(IndexSet2<(K, V): Eq + Hash>);
     }
 }
 pub(crate) use foreach_set;
@@ -66,6 +77,12 @@ macro_rules! foreach_set_create {
             (|size| IndexSet::with_capacity_and_hasher(size, S::default())),
             insert
         );
+        #[cfg(feature = "indexmap_2")]
+        $m!(
+            IndexSet2<T: Eq + Hash, S: BuildHasher + Default>,
+            (|size| IndexSet2::with_capacity_and_hasher(size, S::default())),
+            insert
+        );
     };
 }
 pub(crate) use foreach_set_create;
diff --git a/serde_with/src/duplicate_key_impls/error_on_duplicate.rs b/serde_with/src/duplicate_key_impls/error_on_duplicate.rs
index 0c482c5..b5d1c70 100644
--- a/serde_with/src/duplicate_key_impls/error_on_duplicate.rs
+++ b/serde_with/src/duplicate_key_impls/error_on_duplicate.rs
@@ -1,6 +1,4 @@
 use crate::prelude::*;
-#[cfg(feature = "indexmap_1")]
-use indexmap_1::{IndexMap, IndexSet};
 
 pub trait PreventDuplicateInsertsSet<T> {
     fn new(size_hint: Option<usize>) -> Self;
@@ -37,7 +35,27 @@ where
 }
 
 #[cfg(feature = "indexmap_1")]
-impl<T, S> PreventDuplicateInsertsSet<T> for IndexSet<T, S>
+impl<T, S> PreventDuplicateInsertsSet<T> for indexmap_1::IndexSet<T, S>
+where
+    T: Eq + Hash,
+    S: BuildHasher + Default,
+{
+    #[inline]
+    fn new(size_hint: Option<usize>) -> Self {
+        match size_hint {
+            Some(size) => Self::with_capacity_and_hasher(size, S::default()),
+            None => Self::with_hasher(S::default()),
+        }
+    }
+
+    #[inline]
+    fn insert(&mut self, value: T) -> bool {
+        self.insert(value)
+    }
+}
+
+#[cfg(feature = "indexmap_2")]
+impl<T, S> PreventDuplicateInsertsSet<T> for indexmap_2::IndexSet<T, S>
 where
     T: Eq + Hash,
     S: BuildHasher + Default,
@@ -92,7 +110,27 @@ where
 }
 
 #[cfg(feature = "indexmap_1")]
-impl<K, V, S> PreventDuplicateInsertsMap<K, V> for IndexMap<K, V, S>
+impl<K, V, S> PreventDuplicateInsertsMap<K, V> for indexmap_1::IndexMap<K, V, S>
+where
+    K: Eq + Hash,
+    S: BuildHasher + Default,
+{
+    #[inline]
+    fn new(size_hint: Option<usize>) -> Self {
+        match size_hint {
+            Some(size) => Self::with_capacity_and_hasher(size, S::default()),
+            None => Self::with_hasher(S::default()),
+        }
+    }
+
+    #[inline]
+    fn insert(&mut self, key: K, value: V) -> bool {
+        self.insert(key, value).is_none()
+    }
+}
+
+#[cfg(feature = "indexmap_2")]
+impl<K, V, S> PreventDuplicateInsertsMap<K, V> for indexmap_2::IndexMap<K, V, S>
 where
     K: Eq + Hash,
     S: BuildHasher + Default,
diff --git a/serde_with/src/duplicate_key_impls/first_value_wins.rs b/serde_with/src/duplicate_key_impls/first_value_wins.rs
index 5895eb6..86c4cef 100644
--- a/serde_with/src/duplicate_key_impls/first_value_wins.rs
+++ b/serde_with/src/duplicate_key_impls/first_value_wins.rs
@@ -1,6 +1,4 @@
 use crate::prelude::*;
-#[cfg(feature = "indexmap_1")]
-use indexmap_1::IndexMap;
 
 pub trait DuplicateInsertsFirstWinsMap<K, V> {
     fn new(size_hint: Option<usize>) -> Self;
@@ -38,7 +36,7 @@ where
 }
 
 #[cfg(feature = "indexmap_1")]
-impl<K, V, S> DuplicateInsertsFirstWinsMap<K, V> for IndexMap<K, V, S>
+impl<K, V, S> DuplicateInsertsFirstWinsMap<K, V> for indexmap_1::IndexMap<K, V, S>
 where
     K: Eq + Hash,
     S: BuildHasher + Default,
@@ -65,6 +63,34 @@ where
     }
 }
 
+#[cfg(feature = "indexmap_2")]
+impl<K, V, S> DuplicateInsertsFirstWinsMap<K, V> for indexmap_2::IndexMap<K, V, S>
+where
+    K: Eq + Hash,
+    S: BuildHasher + Default,
+{
+    #[inline]
+    fn new(size_hint: Option<usize>) -> Self {
+        match size_hint {
+            Some(size) => Self::with_capacity_and_hasher(size, S::default()),
+            None => Self::with_hasher(S::default()),
+        }
+    }
+
+    #[inline]
+    fn insert(&mut self, key: K, value: V) {
+        use indexmap_2::map::Entry;
+
+        match self.entry(key) {
+            // we want to keep the first value, so do nothing
+            Entry::Occupied(_) => {}
+            Entry::Vacant(vacant) => {
+                vacant.insert(value);
+            }
+        }
+    }
+}
+
 impl<K, V> DuplicateInsertsFirstWinsMap<K, V> for BTreeMap<K, V>
 where
     K: Ord,
diff --git a/serde_with/src/duplicate_key_impls/last_value_wins.rs b/serde_with/src/duplicate_key_impls/last_value_wins.rs
index 68b3fd7..03f6c0f 100644
--- a/serde_with/src/duplicate_key_impls/last_value_wins.rs
+++ b/serde_with/src/duplicate_key_impls/last_value_wins.rs
@@ -1,6 +1,4 @@
 use crate::prelude::*;
-#[cfg(feature = "indexmap_1")]
-use indexmap_1::IndexSet;
 
 pub trait DuplicateInsertsLastWinsSet<T> {
     fn new(size_hint: Option<usize>) -> Self;
@@ -31,7 +29,28 @@ where
 }
 
 #[cfg(feature = "indexmap_1")]
-impl<T, S> DuplicateInsertsLastWinsSet<T> for IndexSet<T, S>
+impl<T, S> DuplicateInsertsLastWinsSet<T> for indexmap_1::IndexSet<T, S>
+where
+    T: Eq + Hash,
+    S: BuildHasher + Default,
+{
+    #[inline]
+    fn new(size_hint: Option<usize>) -> Self {
+        match size_hint {
+            Some(size) => Self::with_capacity_and_hasher(size, S::default()),
+            None => Self::with_hasher(S::default()),
+        }
+    }
+
+    #[inline]
+    fn replace(&mut self, value: T) {
+        // Hashset already fulfils the contract
+        self.replace(value);
+    }
+}
+
+#[cfg(feature = "indexmap_2")]
+impl<T, S> DuplicateInsertsLastWinsSet<T> for indexmap_2::IndexSet<T, S>
 where
     T: Eq + Hash,
     S: BuildHasher + Default,
diff --git a/serde_with/src/guide/feature_flags.md b/serde_with/src/guide/feature_flags.md
index 39b6d4e..54d79c6 100644
--- a/serde_with/src/guide/feature_flags.md
+++ b/serde_with/src/guide/feature_flags.md
@@ -13,9 +13,10 @@ Using it in a `no_std` environment requires using `default-features = false`, si
 5. [`guide`](#guide)
 6. [`hex`](#hex)
 7. [`indexmap_1`](#indexmap_1)
-8. [`json`](#json)
-9. [`macros` (default)](#macros-default)
-10. [`time_0_3`](#time_0_3)
+8. [`indexmap_2`](#indexmap_2)
+9. [`json`](#json)
+10. [`macros` (default)](#macros-default)
+11. [`time_0_3`](#time_0_3)
 
 ## `alloc`
 
@@ -57,11 +58,20 @@ It enables the `alloc` feature.
 
 ## `indexmap_1`
 
-The `indexmap_1` feature enables implementations of `indexmap_1` specific checks.
+The `indexmap_1` feature enables implementations of `indexmap` v1 specific checks.
 This includes support for checking duplicate keys and duplicate values.
 The legacy feature name `indexmap` is still available for v1 compatability.
 
-This pulls in `indexmap` as a dependency.
+This pulls in `indexmap` v1 as a dependency.
+It enables the `alloc` feature.
+Some functionality is only available when `std` is enabled too.
+
+## `indexmap_2`
+
+The `indexmap_2` feature enables implementations of `indexmap` v2 specific checks.
+This includes support for checking duplicate keys and duplicate values.
+
+This pulls in `indexmap` v2 as a dependency.
 It enables the `alloc` feature.
 Some functionality is only available when `std` is enabled too.
 
diff --git a/serde_with/src/lib.rs b/serde_with/src/lib.rs
index e350ff9..ce2f643 100644
--- a/serde_with/src/lib.rs
+++ b/serde_with/src/lib.rs
@@ -797,7 +797,6 @@ pub struct BytesOrString;
 /// [`formats::Strict`] means that deserialization only supports the type given in `FORMAT`, e.g., if `FORMAT` is `u64` deserialization from a `f64` will error.
 /// [`formats::Flexible`] means that deserialization will perform a best effort to extract the correct duration and allows deserialization from any type.
 /// For example, deserializing `DurationSeconds<f64, Flexible>` will discard any subsecond precision during deserialization from `f64` and will parse a `String` as an integer number.
-/// Serialization of integers will round the duration to the nearest value.
 ///
 /// This type also supports [`chrono::Duration`] with the `chrono_0_4`-[feature flag].
 /// This type also supports [`time::Duration`][::time_0_3::Duration] with the `time_0_3`-[feature flag].
@@ -938,7 +937,6 @@ pub struct DurationSeconds<
 /// De/serialize durations as number of seconds with subsecond precision.
 /// Subsecond precision is *only* supported for [`DurationSecondsWithFrac`], but not for [`DurationSeconds`].
 /// You can configure the serialization format between integers, floats, and stringified numbers with the `FORMAT` specifier and configure the deserialization with the `STRICTNESS` specifier.
-/// Serialization of integers will round the duration to the nearest value.
 ///
 /// The `STRICTNESS` specifier can either be [`formats::Strict`] or [`formats::Flexible`] and defaults to [`formats::Strict`].
 /// [`formats::Strict`] means that deserialization only supports the type given in `FORMAT`, e.g., if `FORMAT` is `u64` deserialization from a `f64` will error.
@@ -1118,7 +1116,6 @@ pub struct DurationNanoSecondsWithFrac<
 /// De/serialize timestamps as seconds since the UNIX epoch.
 /// Subsecond precision is *only* supported for [`TimestampSecondsWithFrac`], but not for [`TimestampSeconds`].
 /// You can configure the serialization format between integers, floats, and stringified numbers with the `FORMAT` specifier and configure the deserialization with the `STRICTNESS` specifier.
-/// Serialization of integers will round the timestamp to the nearest value.
 ///
 /// The `STRICTNESS` specifier can either be [`formats::Strict`] or [`formats::Flexible`] and defaults to [`formats::Strict`].
 /// [`formats::Strict`] means that deserialization only supports the type given in `FORMAT`, e.g., if `FORMAT` is `i64` deserialization from a `f64` will error.
@@ -1272,7 +1269,6 @@ pub struct TimestampSeconds<
 /// De/serialize timestamps as seconds since the UNIX epoch.
 /// Subsecond precision is *only* supported for [`TimestampSecondsWithFrac`], but not for [`TimestampSeconds`].
 /// You can configure the serialization format between integers, floats, and stringified numbers with the `FORMAT` specifier and configure the deserialization with the `STRICTNESS` specifier.
-/// Serialization of integers will round the timestamp to the nearest value.
 ///
 /// The `STRICTNESS` specifier can either be [`formats::Strict`] or [`formats::Flexible`] and defaults to [`formats::Strict`].
 /// [`formats::Strict`] means that deserialization only supports the type given in `FORMAT`, e.g., if `FORMAT` is `i64` deserialization from a `f64` will error.
diff --git a/serde_with/src/ser/duplicates.rs b/serde_with/src/ser/duplicates.rs
index d70f48d..788dec0 100644
--- a/serde_with/src/ser/duplicates.rs
+++ b/serde_with/src/ser/duplicates.rs
@@ -4,6 +4,8 @@ use crate::{
 };
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
+#[cfg(feature = "indexmap_2")]
+use indexmap_2::{IndexMap as IndexMap2, IndexSet as IndexSet2};
 
 macro_rules! set_duplicate_handling {
     ($tyorig:ident < T $(, $typaram:ident : $bound:ident)* >) => {
diff --git a/serde_with/src/ser/impls.rs b/serde_with/src/ser/impls.rs
index e9e10fd..b03b8b4 100644
--- a/serde_with/src/ser/impls.rs
+++ b/serde_with/src/ser/impls.rs
@@ -1,6 +1,8 @@
 use crate::{formats, formats::Strictness, prelude::*};
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
+#[cfg(feature = "indexmap_2")]
+use indexmap_2::{IndexMap as IndexMap2, IndexSet as IndexSet2};
 
 ///////////////////////////////////////////////////////////////////////////////
 // Helper macro used internally
@@ -17,6 +19,8 @@ macro_rules! foreach_map {
         $m!(HashMap<K, V, H: Sized>);
         #[cfg(all(feature = "indexmap_1"))]
         $m!(IndexMap<K, V, H: Sized>);
+        #[cfg(all(feature = "indexmap_2"))]
+        $m!(IndexMap2<K, V, H: Sized>);
     };
 }
 pub(crate) use foreach_map;
@@ -29,6 +33,8 @@ macro_rules! foreach_set {
         $m!(HashSet<$T, H: Sized>);
         #[cfg(all(feature = "indexmap_1"))]
         $m!(IndexSet<$T, H: Sized>);
+        #[cfg(all(feature = "indexmap_2"))]
+        $m!(IndexSet2<$T, H: Sized>);
     };
     ($m:ident) => {
         foreach_set!($m, T);
diff --git a/serde_with_macros/Cargo.toml b/serde_with_macros/Cargo.toml
index f04b5aa..8915ea6 100644
--- a/serde_with_macros/Cargo.toml
+++ b/serde_with_macros/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 authors = ["Jonas Bushart"]
 name = "serde_with_macros"
-rust-version = "1.61"
+rust-version = "1.64"
 version = "3.1.0"
 
 categories = ["encoding"]


Test patch:
diff --git a/serde_with/tests/indexmap_2.rs b/serde_with/tests/indexmap_2.rs
new file mode 100644
index 0000000..58a12b3
--- /dev/null
+++ b/serde_with/tests/indexmap_2.rs
@@ -0,0 +1,262 @@
+#![allow(
+  // clippy is broken and shows wrong warnings
+  // clippy on stable does not know yet about the lint name
+  unknown_lints,
+  // https://github.com/rust-lang/rust-clippy/issues/8867
+  clippy::derive_partial_eq_without_eq,
+)]
+
+mod utils;
+
+use crate::utils::{check_deserialization, check_error_deserialization, is_equal};
+use core::iter::FromIterator;
+use expect_test::expect;
+use indexmap_2::{IndexMap, IndexSet};
+use serde::{Deserialize, Serialize};
+use serde_with::{serde_as, DisplayFromStr, Same};
+use std::net::IpAddr;
+
+#[test]
+fn test_indexmap() {
+    #[serde_as]
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct S(#[serde_as(as = "IndexMap<DisplayFromStr, DisplayFromStr>")] IndexMap<u8, u32>);
+
+    // Normal
+    is_equal(
+        S([(1, 1), (3, 3), (111, 111)].iter().cloned().collect()),
+        expect![[r#"
+            {
+              "1": "1",
+              "3": "3",
+              "111": "111"
+            }"#]],
+    );
+    is_equal(S(IndexMap::default()), expect![[r#"{}"#]]);
+}
+
+#[test]
+fn test_indexset() {
+    #[serde_as]
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct S(#[serde_as(as = "IndexSet<DisplayFromStr>")] IndexSet<u32>);
+
+    // Normal
+    is_equal(
+        S([1, 2, 3, 4, 5].iter().cloned().collect()),
+        expect![[r#"
+            [
+              "1",
+              "2",
+              "3",
+              "4",
+              "5"
+            ]"#]],
+    );
+    is_equal(S(IndexSet::default()), expect![[r#"[]"#]]);
+}
+
+#[test]
+fn test_map_as_tuple_list() {
+    let ip = "1.2.3.4".parse().unwrap();
+    let ip2 = "255.255.255.255".parse().unwrap();
+
+    #[serde_as]
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct SI(#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")] IndexMap<u32, IpAddr>);
+
+    let map: IndexMap<_, _> = vec![(1, ip), (10, ip), (200, ip2)].into_iter().collect();
+    is_equal(
+        SI(map.clone()),
+        expect![[r#"
+            [
+              [
+                "1",
+                "1.2.3.4"
+              ],
+              [
+                "10",
+                "1.2.3.4"
+              ],
+              [
+                "200",
+                "255.255.255.255"
+              ]
+            ]"#]],
+    );
+
+    #[serde_as]
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct SI2(#[serde_as(as = "Vec<(Same, DisplayFromStr)>")] IndexMap<u32, IpAddr>);
+
+    is_equal(
+        SI2(map),
+        expect![[r#"
+            [
+              [
+                1,
+                "1.2.3.4"
+              ],
+              [
+                10,
+                "1.2.3.4"
+              ],
+              [
+                200,
+                "255.255.255.255"
+              ]
+            ]"#]],
+    );
+}
+
+#[test]
+fn duplicate_key_first_wins_indexmap() {
+    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+    struct S(#[serde(with = "::serde_with::rust::maps_first_key_wins")] IndexMap<usize, usize>);
+
+    // Different value and key always works
+    is_equal(
+        S(IndexMap::from_iter(vec![(1, 1), (2, 2), (3, 3)])),
+        expect![[r#"
+            {
+              "1": 1,
+              "2": 2,
+              "3": 3
+            }"#]],
+    );
+
+    // Same value for different keys is ok
+    is_equal(
+        S(IndexMap::from_iter(vec![(1, 1), (2, 1), (3, 1)])),
+        expect![[r#"
+            {
+              "1": 1,
+              "2": 1,
+              "3": 1
+            }"#]],
+    );
+
+    // Duplicate keys, the first one is used
+    check_deserialization(
+        S(IndexMap::from_iter(vec![(1, 1), (2, 2)])),
+        r#"{"1": 1, "2": 2, "1": 3}"#,
+    );
+}
+
+#[test]
+fn prohibit_duplicate_key_indexmap() {
+    #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
+    struct S(
+        #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")] IndexMap<usize, usize>,
+    );
+
+    // Different value and key always works
+    is_equal(
+        S(IndexMap::from_iter(vec![(1, 1), (2, 2), (3, 3)])),
+        expect![[r#"
+            {
+              "1": 1,
+              "2": 2,
+              "3": 3
+            }"#]],
+    );
+
+    // Same value for different keys is ok
+    is_equal(
+        S(IndexMap::from_iter(vec![(1, 1), (2, 1), (3, 1)])),
+        expect![[r#"
+            {
+              "1": 1,
+              "2": 1,
+              "3": 1
+            }"#]],
+    );
+
+    // Duplicate keys are an error
+    check_error_deserialization::<S>(
+        r#"{"1": 1, "2": 2, "1": 3}"#,
+        expect![[r#"invalid entry: found duplicate key at line 1 column 24"#]],
+    );
+}
+
+#[test]
+fn duplicate_value_last_wins_indexset() {
+    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+    struct S(#[serde(with = "::serde_with::rust::sets_last_value_wins")] IndexSet<W>);
+
+    #[derive(Debug, Eq, Deserialize, Serialize)]
+    struct W(i32, bool);
+    impl PartialEq for W {
+        fn eq(&self, other: &Self) -> bool {
+            self.0 == other.0
+        }
+    }
+    impl std::hash::Hash for W {
+        fn hash<H>(&self, state: &mut H)
+        where
+            H: std::hash::Hasher,
+        {
+            self.0.hash(state)
+        }
+    }
+
+    // Different values always work
+    is_equal(
+        S(IndexSet::from_iter(vec![
+            W(1, true),
+            W(2, false),
+            W(3, true),
+        ])),
+        expect![[r#"
+            [
+              [
+                1,
+                true
+              ],
+              [
+                2,
+                false
+              ],
+              [
+                3,
+                true
+              ]
+            ]"#]],
+    );
+
+    let value: S = serde_json::from_str(
+        r#"[
+        [1, false],
+        [1, true],
+        [2, true],
+        [2, false]
+    ]"#,
+    )
+    .unwrap();
+    let entries: Vec<_> = value.0.into_iter().collect();
+    assert_eq!(1, entries[0].0);
+    assert!(entries[0].1);
+    assert_eq!(2, entries[1].0);
+    assert!(!entries[1].1);
+}
+
+#[test]
+fn prohibit_duplicate_value_indexset() {
+    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+    struct S(#[serde(with = "::serde_with::rust::sets_duplicate_value_is_error")] IndexSet<usize>);
+
+    is_equal(
+        S(IndexSet::from_iter(vec![1, 2, 3, 4])),
+        expect![[r#"
+            [
+              1,
+              2,
+              3,
+              4
+            ]"#]],
+    );
+    check_error_deserialization::<S>(
+        r#"[1, 2, 3, 4, 1]"#,
+        expect![[r#"invalid entry: found duplicate value at line 1 column 15"#]],
+    );
+}
diff --git a/serde_with_test/Cargo.toml b/serde_with_test/Cargo.toml
index b7f32ed..45ae8b0 100644
--- a/serde_with_test/Cargo.toml
+++ b/serde_with_test/Cargo.toml
@@ -3,7 +3,7 @@ edition = "2021"
 name = "serde_with_test"
 publish = false
 version = "0.0.0"
-rust-version = "1.61"
+rust-version = "1.64"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html