disk_persist/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    fs,
5    io::{self, ErrorKind},
6    marker::PhantomData,
7    path::{Path, PathBuf},
8};
9
10use serde::{de::DeserializeOwned, Serialize};
11use thiserror::Error;
12
13const DEFAULT_FILE_TYPE: &'static str = "persist";
14
15/// A struct for managing persistent data on disk
16pub struct DiskPersist<D> {
17    path: PathBuf,
18    _type: PhantomData<D>,
19}
20
21impl<D: Serialize + DeserializeOwned> DiskPersist<D> {
22    /// Initialize a new [`DiskPersist`].
23    pub fn init(name: impl AsRef<str>) -> Result<Self, DataError> {
24        let mut path: PathBuf = match dirs::cache_dir() {
25            Some(base) => base,
26            None => return Err(DataError::NoCacheDir),
27        };
28
29        path.push(&format!("{}.{}", name.as_ref(), DEFAULT_FILE_TYPE));
30
31        Ok(Self {
32            _type: PhantomData::default(),
33            path,
34        })
35    }
36
37    /// Initialize a new [`DiskPersist`] with a custom path.
38    pub fn init_with_path(path: impl AsRef<Path>) -> Result<Self, DataError> {
39        let path = path.as_ref().to_path_buf();
40
41        if path.is_dir() {
42            return Err(DataError::FoundDirectory);
43        }
44
45        Ok(Self {
46            _type: PhantomData::default(),
47            path,
48        })
49    }
50
51    /// Update data to disk.
52    pub fn write(&self, data: &D) -> Result<(), DataError> {
53        let bytes = match bincode::serialize(data) {
54            Ok(bytes) => bytes,
55            Err(error) => return Err(DataError::Serde(error)),
56        };
57
58        match fs::write(&self.path, bytes) {
59            Ok(_) => Ok(()),
60            Err(error) => return Err(DataError::Io(error)),
61        }
62    }
63
64    /// Read data from disk.
65    pub fn read(&self) -> Result<Option<D>, DataError> {
66        let bytes = match fs::read(&self.path) {
67            Ok(bytes) => bytes,
68            Err(error) => {
69                return match error.kind() {
70                    ErrorKind::NotFound => return Ok(None),
71                    _ => Err(DataError::Io(error)),
72                }
73            }
74        };
75
76        let deserialized: D = match bincode::deserialize(&bytes) {
77            Ok(deserialized) => deserialized,
78            Err(error) => return Err(DataError::Serde(error)),
79        };
80
81        Ok(Some(deserialized))
82    }
83
84    /// Return a reference to the internal path where data is written.
85    pub fn path(&self) -> &Path {
86        self.path.as_path()
87    }
88}
89
90/// Errors returned by [`DiskPersist`].
91#[derive(Error, Debug)]
92pub enum DataError {
93    /// Error reading or writing to your file.
94    #[error(transparent)]
95    Io(#[from] io::Error),
96    /// Couldn't serialize or deserialize your data, this could mean that the data is corrupted.
97    #[error(transparent)]
98    Serde(#[from] bincode::Error),
99    /// Couldn't find a cache directory on the system.
100    #[error("couldn't find cache directory")]
101    NoCacheDir,
102    /// The path optional save path you used is a directory, and not a file.
103    #[error("optional save path must be a file, not a directory")]
104    FoundDirectory,
105}
106
107#[cfg(test)]
108mod tests {
109    use std::fs;
110
111    use serde::{Deserialize, Serialize};
112
113    use crate::DiskPersist;
114
115    #[derive(Serialize, Deserialize, PartialEq, Debug)]
116    struct Data {
117        name: String,
118        age: u8,
119        location: (f64, f64),
120    }
121
122    impl Default for Data {
123        fn default() -> Self {
124            Self {
125                name: "Jane Doe".to_string(),
126                age: 45,
127                location: (49.24565431256531, 111.35598566896671),
128            }
129        }
130    }
131
132    #[test]
133    fn full_test() {
134        let name = "disk-persist-test";
135
136        // write to disk
137        let write: DiskPersist<Data> = DiskPersist::init(name).unwrap();
138        let write_data = Data::default();
139        write.write(&write_data).unwrap();
140
141        // read from disk
142        let read: DiskPersist<Data> = DiskPersist::init(name).unwrap();
143        let read_data = read.read().unwrap().unwrap();
144
145        // compare what we got (in rust structs)
146        assert_eq!(write_data, read_data);
147
148        // clean up after ourselves
149        fs::remove_file(write.path()).unwrap();
150    }
151}