rusty_store/storage.rs
1use ron::ser::PrettyConfig;
2use serde::{Deserialize, Serialize};
3use std::fmt::Debug;
4use std::fs::{self, File, OpenOptions};
5use std::io::{Read, Write};
6use std::path::PathBuf;
7use thiserror::Error;
8
9use log::debug;
10use log::info;
11use log::warn;
12
13use crate::manager::StoreManager;
14
15#[derive(Error, Debug)]
16pub enum StoreError {
17 #[error("RON parsing error: {0}")]
18 RonParse(#[source] ron::error::SpannedError),
19
20 #[error("RON error: {0}")]
21 Ron(#[source] ron::error::Error),
22
23 #[error("Failed to open file: {0}")]
24 FileOpen(#[source] std::io::Error),
25
26 #[error("Failed to read from file: {0}")]
27 Read(#[source] std::io::Error),
28
29 #[error("Failed to create directory: {0}")]
30 CreateDir(#[source] std::io::Error),
31
32 #[error("Failed to write to file: {0}")]
33 Write(#[source] std::io::Error),
34}
35
36/// Represents the different types of storage locations that can be used for storing data.
37///
38/// The `StoringType` enum provides a way to specify where data should be stored, based on the
39/// platform and the type of data. It includes variants for common storage locations such as
40/// cache, data, and configuration directories, as well as a custom path option.
41///
42/// # Variants
43///
44/// - `Cache`: Path to the user's cache directory.
45/// - `Data`: Path to the user's data directory.
46/// - `Config`: Path to the user's configuration directory.
47/// - `Custom(PathBuf)`: Custom path specified by the user.
48#[derive(Debug, Default)]
49pub enum StoringType {
50 /// Path to the user's cache directory.
51 ///
52 /// |Platform | Value | Example |
53 /// | ------- | ----------------------------------- | ---------------------------- |
54 /// | Linux | `$XDG_CACHE_HOME` or `$HOME`/.cache | /home/alice/.cache |
55 /// | macOS | `$HOME`/Library/Caches | /Users/Alice/Library/Caches |
56 /// | Windows | `{FOLDERID_LocalAppData}` | C:\Users\Alice\AppData\Local |
57 Cache,
58
59 /// Path to the user's data directory.
60 ///
61 /// |Platform | Value | Example |
62 /// | ------- | ---------------------------------------- | ---------------------------------------- |
63 /// | Linux | `$XDG_DATA_HOME` or `$HOME`/.local/share | /home/alice/.local/share |
64 /// | macOS | `$HOME`/Library/Application Support | /Users/Alice/Library/Application Support |
65 /// | Windows | `{FOLDERID_RoamingAppData}` | C:\Users\Alice\AppData\Roaming |
66 #[default]
67 Data,
68
69 /// Path to the user's config directory.
70 ///
71 /// |Platform | Value | Example |
72 /// | ------- | ------------------------------------- | ---------------------------------------- |
73 /// | Linux | `$XDG_CONFIG_HOME` or `$HOME`/.config | /home/alice/.config |
74 /// | macOS | `$HOME`/Library/Application Support | /Users/Alice/Library/Application Support |
75 /// | Windows | `{FOLDERID_RoamingAppData}` | C:\Users\Alice\AppData\Roaming |
76 Config,
77
78 /// Custom path of the store save location
79 Custom(PathBuf),
80}
81
82/// Trait allowing a struct to be managed by `Storage`.
83///
84/// The `Storing` trait provides a way to specify the type of storage location for a struct.
85/// It requires the struct to implement `Serialize`, `Deserialize`, and `Default` traits.
86///
87/// # Example
88///
89/// ```
90/// use rusty_store::{Storage, StoreHandle, Storing, StoringType};
91/// use serde::{Deserialize, Serialize};
92///
93///
94/// #[derive(Serialize, Deserialize, Default)]
95/// struct MyStore {
96/// pub count: u32,
97/// }
98///
99/// impl Storing for MyStore {
100/// fn store_type() -> StoringType {
101/// StoringType::Data
102/// }
103/// }
104/// ```
105pub trait Storing: Serialize + for<'de> Deserialize<'de> + Default {
106 fn store_type() -> StoringType {
107 StoringType::default()
108 }
109}
110
111/// `StoreHandle` acts as a container that holds store data in memory and provides methods to access
112/// and modify this data. The `StoreHandle` does not manage storage directly but facilitates the read and
113/// write operations by holding the data and its identifier.
114#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
115pub struct StoreHandle<T> {
116 store_id: String,
117 store: T,
118}
119
120impl<T: Storing> StoreHandle<T> {
121 /// Creates a new `StoreHandle` instance with the given `store_id`.
122 ///
123 /// # Arguments
124 ///
125 /// * `store_id` - A string slice that holds the identifier for the store.
126 ///
127 /// # Returns
128 ///
129 /// A new `StoreHandle` instance with the specified `store_id` and a default store.
130 pub fn new(store_id: &str) -> Self {
131 Self {
132 store: T::default(),
133 store_id: store_id.to_owned(),
134 }
135 }
136
137 /// Sets the store data.
138 ///
139 /// # Arguments
140 ///
141 /// * `store` - The store data to be set.
142 fn set_store(&mut self, store: T) {
143 debug!("Setting store with id: {}", self.store_id);
144 self.store = store;
145 }
146
147 /// Returns a mutable reference to the stored data.
148 ///
149 /// # Returns
150 ///
151 /// A mutable reference to the stored data.
152 pub fn get_store_mut(&mut self) -> &mut T {
153 &mut self.store
154 }
155
156 /// Returns a reference to the stored data.
157 ///
158 /// # Returns
159 ///
160 /// A reference to the stored data.
161 pub fn get_store(&self) -> &T {
162 &self.store
163 }
164
165 /// Returns a reference to the store identifier.
166 ///
167 /// # Returns
168 ///
169 /// A reference to the store identifier.
170 pub fn store_id(&self) -> &str {
171 &self.store_id
172 }
173}
174
175/// Handles file system paths for reading from and writing to data storage.
176///
177/// The `Storage` struct provides a way to manage file paths used for storing data in different locations.
178/// It simplifies the process of accessing and modifying data by providing methods for these operations.
179///
180/// # Example
181///
182/// ```
183/// use rusty_store::{Storage, StoreHandle, Storing};
184/// use serde::{Deserialize, Serialize};
185///
186/// #[derive(Serialize, Deserialize, Default, Storing)]
187/// pub struct MyStore {
188/// pub count: u32,
189/// }
190///
191/// impl MyStore {
192/// fn increment_count(&mut self) {
193/// self.count += 1;
194/// }
195/// }
196///
197///
198/// // Initialize the Storage with the defaults
199/// let storage = Storage::new("APP_ID");
200///
201/// // Create a handle for managing the store data.
202/// let mut handle = StoreHandle::<MyStore>::new("my_store_id");
203///
204/// // Read existing store from storage
205/// storage
206/// .read(&mut handle)
207/// .expect("Failed to read from storage");
208///
209/// // Modify the store data
210/// let counter = handle.get_store_mut();
211///
212/// counter.increment_count();
213/// counter.increment_count();
214/// counter.increment_count();
215///
216/// // Write changes to disk
217/// storage
218/// .write(&mut handle)
219/// .expect("Failed to write to storage");
220///
221/// let counter = handle.get_store();
222///
223/// println!("Count: {}", counter.count);
224/// ```
225#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
226pub struct Storage {
227 cache_dir: PathBuf,
228 data_dir: PathBuf,
229 config_dir: PathBuf,
230}
231
232impl Storage {
233 /// Creates a new `Storage` instance by obtaining the paths for cache, data, and configuration directories.
234 ///
235 /// # Panics
236 ///
237 /// - Panics if the cache directory, data directory, or configuration directory path cannot be determined.
238 pub fn new(app_id: &str) -> Self {
239 Self {
240 data_dir: dirs::data_dir()
241 .expect("Failed to determine cache directory path")
242 .join(app_id),
243 config_dir: dirs::config_dir()
244 .expect("Failed to determine data directory path")
245 .join(app_id),
246 cache_dir: dirs::cache_dir()
247 .expect("Failed to determine configuration directory path")
248 .join(app_id),
249 }
250 }
251
252 /// Creates a new `Storage` instance with specific cache, data, and config paths.
253 ///
254 /// # Arguments
255 ///
256 /// * `cache_dir` - A `PathBuf` representing the cache directory path.
257 /// * `data_dir` - A `PathBuf` representing the data directory path.
258 /// * `config_dir` - A `PathBuf` representing the configuration directory path.
259 ///
260 /// # Returns
261 ///
262 /// A new `Storage` instance with the specified cache, data, and config paths.
263 ///
264 /// # Example
265 ///
266 /// ```
267 /// use std::path::PathBuf;
268 /// use rusty_store::Storage;
269 ///
270 /// let cache_dir = PathBuf::from("/path/to/cache");
271 /// let data_dir = PathBuf::from("/path/to/data");
272 /// let config_dir = PathBuf::from("/path/to/config");
273 ///
274 /// let storage = Storage::from_dirs(cache_dir, data_dir, config_dir);
275 /// ```
276 pub fn from_dirs(cache_dir: PathBuf, data_dir: PathBuf, config_dir: PathBuf) -> Self {
277 Self {
278 cache_dir,
279 data_dir,
280 config_dir,
281 }
282 }
283
284 /// Returns a new StoreManager of type `T` with the given `store_id`
285 pub fn new_manager<T: Storing>(&self, store_id: &str) -> Result<StoreManager<T>, StoreError> {
286 StoreManager::<T>::new(self, store_id)
287 }
288
289 /// Returns a new Handle of type `T` with the given `store_id`
290 pub fn new_handle<T: Storing>(&self, store_id: &str) -> StoreHandle<T> {
291 StoreHandle::<T>::new(store_id)
292 }
293
294 /// Reads the store from a file and updates the provided `StoreHandle`.
295 /// If the file does not exist, it creates a default store if a default is available.
296 ///
297 /// # Example
298 ///
299 /// ```
300 /// use rusty_store::{Storage, StoreHandle, Storing};
301 /// use serde::{Deserialize, Serialize};
302 ///
303 /// #[derive(Serialize, Deserialize, Default, Storing)]
304 /// pub struct MyStore {
305 /// pub count: u32,
306 /// }
307 ///
308 /// impl MyStore {
309 /// fn increment_count(&mut self) {
310 /// self.count += 1;
311 /// }
312 /// }
313 ///
314 /// let storage = Storage::new("APP_ID");
315 /// let mut handle: StoreHandle<MyStore> = StoreHandle::new("my_store_id");
316 ///
317 /// storage.read(&mut handle).expect("Failed to read store");
318 ///
319 /// ```
320 pub fn read<T: Storing>(&self, handle: &mut StoreHandle<T>) -> Result<(), StoreError> {
321 debug!("Reading store with id: {}", handle.store_id());
322 self.open_file::<T, _>(
323 |file, handle| {
324 let store = Self::read_string(file).map_err(StoreError::Read)?;
325 let store_data: T = ron::from_str(&store).map_err(StoreError::RonParse)?;
326
327 handle.set_store(store_data);
328
329 info!("Successfully read store with id: {}", handle.store_id());
330 Ok(())
331 },
332 handle,
333 )
334 }
335
336 /// Writes the current store `T` from the provided `StoreHandle` to a file.
337 /// If the file does not exist, it creates a default store if a default is available.
338 ///
339 /// # Example
340 ///
341 /// ```
342 /// use rusty_store::{Storage, StoreHandle, Storing};
343 /// use serde::{Deserialize, Serialize};
344 ///
345 /// #[derive(Serialize, Deserialize, Default, Storing)]
346 /// pub struct MyStore {
347 /// pub count: u32,
348 /// }
349 ///
350 /// impl MyStore {
351 /// fn increment_count(&mut self) {
352 /// self.count += 1;
353 /// }
354 /// }
355 ///
356 /// let storage = Storage::new("APP_ID");
357 /// let mut handle: StoreHandle<MyStore> = StoreHandle::new("my_store_id");
358 ///
359 /// storage.write(&mut handle).expect("Failed to read store");
360 ///
361 /// ```
362 pub fn write<T: Storing>(&self, handle: &mut StoreHandle<T>) -> Result<(), StoreError> {
363 debug!("Writing store with id: {}", handle.store_id());
364 self.open_file::<T, _>(
365 |file: &mut File, handle| {
366 let store = handle.get_store_mut();
367
368 let str =
369 ron::ser::to_string_pretty(&store, PrettyConfig::new().compact_arrays(true))
370 .map_err(StoreError::Ron)?;
371
372 file.set_len(0).map_err(StoreError::Write)?;
373 file.write_all(str.as_bytes()).map_err(StoreError::Write)?;
374 file.flush().map_err(StoreError::Write)?;
375
376 info!("Successfully wrote store with id: {}", handle.store_id());
377 Ok(())
378 },
379 handle,
380 )
381 }
382
383 /// Opens the file for reading or writing. If the file does not exist, it attempts
384 /// to create a default store if a default is provided.
385 fn open_file<T, F>(
386 &self,
387 mut operation: F,
388 handle: &mut StoreHandle<T>,
389 ) -> Result<(), StoreError>
390 where
391 T: Storing,
392 F: FnMut(&mut File, &mut StoreHandle<T>) -> Result<(), StoreError>,
393 {
394 let mut dir_path = self.dir_path::<T>();
395 dir_path.push(handle.store_id()); // i don't like this
396
397 debug!("Opening file at path: {:?}", dir_path);
398
399 match OpenOptions::new()
400 .read(true)
401 .write(true)
402 .create(false)
403 .truncate(false)
404 .open(&dir_path)
405 {
406 Ok(mut config) => {
407 debug!("File opened successfully at path: {:?}", dir_path);
408 operation(&mut config, handle)
409 }
410 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
411 warn!(
412 "File not found at path: {:?}, creating default store",
413 dir_path
414 );
415 self.store_default::<T>(dir_path)?;
416 self.open_file(operation, handle)
417 }
418 Err(err) => {
419 warn!(
420 "Failed to open file at path: {:?}, error: {:?}",
421 dir_path, err
422 );
423 Err(StoreError::FileOpen(err))
424 }
425 }
426 }
427
428 fn store_default<T: Storing>(&self, path: PathBuf) -> Result<(), StoreError> {
429 debug!("Storing default configuration at path: {:?}", path);
430 if let Some(parent) = path.parent() {
431 fs::create_dir_all(parent).map_err(StoreError::CreateDir)?;
432 info!("Created directory for path: {:?}", parent);
433 }
434
435 let default_store = T::default();
436 let str = ron::ser::to_string_pretty(&default_store, PrettyConfig::new())
437 .map_err(StoreError::Ron)?;
438 fs::write(&path, str).map_err(StoreError::Write)?;
439 info!("Default store written at path: {:?}", &path);
440
441 Ok(())
442 }
443
444 fn dir_path<T: Storing>(&self) -> PathBuf {
445 let path = match T::store_type() {
446 StoringType::Cache => self.cache_dir.clone(),
447 StoringType::Data => self.data_dir.clone(),
448 StoringType::Config => self.config_dir.clone(),
449 StoringType::Custom(path) => path,
450 };
451 debug!(
452 "Resolved directory path for store type: {:?} to path: {:?}",
453 T::store_type(),
454 path
455 );
456 path
457 }
458
459 fn read_string(mut file: &File) -> Result<String, std::io::Error> {
460 let mut buf = String::new();
461 file.read_to_string(&mut buf)?;
462 debug!("Read string from file, length: {}", buf.len());
463 Ok(buf)
464 }
465}