save_load_traits/
impl_for_hashmap.rs

1// ---------------- [ File: save-load-traits/src/impl_for_hashmap.rs ]
2crate::ix!();
3
4/// A specialized SaveToFile/LoadFromFile impl for HashMap<K, V>,
5/// so you can avoid the universal blanket approach and thus not conflict with
6/// your macro-based `impl_default_save_to_file_traits!` expansions.
7///
8/// This requires that K,V: Serialize + DeserializeOwned. Also note that
9/// we require K: Eq + Hash. Then you can do:
10///
11///   let map: HashMap<String, MyStruct> = ...;
12///   map.save_to_file("some.json").await?;
13///
14#[async_trait]
15impl<K, V> SaveToFile for HashMap<K, V>
16where
17    K: Serialize + DeserializeOwned + Eq + Hash + Send + Sync + 'static,
18    V: Serialize + DeserializeOwned + Send + Sync + 'static,
19{
20    type Error = SaveLoadError;
21
22    async fn save_to_file(
23        &self,
24        filename: impl AsRef<Path> + Send
25    ) -> Result<(), Self::Error> {
26        let json_data = serde_json::to_string_pretty(&self)
27            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
28
29        let mut file = File::create(filename).await?;
30        file.write_all(json_data.as_bytes()).await?;
31        Ok(())
32    }
33}
34
35#[async_trait]
36impl<K, V> LoadFromFile for HashMap<K, V>
37where
38    K: Serialize + DeserializeOwned + Eq + Hash + Send + Sync + 'static,
39    V: Serialize + DeserializeOwned + Send + Sync + 'static,
40{
41    type Error = SaveLoadError;
42
43    async fn load_from_file(filename: impl AsRef<Path> + Send)
44        -> Result<Self, Self::Error>
45    {
46        let mut file = File::open(filename).await?;
47        let mut buf = Vec::new();
48        file.read_to_end(&mut buf).await?;
49        let map: HashMap<K, V> =
50            serde_json::from_slice(&buf)
51                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
52        Ok(map)
53    }
54}
55
56/// A small test verifying that this specialized HashMap impl works.
57/// In real usage, K,V must each implement Serialize + Deserialize + Eq + Hash (for K).
58#[cfg(test)]
59mod test_hashmap_save_load {
60    use super::*;
61    use tokio::fs;
62    use traced_test::traced_test;
63    use std::collections::HashMap;
64    use std::path::PathBuf;
65
66    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
67    struct FakeLeaf {
68        label: String,
69        id: u32,
70    }
71
72    #[traced_test]
73    async fn test_save_load_hashmap_basic() {
74        let mut my_map: HashMap<String, FakeLeaf> = HashMap::new();
75        my_map.insert(
76            "Alpha".to_string(),
77            FakeLeaf { label: "AlphaLeaf".into(), id: 1 }
78        );
79        my_map.insert(
80            "Beta".to_string(),
81            FakeLeaf { label: "BetaLeaf".into(), id: 2 }
82        );
83
84        let tmpfile = PathBuf::from("test_hashmap.json");
85        my_map.save_to_file(&tmpfile).await.expect("save failed");
86
87        let loaded_map = HashMap::<String, FakeLeaf>::load_from_file(&tmpfile)
88            .await
89            .expect("load failed");
90        assert_eq!(loaded_map, my_map, "Roundtrip mismatch for HashMap<String, FakeLeaf>");
91
92        fs::remove_file(&tmpfile).await.ok();
93    }
94
95    #[traced_test]
96    async fn test_load_non_existent_map() {
97        let path = PathBuf::from("unlikely_map_123.json");
98        let result = HashMap::<String, FakeLeaf>::load_from_file(&path).await;
99        assert!(result.is_err(), "Should error on non-existent file");
100    }
101
102    #[traced_test]
103    async fn test_load_malformed_map_json() {
104        let tmpfile = PathBuf::from("test_malformed.json");
105        fs::write(&tmpfile, b"NOT VALID JSON").await.expect("write fail");
106        let result = HashMap::<String, FakeLeaf>::load_from_file(&tmpfile).await;
107        assert!(result.is_err(), "Should error on malformed JSON");
108        fs::remove_file(&tmpfile).await.ok();
109    }
110}