Skip to main content

easy_storage/
lib.rs

1//! # Examples
2//!
3//! ```
4//! use serde::{Deserialize, Serialize};
5//! use easy_storage::Storeable;
6//!
7//! #[derive(Debug, Serialize, Deserialize)]
8//! struct User {
9//!     name: String,
10//!     email: String,
11//! }
12//!
13//! impl Storeable for User {}
14//!
15//! let user = User {
16//!     name: "Alice".to_string(),
17//!     email: "alice@alice.com".to_string(),
18//! };
19//! let save_path = std::env::current_dir().unwrap().join("test").join("user.toml");
20//! match user.save_by_extension(&save_path, true) {
21//!     Ok(_) => println!("success."),
22//!     Err(e) => println!("Error: {e}"),
23//! }
24
25//! match User::load_by_extension(save_path) {
26//!     Ok(s) => println!("{s:?}"),
27//!     Err(e) => println!("Error: {e}"),
28//!     }
29//! //! ```
30
31use serde::{Serialize, de::DeserializeOwned};
32use std::{
33    fs::{self, OpenOptions},
34    io::Write,
35    path::Path,
36};
37use thiserror::Error;
38
39#[derive(Debug, Error)]
40pub enum Error {
41    #[error("IO error: {0}")]
42    IoE(#[from] std::io::Error),
43    #[error("serde error: {0}")]
44    JsonE(#[from] serde_json::Error),
45    #[error("parse toml error: {0}")]
46    ParTomlE(#[from] toml::ser::Error),
47    #[error("parse toml error: {0}")]
48    DesTomlE(#[from] toml::de::Error),
49    #[error("extension does not exist.")]
50    ExtensionDoesNotExist,
51}
52
53pub enum Format {
54    Json,
55    Toml,
56}
57
58fn path_to_format<P: AsRef<Path>>(path: P) -> Result<Format, Error> {
59    if let Some(v) = path.as_ref().extension().and_then(|f| f.to_str()) {
60        match v {
61            "json" => Ok(Format::Json),
62            "toml" => Ok(Format::Toml),
63            _ => Err(Error::ExtensionDoesNotExist),
64        }
65    } else {
66        Err(Error::ExtensionDoesNotExist)
67    }
68}
69
70pub trait Storeable: Serialize + DeserializeOwned + Sized {
71    /// Save to file.
72    ///
73    /// # Arguments
74    /// * `path` - A string slice that holds the path to the file.
75    /// * `new_create` - A boolean that indicates whether to create a new file if it does not exist.
76    /// * `format` - A `Format` enum that indicates the format to save the file in.
77    ///
78    /// # Returns
79    /// * `Result<(), Error>` - A `Result` enum that indicates whether the operation was successful.
80    fn save<P: AsRef<Path>>(&self, path: P, new_create: bool, format: Format) -> Result<(), Error> {
81        let s = match format {
82            Format::Json => serde_json::to_string_pretty(self)?,
83            Format::Toml => toml::to_string_pretty(self)?,
84        };
85
86        path.as_ref().parent().map(fs::create_dir_all).transpose()?;
87
88        let mut f = OpenOptions::new()
89            .write(true)
90            .truncate(true)
91            .create(new_create)
92            .open(path)?;
93
94        f.write_all(s.as_bytes())?;
95        Ok(())
96    }
97
98    /// save to file by extension of `path`
99    ///
100    /// supported extensions are `json` and `toml`.
101    ///
102    /// # Arguments
103    /// * `path` - path to the file.
104    /// * `new_create` - a boolean that indicates whether to create a new if it does not exist.
105    ///
106    /// # Returns
107    /// * `Result<(), Error>` - return errors if path does not include extension or include a not-supported extension or others reasons(io, fs, json(toml) parse).
108    fn save_by_extension<P: AsRef<Path>>(&self, path: P, new_create: bool) -> Result<(), Error> {
109        let format = path_to_format(&path)?;
110        self.save(path, new_create, format)
111    }
112
113    /// Load from file.
114    ///
115    /// # Arguments
116    /// * `path` - A string slice that holds the path to the file.
117    /// * `format` - A `Format` enum that indicates the format to load the file from.
118    ///
119    /// # Returns
120    /// * `Result<Self, Error>` - A `Result` enum that indicates whether the operation was successful.
121    fn load<P: AsRef<Path>>(path: P, format: Format) -> Result<Self, Error> {
122        let content = std::fs::read_to_string(path)?;
123        // return deserialized date
124        Ok(match format {
125            Format::Json => serde_json::from_str::<Self>(&content)?,
126            Format::Toml => toml::from_str::<Self>(&content)?,
127        })
128    }
129
130    /// load from file by extension of `path`
131    ///
132    /// supported extensions are `json` and `toml`
133    ///
134    /// # Arguments
135    ///
136    /// * `path` - path to load file.
137    ///
138    /// # Returns
139    /// * `Result<(), Error>` - return errors if path does not include extension or include a not-supported exttension or others reasons(io, fs, json(toml) parse).
140    fn load_by_extension<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
141        let format = path_to_format(&path)?;
142        Self::load(path, format)
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use crate::Storeable;
149    use serde::{Deserialize, Serialize};
150    use std::path::PathBuf;
151
152    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
153    struct User {
154        name: String,
155        email: String,
156    }
157
158    impl Storeable for User {}
159
160    fn ready_test_env() -> PathBuf {
161        let test_path = std::env::current_dir().unwrap().join("test");
162        if !test_path.exists() {
163            std::fs::create_dir_all(&test_path).unwrap();
164        }
165        test_path
166    }
167
168    #[test]
169    fn save_test() {
170        let test_f_path = ready_test_env();
171        let save_path = test_f_path.join("user.toml");
172        let user = User {
173            name: "Alice".to_string(),
174            email: "alice@alice.com".to_string(),
175        };
176        let res = user.save_by_extension(save_path, true);
177        if let Err(e) = &res {
178            eprintln!("{e}");
179        }
180        assert!(res.is_ok());
181    }
182
183    #[test]
184    fn load_test() {
185        let test_f_path = ready_test_env();
186        let save_path = test_f_path.join("user.toml");
187        let user = User {
188            name: "Alice".to_string(),
189            email: "alice@alice.com".to_string(),
190        };
191
192        let _ = dbg!(user.save_by_extension(&save_path, true));
193
194        let loaded = User::load_by_extension(save_path);
195        match loaded {
196            Ok(v) => assert_eq!(v, user),
197            Err(_) => assert!(loaded.is_ok()),
198        }
199    }
200}