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}