use {
anyhow::anyhow,
serde::{de::DeserializeOwned, Serialize},
std::{
fs::File,
io::{Read, Write},
ops::{Deref, DerefMut},
path::PathBuf,
},
};
#[derive(Debug)]
pub struct OnDisk<T> {
inner: T,
path: PathBuf,
}
impl<T: Serialize + DeserializeOwned + Default> OnDisk<T> {
pub fn open(path: PathBuf) -> anyhow::Result<Self> {
let mut file = File::open(&path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let inner = toml::from_str(&content)?;
Ok(Self { path, inner })
}
pub fn open_or_default(path: PathBuf) -> anyhow::Result<Self> {
let mut file = match File::open(&path) {
Ok(file) => file,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Self {
path,
inner: Default::default(),
})
}
Err(e) => Err(e)?,
};
let mut content = String::new();
file.read_to_string(&mut content)?;
if content.trim().is_empty() {
return Ok(Self {
path,
inner: Default::default(),
});
}
let inner = toml::from_str(&content)?;
Ok(Self { path, inner })
}
pub fn new_from_default(path: PathBuf) -> Self {
Self {
path,
inner: Default::default(),
}
}
pub fn save(&self) -> anyhow::Result<()> {
let folder = self
.path
.parent()
.ok_or(anyhow!("expected file to be in a folder"))?;
std::fs::create_dir_all(folder)?;
let mut file = File::create(&self.path)?;
let content = toml::to_string(&self.inner)?;
file.write_all(content.as_bytes())?;
Ok(())
}
pub fn into_inner(self) -> T {
self.inner
}
}
impl<T> Deref for OnDisk<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> DerefMut for OnDisk<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
#[cfg(test)]
mod tests {
use {
super::*,
serde::{Deserialize, Serialize},
};
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
struct TestData {
#[serde(default)]
value: String,
}
#[test]
fn roundtrip_save_and_open() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
let mut disk = OnDisk::<TestData>::new_from_default(path.clone());
disk.value = "hello".into();
disk.save().unwrap();
let loaded = OnDisk::<TestData>::open(path).unwrap();
assert_eq!(loaded.value, "hello");
}
#[test]
fn open_missing_file_errors() {
let result = OnDisk::<TestData>::open("/nonexistent/path.toml".into());
assert!(result.is_err());
}
#[test]
fn open_or_default_missing_file_returns_default() {
let disk = OnDisk::<TestData>::open_or_default("/nonexistent/path.toml".into()).unwrap();
assert_eq!(disk.value, "");
}
#[test]
fn open_or_default_empty_file_returns_default() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), "").unwrap();
let disk = OnDisk::<TestData>::open_or_default(tmp.path().to_path_buf()).unwrap();
assert_eq!(disk.value, "");
}
#[test]
fn open_invalid_toml_errors() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), "not valid [[[ toml").unwrap();
let result = OnDisk::<TestData>::open(tmp.path().to_path_buf());
assert!(result.is_err());
}
#[test]
fn into_inner_returns_data() {
let mut disk = OnDisk::<TestData>::new_from_default("/tmp/test.toml".into());
disk.value = "inner".into();
let data = disk.into_inner();
assert_eq!(data.value, "inner");
}
}