save_load_traits/
impl_default_macro.rs

1// ---------------- [ File: save-load-traits/src/impl_default_macro.rs ]
2crate::ix!();
3
4#[macro_export]
5macro_rules! impl_default_save_to_file_traits {
6    ($ty:ty) => {
7        #[async_trait]
8        impl LoadFromFile for $ty {
9            type Error = SaveLoadError;
10
11            async fn load_from_file(
12                filename: impl AsRef<std::path::Path> + Send,
13            ) -> Result<Self, Self::Error> {
14                debug!("Attempting to load `{}` from file: {:?}", stringify!($ty), filename.as_ref());
15
16                let content = match tokio::fs::read_to_string(filename.as_ref()).await {
17                    Ok(c) => {
18                        trace!("File content successfully read for `{}`: {}", stringify!($ty), c);
19                        c
20                    },
21                    Err(e) => {
22                        error!("Failed to read file for `{}`: {:?}", stringify!($ty), e);
23                        return Err(SaveLoadError::IoError(e));
24                    }
25                };
26
27                match serde_json::from_str(&content) {
28                    Ok(instance) => {
29                        info!("Successfully deserialized `{}` from file: {:?}", stringify!($ty), filename.as_ref());
30                        Ok(instance)
31                    },
32                    Err(e) => {
33                        error!("Deserialization error for `{}`: {:?}", stringify!($ty), e);
34                        return Err(SaveLoadError::JsonParseError(e.into()));
35                    }
36                }
37            }
38        }
39
40        #[async_trait]
41        impl SaveToFile for $ty {
42            type Error = SaveLoadError;
43
44            async fn save_to_file(
45                &self,
46                filename: impl AsRef<std::path::Path> + Send,
47            ) -> Result<(), Self::Error> {
48                debug!("Attempting to save `{}` to file: {:?}", stringify!($ty), filename.as_ref());
49
50                let serialized = match serde_json::to_string_pretty(self) {
51                    Ok(json_str) => {
52                        trace!("Successfully serialized `{}` to JSON: {}", stringify!($ty), json_str);
53                        json_str
54                    },
55                    Err(e) => {
56                        error!("Serialization error for `{}`: {:?}", stringify!($ty), e);
57                        return Err(SaveLoadError::JsonParseError(e.into()));
58                    }
59                };
60
61                if let Err(e) = tokio::fs::write(filename.as_ref(), &serialized).await {
62                    error!("Failed to write `{}` to file: {:?}", stringify!($ty), e);
63                    return Err(SaveLoadError::IoError(e));
64                }
65
66                info!("Successfully saved `{}` to file: {:?}", stringify!($ty), filename.as_ref());
67                Ok(())
68            }
69        }
70    }
71}
72
73#[cfg(test)]
74mod default_impl_save_to_file_traits_when_serde_test {
75    use super::*;
76
77    #[derive(Serialize, Deserialize, Debug, PartialEq)]
78    struct TestData {
79        foo: String,
80        bar: i32,
81    }
82
83    // We now use our macro that returns `SaveLoadError` instead of `serde_json::Error`.
84    impl_default_save_to_file_traits!(TestData);
85
86    #[traced_test]
87    async fn test_save_and_load() {
88        let data = TestData {
89            foo: "Hello".to_string(),
90            bar: 42,
91        };
92
93        let dir = tempdir().expect("Failed to create temp directory");
94        let file_path = dir.path().join("test.json");
95
96        info!("Attempting to save `TestData` to temporary file.");
97        data.save_to_file(&file_path)
98            .await
99            .expect("Failed to save data");
100
101        info!("Attempting to load `TestData` back from the same file.");
102        let loaded = TestData::load_from_file(&file_path)
103            .await
104            .expect("Failed to load data");
105        pretty_assert_eq!(data, loaded, "Loaded data did not match saved data.");
106    }
107
108    #[traced_test]
109    async fn test_load_from_non_existent_file() {
110        let non_existent_path = "definitely_does_not_exist.json";
111        warn!("Trying to load `TestData` from a non-existent file. Expecting an error.");
112        let result = TestData::load_from_file(non_existent_path).await;
113        match result {
114            Err(SaveLoadError::IoError(_)) => {
115                info!("Got expected IoError for non-existent file.")
116            },
117            other => panic!("Expected an IoError, got {:?}", other),
118        }
119    }
120
121    #[traced_test]
122    async fn test_save_invalid_path() {
123        let dir = tempdir().expect("Failed to create temp directory");
124        let invalid_file_path = dir.path(); // Using the directory path instead of a file.
125
126        let data = TestData {
127            foo: "Hello".to_string(),
128            bar: 42,
129        };
130
131        warn!("Trying to save `TestData` to a directory path instead of a file. Expecting an error.");
132        let result = data.save_to_file(&invalid_file_path).await;
133        match result {
134            Err(SaveLoadError::IoError(_)) => {
135                info!("Got expected IoError for invalid save path.")
136            },
137            other => panic!("Expected an IoError, got {:?}", other),
138        }
139    }
140
141    #[traced_test]
142    async fn test_load_corrupt_file() {
143        let dir = tempdir().expect("Failed to create temp directory");
144        let file_path = dir.path().join("corrupt.json");
145
146        debug!("Writing invalid JSON to file to test parse errors.");
147        tokio::fs::write(&file_path, b"not valid json")
148            .await
149            .expect("Failed to write corrupt data to file");
150
151        warn!("Trying to load `TestData` from a corrupt JSON file. Expecting a JsonParseError.");
152        let result = TestData::load_from_file(&file_path).await;
153        match result {
154            Err(SaveLoadError::JsonParseError(_)) => {
155                info!("Got expected JsonParseError for corrupt file.")
156            },
157            other => panic!("Expected a JsonParseError, got {:?}", other),
158        }
159    }
160}