use serde::{Serialize, de::DeserializeOwned};
#[derive(Debug)]
pub enum PersistError {
NotSupported,
Io(std::io::Error),
Serialize(ron::Error),
Deserialize(ron::de::SpannedError),
}
impl std::fmt::Display for PersistError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PersistError::NotSupported => {
formatter.write_str("persist: not supported on this platform")
}
PersistError::Io(error) => write!(formatter, "persist io: {error}"),
PersistError::Serialize(error) => write!(formatter, "persist serialize: {error}"),
PersistError::Deserialize(error) => write!(formatter, "persist deserialize: {error}"),
}
}
}
impl std::error::Error for PersistError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
PersistError::Io(error) => Some(error),
PersistError::Serialize(error) => Some(error),
PersistError::Deserialize(error) => Some(error),
PersistError::NotSupported => None,
}
}
}
impl From<std::io::Error> for PersistError {
fn from(error: std::io::Error) -> Self {
PersistError::Io(error)
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn save<T: Serialize>(path: &str, value: &T) -> Result<(), PersistError> {
let content = ron::ser::to_string_pretty(value, ron::ser::PrettyConfig::default())
.map_err(PersistError::Serialize)?;
if let Some(parent) = std::path::Path::new(path).parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, content.as_bytes())?;
Ok(())
}
#[cfg(target_arch = "wasm32")]
pub fn save<T: Serialize>(_path: &str, _value: &T) -> Result<(), PersistError> {
Err(PersistError::NotSupported)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load<T: DeserializeOwned>(path: &str) -> Result<T, PersistError> {
let content = std::fs::read_to_string(path)?;
ron::from_str(&content).map_err(PersistError::Deserialize)
}
#[cfg(target_arch = "wasm32")]
pub fn load<T: DeserializeOwned>(_path: &str) -> Result<T, PersistError> {
Err(PersistError::NotSupported)
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct TestData {
level: u32,
score: u64,
name: String,
}
#[test]
fn round_trip_basic() {
let tmp = std::env::temp_dir().join("lunar_persist_test_basic.ron");
let path = tmp.to_str().unwrap();
let original = TestData {
level: 5,
score: 9999,
name: "hero".into(),
};
save(path, &original).expect("save failed");
let loaded: TestData = load(path).expect("load failed");
assert_eq!(original, loaded);
let _ = std::fs::remove_file(path);
}
#[test]
fn round_trip_nested_dir() {
let tmp = std::env::temp_dir()
.join("lunar_persist_nested")
.join("slot0.ron");
let path = tmp.to_str().unwrap();
let original = TestData {
level: 1,
score: 0,
name: "new game".into(),
};
save(path, &original).expect("save with nested dir failed");
let loaded: TestData = load(path).expect("load from nested dir failed");
assert_eq!(original, loaded);
let _ = std::fs::remove_file(path);
}
#[test]
fn load_missing_file_returns_io_error() {
let result = load::<TestData>("/tmp/lunar_persist_this_does_not_exist.ron");
assert!(matches!(result, Err(PersistError::Io(_))));
}
#[test]
fn load_bad_ron_returns_deserialize_error() {
let tmp = std::env::temp_dir().join("lunar_persist_bad.ron");
let path = tmp.to_str().unwrap();
std::fs::write(path, b"not valid ron {{{{").unwrap();
let result = load::<TestData>(path);
assert!(matches!(result, Err(PersistError::Deserialize(_))));
let _ = std::fs::remove_file(path);
}
}