diff --git a/bounded-collections/CHANGELOG.md b/bounded-collections/CHANGELOG.md index 101c91ec..f6e01625 100644 --- a/bounded-collections/CHANGELOG.md +++ b/bounded-collections/CHANGELOG.md @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog]. [Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ +## [0.2.1] - 2024-10-08 +- Added `serde` support for `BoundedBTreeMap`. [#870](https://github.com/paritytech/parity-common/pull/870) + ## [0.2.0] - 2024-01-29 - Added `try_rotate_left` and `try_rotate_right` to `BoundedVec`. [#800](https://github.com/paritytech/parity-common/pull/800) diff --git a/bounded-collections/Cargo.toml b/bounded-collections/Cargo.toml index f9496eb8..674b934f 100644 --- a/bounded-collections/Cargo.toml +++ b/bounded-collections/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bounded-collections" -version = "0.2.0" +version = "0.2.1" authors = ["Parity Technologies "] license = "MIT OR Apache-2.0" homepage = "https://github.com/paritytech/parity-common" diff --git a/bounded-collections/src/bounded_btree_map.rs b/bounded-collections/src/bounded_btree_map.rs index c3369c19..6a441941 100644 --- a/bounded-collections/src/bounded_btree_map.rs +++ b/bounded-collections/src/bounded_btree_map.rs @@ -21,6 +21,11 @@ use crate::{Get, TryCollect}; use alloc::collections::BTreeMap; use codec::{Compact, Decode, Encode, MaxEncodedLen}; use core::{borrow::Borrow, marker::PhantomData, ops::Deref}; +#[cfg(feature = "serde")] +use serde::{ + de::{Error, MapAccess, Visitor}, + Deserialize, Deserializer, Serialize, +}; /// A bounded map based on a B-Tree. /// @@ -29,9 +34,70 @@ use core::{borrow::Borrow, marker::PhantomData, ops::Deref}; /// /// Unlike a standard `BTreeMap`, there is an enforced upper limit to the number of items in the /// map. All internal operations ensure this bound is respected. +#[cfg_attr(feature = "serde", derive(Serialize), serde(transparent))] #[derive(Encode, scale_info::TypeInfo)] #[scale_info(skip_type_params(S))] -pub struct BoundedBTreeMap(BTreeMap, PhantomData); +pub struct BoundedBTreeMap( + BTreeMap, + #[cfg_attr(feature = "serde", serde(skip_serializing))] PhantomData, +); + +#[cfg(feature = "serde")] +impl<'de, K, V, S: Get> Deserialize<'de> for BoundedBTreeMap +where + K: Deserialize<'de> + Ord, + V: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Create a visitor to visit each element in the map + struct BTreeMapVisitor(PhantomData<(K, V, S)>); + + impl<'de, K, V, S> Visitor<'de> for BTreeMapVisitor + where + K: Deserialize<'de> + Ord, + V: Deserialize<'de>, + S: Get, + { + type Value = BTreeMap; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("a map") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let size = map.size_hint().unwrap_or(0); + let max = S::get() as usize; + if size > max { + Err(A::Error::custom("map exceeds the size of the bounds")) + } else { + let mut values = BTreeMap::new(); + + while let Some(key) = map.next_key()? { + if values.len() >= max { + return Err(A::Error::custom("map exceeds the size of the bounds")); + } + let value = map.next_value()?; + values.insert(key, value); + } + + Ok(values) + } + } + } + + let visitor: BTreeMapVisitor = BTreeMapVisitor(PhantomData); + deserializer.deserialize_map(visitor).map(|v| { + BoundedBTreeMap::::try_from(v) + .map_err(|_| Error::custom("failed to create a BoundedBTreeMap from the provided map")) + })? + } +} impl Decode for BoundedBTreeMap where @@ -44,7 +110,7 @@ where // the len is too big. let len: u32 = >::decode(input)?.into(); if len > S::get() { - return Err("BoundedBTreeMap exceeds its limit".into()) + return Err("BoundedBTreeMap exceeds its limit".into()); } input.descend_ref()?; let inner = Result::from_iter((0..len).map(|_| Decode::decode(input)))?; @@ -662,4 +728,60 @@ mod test { } let _foo = Foo::default(); } + + #[cfg(feature = "serde")] + mod serde { + use super::*; + use crate::alloc::string::ToString; + + #[test] + fn test_bounded_btreemap_serializer() { + let mut map = BoundedBTreeMap::>::new(); + map.try_insert(0, 100).unwrap(); + map.try_insert(1, 101).unwrap(); + map.try_insert(2, 102).unwrap(); + + let serialized = serde_json::to_string(&map).unwrap(); + assert_eq!(serialized, r#"{"0":100,"1":101,"2":102}"#); + } + + #[test] + fn test_bounded_btreemap_deserializer() { + let json_str = r#"{"0":100,"1":101,"2":102}"#; + let map: Result>, serde_json::Error> = serde_json::from_str(json_str); + assert!(map.is_ok()); + let map = map.unwrap(); + + assert_eq!(map.len(), 3); + assert_eq!(map.get(&0), Some(&100)); + assert_eq!(map.get(&1), Some(&101)); + assert_eq!(map.get(&2), Some(&102)); + } + + #[test] + fn test_bounded_btreemap_deserializer_bound() { + let json_str = r#"{"0":100,"1":101,"2":102}"#; + let map: Result>, serde_json::Error> = serde_json::from_str(json_str); + assert!(map.is_ok()); + let map = map.unwrap(); + + assert_eq!(map.len(), 3); + assert_eq!(map.get(&0), Some(&100)); + assert_eq!(map.get(&1), Some(&101)); + assert_eq!(map.get(&2), Some(&102)); + } + + #[test] + fn test_bounded_btreemap_deserializer_failed() { + let json_str = r#"{"0":100,"1":101,"2":102,"3":103,"4":104}"#; + let map: Result>, serde_json::Error> = serde_json::from_str(json_str); + + match map { + Err(e) => { + assert!(e.to_string().contains("map exceeds the size of the bounds")); + }, + _ => unreachable!("deserializer must raise error"), + } + } + } }