crate::ix!();
#[async_trait]
impl<T> SaveToFile for Vec<T>
where
T: Serialize + DeserializeOwned + Send + Sync + 'static,
{
type Error = SaveLoadError;
async fn save_to_file(
&self,
filename: impl AsRef<Path> + Send,
) -> Result<(), Self::Error> {
let json_data = serde_json::to_string_pretty(&self)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let mut file = File::create(filename).await?;
file.write_all(json_data.as_bytes()).await?;
Ok(())
}
}
#[async_trait]
impl<T> LoadFromFile for Vec<T>
where
T: Serialize + DeserializeOwned + Send + Sync + 'static,
{
type Error = SaveLoadError;
async fn load_from_file(filename: impl AsRef<Path> + Send)
-> Result<Self, Self::Error>
{
let mut file = File::open(filename).await?;
let mut buf = Vec::new();
file.read_to_end(&mut buf).await?;
let list_of_items: Vec<T> = serde_json::from_slice(&buf)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(list_of_items)
}
}
#[cfg(test)]
mod test_vec_t_save_load {
use super::*;
use tokio::fs;
use traced_test::traced_test;
use std::path::PathBuf;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
struct ExampleItem {
name: String,
value: i32,
}
#[traced_test]
async fn test_save_load_vec_example_item() {
let items = vec![
ExampleItem { name: "First".into(), value: 10 },
ExampleItem { name: "Second".into(), value: 20 },
];
let tmpfile = PathBuf::from("test_vec_example_item.json");
items.save_to_file(&tmpfile).await.expect("save failed");
let loaded_vec = <Vec<ExampleItem>>::load_from_file(&tmpfile)
.await
.expect("load failed");
assert_eq!(loaded_vec, items, "Roundtrip mismatch for Vec<ExampleItem>");
fs::remove_file(&tmpfile).await.ok();
}
#[traced_test]
async fn test_save_load_vec_string() {
let data = vec!["alpha".to_string(), "beta".to_string()];
let tmpfile = PathBuf::from("test_vec_string.json");
data.save_to_file(&tmpfile).await.expect("save failed");
let loaded = <Vec<String>>::load_from_file(&tmpfile)
.await
.expect("load failed");
assert_eq!(loaded, data, "Roundtrip mismatch for Vec<String>");
fs::remove_file(&tmpfile).await.ok();
}
#[traced_test]
async fn test_load_non_existent_file() {
let result = <Vec<ExampleItem>>::load_from_file("no_such_vec_123.json").await;
assert!(result.is_err(), "Should fail for a nonexistent file");
}
#[traced_test]
async fn test_load_corrupt_json() {
let tmpfile = PathBuf::from("test_vec_t_corrupt.json");
fs::write(&tmpfile, b"not valid json")
.await
.expect("write fail");
let result = <Vec<ExampleItem>>::load_from_file(&tmpfile).await;
assert!(result.is_err(), "Should fail for corrupt JSON");
fs::remove_file(&tmpfile).await.ok();
}
}
#[cfg(test)]
mod test_vec_string_save_and_load {
use super::*;
use tokio::fs;
#[traced_test]
async fn test_save_load_empty_vec() {
let tmpfile = PathBuf::from("test_vec_string_empty.json");
let empty_vec: Vec<String> = vec![];
empty_vec.save_to_file(&tmpfile).await.expect("save failed");
let loaded_vec = <Vec<String> as LoadFromFile>::load_from_file(&tmpfile)
.await
.expect("load failed");
assert_eq!(loaded_vec, empty_vec, "Loaded vec must match saved vec");
fs::remove_file(&tmpfile).await.unwrap_or(());
}
#[traced_test]
async fn test_save_load_nonempty_vec() {
let tmpfile = PathBuf::from("test_vec_string_nonempty.json");
let sample_vec = vec!["Alpha".to_string(), "Beta".to_string(), "Gamma".to_string()];
sample_vec.save_to_file(&tmpfile).await.expect("save failed");
let loaded_vec = <Vec<String> as LoadFromFile>::load_from_file(&tmpfile)
.await
.expect("load failed");
assert_eq!(loaded_vec, sample_vec, "Loaded vec must match saved vec");
fs::remove_file(&tmpfile).await.unwrap_or(());
}
#[traced_test]
async fn test_save_load_special_chars() {
let tmpfile = PathBuf::from("test_vec_string_special.json");
let special_vec = vec![
"Line\nbreak".to_string(),
"Tab\tchar".to_string(),
"Unicode: λ -> λ".to_string()
];
special_vec.save_to_file(&tmpfile).await.expect("save failed");
let loaded = <Vec<String> as LoadFromFile>::load_from_file(&tmpfile)
.await
.expect("load failed");
assert_eq!(loaded, special_vec, "Loaded special-chars must match saved data");
fs::remove_file(&tmpfile).await.unwrap_or(());
}
#[traced_test]
async fn test_load_non_existent_file_error() {
let tmpfile = PathBuf::from("this_file_should_not_exist_123.json");
let load_result = <Vec<String> as LoadFromFile>::load_from_file(&tmpfile).await;
assert!(load_result.is_err(), "Expected an error when file does not exist");
}
#[traced_test]
async fn test_load_malformed_json() {
let tmpfile = PathBuf::from("test_vec_string_malformed.json");
let contents = b"This is not valid JSON at all.";
{
let mut f = tokio::fs::File::create(&tmpfile).await.expect("create fail");
use tokio::io::AsyncWriteExt;
f.write_all(contents).await.expect("write fail");
}
let load_result = <Vec<String> as LoadFromFile>::load_from_file(&tmpfile).await;
assert!(load_result.is_err(), "Expected an error for malformed JSON input");
fs::remove_file(&tmpfile).await.unwrap_or(());
}
}