From 1bfa38c5220beb9c740ea126e92121a4b1055cf6 Mon Sep 17 00:00:00 2001 From: Steven <112043913+stevencartavia@users.noreply.github.com> Date: Sat, 2 Nov 2024 02:24:18 -0600 Subject: [PATCH] feat: serde helpers for hashmaps and btreemaps with quantity key types (#1579) * feat: serde helpers for hashmaps and btreemaps w/ quantity key types * requested changes * use alloy map * fix test --------- Co-authored-by: Matthias Seitz --- crates/serde/Cargo.toml | 4 +- crates/serde/src/other/mod.rs | 6 +- crates/serde/src/quantity.rs | 183 +++++++++++++++++++++++++++++++++- 3 files changed, 184 insertions(+), 9 deletions(-) diff --git a/crates/serde/Cargo.toml b/crates/serde/Cargo.toml index 43c42392050..5fe3cd6ca9e 100644 --- a/crates/serde/Cargo.toml +++ b/crates/serde/Cargo.toml @@ -19,12 +19,12 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] -alloy-primitives = { workspace = true, features = ["rlp", "serde"] } +alloy-primitives = { workspace = true, features = ["serde", "map"] } serde.workspace = true serde_json = { workspace = true, features = ["alloc"] } # arbitrary -arbitrary = { version = "1.3", features = ["derive"], optional = true } +arbitrary = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] alloy-primitives = { workspace = true, features = [ diff --git a/crates/serde/src/other/mod.rs b/crates/serde/src/other/mod.rs index c70d14921c6..5b120a8aa2c 100644 --- a/crates/serde/src/other/mod.rs +++ b/crates/serde/src/other/mod.rs @@ -1,6 +1,6 @@ //! Support for capturing other fields. -use alloc::collections::BTreeMap; +use alloc::{collections::BTreeMap, string::String}; use core::{ fmt, ops::{Deref, DerefMut}, @@ -8,9 +8,6 @@ use core::{ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; -#[cfg(not(feature = "std"))] -use alloc::string::String; - #[cfg(any(test, feature = "arbitrary"))] mod arbitrary_; @@ -243,6 +240,7 @@ where #[cfg(test)] mod tests { use super::*; + use alloc::string::ToString; use rand::Rng; use similar_asserts::assert_eq; diff --git a/crates/serde/src/quantity.rs b/crates/serde/src/quantity.rs index fb25fc4acd9..0d14b2cb448 100644 --- a/crates/serde/src/quantity.rs +++ b/crates/serde/src/quantity.rs @@ -164,6 +164,141 @@ pub mod u128_vec_vec_opt { } } +/// Serde functions for encoding a `HashMap` of primitive numbers using the Ethereum "quantity" +/// format. +/// +/// See [`quantity`](self) for more information. +pub mod hashmap { + use super::private::ConvertRuint; + use alloy_primitives::map::HashMap; + use core::{fmt, hash::BuildHasher, marker::PhantomData}; + use serde::{ + de::MapAccess, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer, + }; + + /// Serializes a `HashMap` of primitive numbers as a "quantity" hex string. + pub fn serialize(map: &HashMap, serializer: S) -> Result + where + K: ConvertRuint, + V: Serialize, + S: Serializer, + H: BuildHasher, + { + let mut map_ser = serializer.serialize_map(Some(map.len()))?; + for (key, value) in map { + map_ser.serialize_entry(&key.into_ruint(), value)?; + } + map_ser.end() + } + + /// Deserializes a `HashMap` of primitive numbers from a "quantity" hex string. + pub fn deserialize<'de, K, V, D, H>(deserializer: D) -> Result, D::Error> + where + K: ConvertRuint + Eq + core::hash::Hash, + V: Deserialize<'de>, + D: Deserializer<'de>, + H: BuildHasher + Default, + { + struct HashMapVisitor { + marker: PhantomData<(K, V, H)>, + } + + impl<'de, K, V, H> serde::de::Visitor<'de> for HashMapVisitor + where + K: ConvertRuint + Eq + core::hash::Hash, + V: Deserialize<'de>, + H: BuildHasher + Default, + { + type Value = HashMap; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a map with quantity hex-encoded keys") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut values = + HashMap::with_capacity_and_hasher(map.size_hint().unwrap_or(0), H::default()); + + while let Some((key, value)) = map.next_entry::()? { + values.insert(K::from_ruint(key), value); + } + Ok(values) + } + } + + let visitor = HashMapVisitor { marker: PhantomData }; + deserializer.deserialize_map(visitor) + } +} + +/// Serde functions for encoding a `BTreeMap` of primitive numbers using the Ethereum "quantity" +/// format. +pub mod btreemap { + use super::private::ConvertRuint; + use alloc::collections::BTreeMap; + use core::{fmt, marker::PhantomData}; + use serde::{ + de::MapAccess, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer, + }; + + /// Serializes a `BTreeMap` of primitive numbers as a "quantity" hex string. + pub fn serialize(value: &BTreeMap, serializer: S) -> Result + where + K: ConvertRuint + Ord, + V: Serialize, + S: Serializer, + { + let mut map = serializer.serialize_map(Some(value.len()))?; + for (key, val) in value { + map.serialize_entry(&key.into_ruint(), val)?; + } + map.end() + } + + /// Deserializes a `BTreeMap` of primitive numbers from a "quantity" hex string. + pub fn deserialize<'de, K, V, D>(deserializer: D) -> Result, D::Error> + where + K: ConvertRuint + Ord, + V: Deserialize<'de>, + D: Deserializer<'de>, + { + struct BTreeMapVisitor { + key_marker: PhantomData, + value_marker: PhantomData, + } + + impl<'de, K, V> serde::de::Visitor<'de> for BTreeMapVisitor + where + K: ConvertRuint + Ord, + V: Deserialize<'de>, + { + type Value = BTreeMap; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a map with quantity hex-encoded keys") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut values = BTreeMap::new(); + + while let Some((key, value)) = map.next_entry::()? { + values.insert(K::from_ruint(key), value); + } + Ok(values) + } + } + + let visitor = BTreeMapVisitor { key_marker: PhantomData, value_marker: PhantomData }; + deserializer.deserialize_map(visitor) + } +} + /// Private implementation details of the [`quantity`](self) module. #[allow(unnameable_types)] mod private { @@ -210,10 +345,8 @@ mod private { #[cfg(test)] mod tests { - use serde::{Deserialize, Serialize}; - - #[cfg(not(feature = "std"))] use alloc::{string::ToString, vec, vec::Vec}; + use serde::{Deserialize, Serialize}; #[test] fn test_hex_u64() { @@ -318,4 +451,48 @@ mod tests { let deserialized: Value = serde_json::from_str(&s).unwrap(); assert_eq!(val, deserialized); } + + #[test] + fn test_u128_hashmap_via_ruint() { + use alloy_primitives::map::HashMap; + + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Value { + #[serde(with = "super::hashmap")] + inner: HashMap, + } + + let mut inner_map = HashMap::default(); + inner_map.insert(1000, 2000); + inner_map.insert(3000, 4000); + + let val = Value { inner: inner_map.clone() }; + let s = serde_json::to_string(&val).unwrap(); + + // Deserialize and verify that the original `val` and deserialized version match + let deserialized: Value = serde_json::from_str(&s).unwrap(); + assert_eq!(val.inner, deserialized.inner); + } + + #[test] + fn test_u128_btreemap_via_ruint() { + use alloc::collections::BTreeMap; + + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Value { + #[serde(with = "super::btreemap")] + inner: BTreeMap, + } + + let mut inner_map = BTreeMap::new(); + inner_map.insert(1000, 2000); + inner_map.insert(3000, 4000); + + let val = Value { inner: inner_map }; + let s = serde_json::to_string(&val).unwrap(); + assert_eq!(s, "{\"inner\":{\"0x3e8\":2000,\"0xbb8\":4000}}"); + + let deserialized: Value = serde_json::from_str(&s).unwrap(); + assert_eq!(val, deserialized); + } }