cr_program_settings/
lib.rs

1//! `cr_program_state` is a library that simplifies saving a settings file for the program.
2#![warn(missing_docs)]
3
4/// Global settings file path list, paths are added when successfully loaded, or when successfully saved.
5pub static SETTINGS_PATHS: RwLock<Vec<PathBuf>> = RwLock::new(vec![]);
6
7use crate::LoadSettingsError::{DeserializationError, IOError};
8use serde::{Deserialize, Serialize};
9use std::fs::File;
10use std::io::{Error, Read, Write};
11use std::path::PathBuf;
12use std::sync::RwLock;
13use std::{fs, io};
14
15/// Prelude module that contains all the imports for `cr_program_settings`;
16pub mod prelude {
17    pub use crate::{
18        delete_setting_file, delete_settings, get_user_home, load_settings,
19        load_settings_with_filename, save_settings, save_settings_with_filename,
20        settings_container, SETTINGS_PATHS,
21    };
22}
23
24/// Source code for the settings container.
25pub mod settings_container;
26
27/// Returns the users home as an optional using the "home" crate
28pub fn get_user_home() -> Option<PathBuf> {
29    home::home_dir()
30}
31
32#[macro_export]
33/// Saves settings given a struct to save, to the home directory with a name matching the crate name
34///
35/// Syntax:
36///     save_settings!(settings_struct)
37///     save_settings!(settings_struct, file_name)
38///     save_settings!(settings_struct, file_name, folder_name)
39///
40/// ```
41/// use serde::{Deserialize, Serialize};
42/// use cr_program_settings::prelude::*;
43///
44/// // create a struct we want to save, it needs to implement at a minimum of Serialize and Deserialize
45/// #[derive(Serialize,Deserialize, PartialEq, Debug)]
46/// struct Settings{
47/// setting1: u32,
48/// setting2: String,
49/// setting3: Vec<bool>,
50/// }
51///
52/// let settings = Settings{
53///     setting1: 128,
54///     setting2: "this is a cool setting struct".to_string(),
55///     setting3: vec![false,true,false,false]
56/// };
57///
58/// save_settings!(settings).expect("Settings were unable to be saved");
59///
60/// // -- snip --
61///
62/// let loaded_settings = load_settings!(Settings).expect("Unable to read settings file");
63///
64/// assert_eq!(settings,loaded_settings);
65///
66/// save_settings!(settings,"cool_filename.ser").expect("Unable to save settings with specific filename");
67///
68/// // -- snip --
69///
70/// let specific_settings_loaded = load_settings!(Settings,"cool_filename.ser").expect("Unable to load settings with specific filename");
71///
72/// assert_eq!(settings,specific_settings_loaded);
73/// ```
74macro_rules! save_settings {
75    ($settings:expr) => {
76        save_settings(env!("CARGO_CRATE_NAME"), &$settings)
77    };
78    ($settings: expr, $file_name: expr) => {
79        save_settings_with_filename(env!("CARGO_CRATE_NAME"), &$file_name, &$settings)
80    };
81    ($settings: expr, $file_name: expr, $folder_name: expr) => {
82        save_settings_with_filename($folder_name, &$file_name, &$settings)
83    };
84}
85
86#[macro_export]
87/// Loads settings given a type to load, from the home directory with a name matching the crate name
88///
89/// Syntax:
90///     load_settings!(SETTINGS_TYPE)
91///     load_settings!(SETTINGS_TYPE, file_name)
92///     load_settings!(SETTINGS_TYPE, file_name,folder_name)
93///
94/// For more usage examples, see save_settings!() documentation.
95/// ```
96/// use serde::{Deserialize, Serialize};
97/// use cr_program_settings::prelude::*;
98///
99/// // create a struct we want to save, it needs to implement at a minimum of Serialize and Deserialize
100/// #[derive(Serialize,Deserialize, PartialEq, Debug)]
101/// struct Settings{
102/// setting1: u32,
103/// setting2: String,
104/// setting3: Vec<bool>,
105/// }
106///
107/// let settings = Settings{
108///     setting1: 128,
109///     setting2: "this is a cool setting struct".to_string(),
110///     setting3: vec![false,true,false,false]
111/// };
112///
113/// save_settings!(settings,"odd_file_name.ser","unit_test_temp").expect("Unable to save settings to file");
114///
115/// let loaded_settings = load_settings!(Settings, "odd_file_name.ser","unit_test_temp").expect("Failed to load settings file");
116///
117/// assert_eq!(settings,loaded_settings);
118/// ```
119macro_rules! load_settings {
120    ($setting_type:ty) => {
121        load_settings::<$setting_type>(env!("CARGO_CRATE_NAME"))
122    };
123    ($setting_type:ty,$file_name: expr) => {
124        load_settings_with_filename::<$setting_type>(env!("CARGO_CRATE_NAME"), $file_name)
125    };
126    ($setting_type:ty,$file_name: expr,$folder_name: expr) => {
127        load_settings_with_filename::<$setting_type>($folder_name, $file_name)
128    };
129}
130
131#[macro_export]
132/// Deletes settings located at the home directory with a name matching the crate name
133/// Syntax:
134///     delete_settings!() // deletes file named: env!("CARGO_CRATE_NAME") file stored in the folder named: env!("CARGO_CRATE_NAME")
135///     delete_settings!(file_name) // deletes the file named: file_name stored in the folder named: env!("CARGO_CRATE_NAME")
136///     delete_settings!(file_name, folder_name) // deletes the file named: file_name stored in the folder named: folder_name
137macro_rules! delete_settings {
138    () => {
139        delete_settings(env!("CARGO_CRATE_NAME"))
140    };
141    ($file_name: expr) => {
142        delete_setting_file(env!("CARGO_CRATE_NAME"), $file_name)
143    };
144    ($file_name: expr,$folder_name: expr) => {
145        delete_setting_file($folder_name, $file_name)
146    };
147}
148
149#[derive(Debug)]
150/// An enum state representing the kinds of errors that saving settings has
151pub enum SaveSettingsError {
152    /// The library was unable to find the users home directory
153    FailedToGetUserHome,
154    /// The library encountered an io error when saving or creating the file or directory
155    IOError(Error),
156    /// The library encountered an error while serializing the struct
157    SerializationError(toml::ser::Error),
158}
159
160/// Saves a serializable settings object to a given filename in `USER_HOME/crate_name/file_name`
161pub fn save_settings_with_filename<T>(
162    crate_name: &str,
163    file_name: &str,
164    settings: &T,
165) -> Result<(), SaveSettingsError>
166where
167    T: Serialize,
168{
169    match get_user_home() {
170        None => Err(SaveSettingsError::FailedToGetUserHome),
171        Some(home_dir) => {
172            let settings_path = home_dir.join(PathBuf::from(crate_name));
173            let settings_file_path = settings_path.join(PathBuf::from(file_name));
174            match fs::create_dir_all(&settings_path) {
175                Ok(_) => match File::create(&settings_file_path) {
176                    Ok(mut file) => match toml::to_string_pretty(&settings) {
177                        Ok(serialized_data) => match file.write_all(serialized_data.as_bytes()) {
178                            Ok(_) => {
179                                {
180                                    let mut lock = SETTINGS_PATHS.write().unwrap();
181                                    lock.push(settings_file_path);
182                                }
183                                Ok(())
184                            }
185                            Err(err) => Err(SaveSettingsError::IOError(err)),
186                        },
187                        Err(err) => Err(SaveSettingsError::SerializationError(err)),
188                    },
189                    Err(err) => Err(SaveSettingsError::IOError(err)),
190                },
191                Err(err) => Err(SaveSettingsError::IOError(err)),
192            }
193        }
194    }
195}
196
197/// Saves the settings file given in a directory named using the crate name
198/// Given a struct and a crate name of `my_cool_rust_project`, the program
199/// would save it to `/home/username/my_cool_rust_project/my_cool_rust_project.ser`
200pub fn save_settings<T>(crate_name: &str, settings: &T) -> Result<(), SaveSettingsError>
201where
202    T: Serialize,
203{
204    save_settings_with_filename(crate_name, format!("{}.ser", crate_name).as_str(), settings)
205}
206
207#[derive(Debug)]
208/// Enum state representing the possible errors that can occur when loading settings
209pub enum LoadSettingsError {
210    /// The library was unable to find the users home directory
211    FailedToGetUserHome,
212    /// The library encountered an io error while reading the file or accessing the directory
213    IOError(Error),
214    /// The library encountered an error while deserializing the settings file
215    DeserializationError(toml::de::Error),
216}
217
218/// Loads a settings serialized file from `USER_HOME/crate_name/file_name`
219pub fn load_settings_with_filename<T>(
220    crate_name: &str,
221    file_name: &str,
222) -> Result<T, LoadSettingsError>
223where
224    for<'a> T: Deserialize<'a>,
225{
226    match get_user_home() {
227        None => Err(LoadSettingsError::FailedToGetUserHome),
228        Some(home_dir) => {
229            let settings_path = home_dir.join(PathBuf::from(crate_name));
230            let settings_file_path = settings_path.join(PathBuf::from(file_name));
231            match File::open(&settings_file_path) {
232                Ok(mut file) => {
233                    let mut file_data = String::new();
234                    match file.read_to_string(&mut file_data) {
235                        Ok(_) => match toml::from_str::<T>(&file_data) {
236                            Ok(thing) => {
237                                {
238                                    let mut lock = SETTINGS_PATHS.write().unwrap();
239                                    if !lock.contains(&settings_file_path) {
240                                        lock.push(settings_file_path);
241                                    }
242                                }
243                                Ok(thing)
244                            }
245                            Err(err) => Err(DeserializationError(err)),
246                        },
247                        Err(err) => Err(IOError(err)),
248                    }
249                }
250                Err(err) => Err(IOError(err)),
251            }
252        }
253    }
254}
255
256/// Loads a given settings file from the home directory and the given crate name.
257/// Given `my_cool_rust_project`, the program would search in `/home/username/my_cool_rust_project` for a settings file
258pub fn load_settings<T>(crate_name: &str) -> Result<T, LoadSettingsError>
259where
260    for<'a> T: Deserialize<'a>,
261{
262    load_settings_with_filename(crate_name, format!("{}.ser", crate_name).as_str())
263}
264
265/// Deletes the settings directory found in the `<user home>/crate_name`
266/// e.g. `/home/username/my_cool_project`
267pub fn delete_settings(crate_name: &str) -> io::Result<()> {
268    let home_dir = get_user_home().unwrap();
269    let settings_path = home_dir.join(PathBuf::from(crate_name));
270    fs::remove_dir_all(&settings_path)?;
271    SETTINGS_PATHS
272        .write()
273        .unwrap()
274        .retain(|path| match path.parent() {
275            None => true,
276            Some(parent) => parent != settings_path,
277        });
278    Ok(())
279}
280
281/// Deletes a specific settings file
282/// ```
283/// use std::ffi::OsStr;
284/// use serde::{Deserialize, Serialize};
285/// use cr_program_settings::prelude::*;
286/// #[derive(Serialize,Deserialize)]
287/// struct TestStruct {field1: u32}
288///
289/// let s = TestStruct{field1: 6};
290///
291/// let sn = "settings_file_978.ser";
292/// save_settings!(s,sn);
293/// assert!(SETTINGS_PATHS.read().unwrap().iter().any(|path| {
294/// match path.file_name() {
295/// None => { false }
296/// Some(file_name) => { file_name == sn }
297/// }
298/// }));
299///
300/// delete_settings!(sn);
301/// assert!(!SETTINGS_PATHS.read().unwrap().iter().any(|path| {
302/// match path.file_name() {
303/// None => { false }
304/// Some(file_name) => { file_name == sn }
305/// }
306/// }));
307///
308///
309/// ```
310pub fn delete_setting_file(crate_name: &str, file_name: &str) -> io::Result<()> {
311    let home_dir = get_user_home().unwrap();
312    let settings_path = home_dir.join(PathBuf::from(crate_name));
313    let settings_file = settings_path.join(file_name);
314    fs::remove_file(&settings_file)?;
315    SETTINGS_PATHS
316        .write()
317        .unwrap()
318        .retain(|path| path != &settings_file);
319    Ok(())
320}