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
15pub struct DiskPersist<D> {
17 path: PathBuf,
18 _type: PhantomData<D>,
19}
20
21impl<D: Serialize + DeserializeOwned> DiskPersist<D> {
22 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 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 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 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 pub fn path(&self) -> &Path {
86 self.path.as_path()
87 }
88}
89
90#[derive(Error, Debug)]
92pub enum DataError {
93 #[error(transparent)]
95 Io(#[from] io::Error),
96 #[error(transparent)]
98 Serde(#[from] bincode::Error),
99 #[error("couldn't find cache directory")]
101 NoCacheDir,
102 #[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 let write: DiskPersist<Data> = DiskPersist::init(name).unwrap();
138 let write_data = Data::default();
139 write.write(&write_data).unwrap();
140
141 let read: DiskPersist<Data> = DiskPersist::init(name).unwrap();
143 let read_data = read.read().unwrap().unwrap();
144
145 assert_eq!(write_data, read_data);
147
148 fs::remove_file(write.path()).unwrap();
150 }
151}