Skip to content

Commit

Permalink
feat: serde helpers for hashmaps and btreemaps with quantity key types (
Browse files Browse the repository at this point in the history
#1579)

* feat: serde helpers for hashmaps and btreemaps w/ quantity key types

* requested changes

* use alloy map

* fix test

---------

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
  • Loading branch information
stevencartavia and mattsse authored Nov 2, 2024
1 parent 209cd78 commit 1bfa38c
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 9 deletions.
4 changes: 2 additions & 2 deletions crates/serde/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
6 changes: 2 additions & 4 deletions crates/serde/src/other/mod.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
//! Support for capturing other fields.
use alloc::collections::BTreeMap;
use alloc::{collections::BTreeMap, string::String};
use core::{
fmt,
ops::{Deref, DerefMut},
};
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_;

Expand Down Expand Up @@ -243,6 +240,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
use rand::Rng;
use similar_asserts::assert_eq;

Expand Down
183 changes: 180 additions & 3 deletions crates/serde/src/quantity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<K, V, S, H>(map: &HashMap<K, V, H>, serializer: S) -> Result<S::Ok, S::Error>
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<HashMap<K, V, H>, D::Error>
where
K: ConvertRuint + Eq + core::hash::Hash,
V: Deserialize<'de>,
D: Deserializer<'de>,
H: BuildHasher + Default,
{
struct HashMapVisitor<K, V, H> {
marker: PhantomData<(K, V, H)>,
}

impl<'de, K, V, H> serde::de::Visitor<'de> for HashMapVisitor<K, V, H>
where
K: ConvertRuint + Eq + core::hash::Hash,
V: Deserialize<'de>,
H: BuildHasher + Default,
{
type Value = HashMap<K, V, H>;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a map with quantity hex-encoded keys")
}

fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
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::<K::Ruint, V>()? {
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<K, V, S>(value: &BTreeMap<K, V>, serializer: S) -> Result<S::Ok, S::Error>
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<BTreeMap<K, V>, D::Error>
where
K: ConvertRuint + Ord,
V: Deserialize<'de>,
D: Deserializer<'de>,
{
struct BTreeMapVisitor<K, V> {
key_marker: PhantomData<K>,
value_marker: PhantomData<V>,
}

impl<'de, K, V> serde::de::Visitor<'de> for BTreeMapVisitor<K, V>
where
K: ConvertRuint + Ord,
V: Deserialize<'de>,
{
type Value = BTreeMap<K, V>;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a map with quantity hex-encoded keys")
}

fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut values = BTreeMap::new();

while let Some((key, value)) = map.next_entry::<K::Ruint, V>()? {
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 {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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<u128, u128>,
}

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<u128, u128>,
}

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);
}
}

0 comments on commit 1bfa38c

Please sign in to comment.