diff --git a/CHANGELOG.md b/CHANGELOG.md index 69fe74007..4fa562216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Optional serde support for agb-hashmap via the `serde` feature flag + ### Fixed - Fixed build error due to breaking change in `xmrs`. diff --git a/agb-hashmap/Cargo.toml b/agb-hashmap/Cargo.toml index f4f91ac79..71234a47d 100644 --- a/agb-hashmap/Cargo.toml +++ b/agb-hashmap/Cargo.toml @@ -9,11 +9,14 @@ exclude = ["/benches"] [features] allocator_api = [] +serde = ["dep:serde"] [dependencies] rustc-hash = { version = "1", default-features = false } +serde = { version = "1", default-features = false, optional = true } [dev-dependencies] rand = { version = "0.8", default-features = false, features = ["small_rng"] } lazy_static = "1.4" quickcheck = "1" +serde_json = { version = "1", default-features = false, features = ["alloc"] } diff --git a/agb-hashmap/src/lib.rs b/agb-hashmap/src/lib.rs index 940df370a..30c13fd00 100644 --- a/agb-hashmap/src/lib.rs +++ b/agb-hashmap/src/lib.rs @@ -44,6 +44,9 @@ mod allocate { pub(crate) use core::alloc::Allocator; } +#[cfg(feature = "serde")] +mod serde; + use core::{ borrow::Borrow, fmt::Debug, diff --git a/agb-hashmap/src/serde.rs b/agb-hashmap/src/serde.rs new file mode 100644 index 000000000..5d7b201a4 --- /dev/null +++ b/agb-hashmap/src/serde.rs @@ -0,0 +1,198 @@ +use core::{hash::Hash, marker::PhantomData}; +use serde::{ + de::{MapAccess, SeqAccess, Visitor}, + ser::SerializeMap, + Deserialize, Serialize, +}; + +use crate::{ClonableAllocator, HashMap, HashSet}; + +mod hashmap { + use super::*; + + impl Serialize + for HashMap + { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(Some(self.len()))?; + + for (key, value) in self { + map.serialize_entry(key, value)?; + } + + map.end() + } + } + + impl<'de, K, V> Deserialize<'de> for HashMap + where + K: Deserialize<'de> + Hash + Eq, + V: Deserialize<'de>, + { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_map(HashMapVisitor::new()) + } + } + + struct HashMapVisitor { + _marker: PhantomData HashMap>, + } + + impl HashMapVisitor { + fn new() -> Self { + Self { + _marker: PhantomData, + } + } + } + + impl<'de, K, V> Visitor<'de> for HashMapVisitor + where + K: Deserialize<'de> + Hash + Eq, + V: Deserialize<'de>, + { + type Value = HashMap; + + fn expecting(&self, formatter: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { + formatter.write_str("an agb::HashMap") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(8)); + + while let Some((key, value)) = access.next_entry()? { + map.insert(key, value); + } + + Ok(map) + } + } +} + +mod hashset { + + use super::*; + + impl Serialize for HashSet { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_seq(self) + } + } + + struct HashSetVisitor { + _marker: PhantomData HashSet>, + } + + impl HashSetVisitor { + fn new() -> Self { + Self { + _marker: PhantomData, + } + } + } + + impl<'de, K> Visitor<'de> for HashSetVisitor + where + K: Deserialize<'de> + Hash + Eq, + { + type Value = HashSet; + + fn expecting(&self, formatter: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { + formatter.write_str("an agb::HashSet") + } + + fn visit_seq(self, mut access: A) -> Result + where + A: SeqAccess<'de>, + { + let mut set = HashSet::with_capacity(access.size_hint().unwrap_or(8)); + + while let Some(value) = access.next_element()? { + set.insert(value); + } + + Ok(set) + } + } + + impl<'de, K> Deserialize<'de> for HashSet + where + K: Deserialize<'de> + Hash + Eq, + { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_seq(HashSetVisitor::new()) + } + } +} + +#[cfg(test)] +mod test { + use alloc::{ + string::{String, ToString}, + vec::Vec, + }; + + use crate::{HashMap, HashSet}; + + #[test] + fn deserialize_map() { + let json = r#" + { + "three": 3, + "seven": 7 + } + "#; + + let map = serde_json::from_str::>(json).unwrap(); + + assert_eq!( + map, + HashMap::from_iter([("three".to_string(), 3), ("seven".to_string(), 7)]) + ); + } + + #[test] + fn serialize_map() { + let map = HashMap::from_iter([("three".to_string(), 3), ("seven".to_string(), 7)]); + + let json = serde_json::to_string(&map).unwrap(); + + let possibilities = &[r#"{"three":3,"seven":7}"#, r#"{"seven":7,"three":3}"#]; + + assert!(possibilities.contains(&json.as_str())); + } + + #[test] + fn deserialize_hashset() { + let json = "[1, 2, 5, 8, 9, 3, 4]"; + let set = serde_json::from_str::>(json).unwrap(); + + assert_eq!(set, HashSet::from_iter([1, 2, 3, 4, 5, 8, 9])); + } + + #[test] + fn serialize_hashset() { + let set = HashSet::from_iter([1, 2, 3, 5, 8, 9, 10]); + let serialized = serde_json::to_string(&set).unwrap(); + + let mut deserialized = serde_json::from_str::>(&serialized).unwrap(); + deserialized.sort(); + + assert_eq!(deserialized, &[1, 2, 3, 5, 8, 9, 10]); + } +} diff --git a/justfile b/justfile index a3164741b..5f885e098 100644 --- a/justfile +++ b/justfile @@ -19,6 +19,10 @@ clippy: just _all-crates _clippy test: + # test the workspace + cargo test + # also need to explicitly hit the serde tests in agb-hashmap + (cd agb-hashmap && cargo test --features=serde serde) just _test-debug agb just _test-debug tracker/agb-tracker just _test-multiboot