preferences/
lib.rs

1#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
2#![allow(clippy::enum_glob_use)]
3//! *Read and write user-specific application data*
4//!
5//! This crate allows Rust developers to store and retrieve user-local preferences and other
6//! application data in a flexible and platform-appropriate way.
7//!
8//! Though it was originally inspired by Java's convenient
9//! [Preferences API](https://docs.oracle.com/javase/8/docs/api/java/util/prefs/Preferences.html),
10//! this crate is more flexible. *Any* struct or enum that implements
11//! [`serde`][serde-api]'s `Serialize` and `Deserialize`
12//! traits can be stored and retrieved as user data. Implementing those traits is
13//! trivial with the `#[derive(Serialize, Deserialize)]` attribute.
14//!
15//! # Usage
16//! For convenience, the type [`PreferencesMap<T>`](type.PreferencesMap.html) is provided. (It's
17//! actually just [`std::collections::HashMap<String, T>`][hashmap-api], where `T` defaults to
18//! `String`). This mirrors the Java API, which models user data as an opaque key-value store. As
19//! long as  `T` is serializable and deserializable, [`Preferences`](trait.Preferences.html)
20//! will be implemented for your map instance. This allows you to seamlessly save and load
21//! user data with the `save(..)` and `load(..)` trait methods from `Preferences`.
22//!
23//! # Basic example
24//! ```
25//! extern crate preferences;
26//! use preferences::{AppInfo, PreferencesMap, Preferences};
27//!
28//! const APP_INFO: AppInfo = AppInfo{name: "preferences", author: "Rust language community"};
29//!
30//! fn main() {
31//!
32//!     // Create a new preferences key-value map
33//!     // (Under the hood: HashMap<String, String>)
34//!     let mut faves: PreferencesMap<String> = PreferencesMap::new();
35//!
36//!     // Edit the preferences (std::collections::HashMap)
37//!     faves.insert("color".into(), "blue".into());
38//!     faves.insert("programming language".into(), "Rust".into());
39//!
40//!     // Store the user's preferences
41//!     let prefs_key = "tests/docs/basic-example";
42//!     let save_result = faves.save(&APP_INFO, prefs_key);
43//!     assert!(save_result.is_ok());
44//!
45//!     // ... Then do some stuff ...
46//!
47//!     // Retrieve the user's preferences
48//!     let load_result = PreferencesMap::<String>::load(&APP_INFO, prefs_key);
49//!     assert!(load_result.is_ok());
50//!     assert_eq!(load_result.unwrap(), faves);
51//!
52//! }
53//! ```
54//!
55//! # Using custom data types
56//! ```
57//! extern crate preferences;
58//! extern crate serde;
59//! use preferences::{AppInfo, Preferences};
60//! use serde::{Serialize, Deserialize};
61//!
62//! const APP_INFO: AppInfo = AppInfo{name: "preferences", author: "Rust language community"};
63//!
64//! // Deriving `Serialize` and `Deserialize` on a struct/enum automatically implements
65//! // the `Preferences` trait.
66//! #[derive(Serialize, Deserialize, PartialEq, Debug)]
67//! struct PlayerData {
68//!     level: u32,
69//!     health: f32,
70//! }
71//!
72//! fn main() {
73//!
74//!     let player = PlayerData{level: 2, health: 0.75};
75//!
76//!     let prefs_key = "tests/docs/custom-types";
77//!     let save_result = player.save(&APP_INFO, prefs_key);
78//!     assert!(save_result.is_ok());
79//!
80//!     // Method `load` is from trait `Preferences`.
81//!     let load_result = PlayerData::load(&APP_INFO, prefs_key);
82//!     assert!(load_result.is_ok());
83//!     assert_eq!(load_result.unwrap(), player);
84//!
85//! }
86//! ```
87//!
88//! # Using custom data types with `PreferencesMap`
89//! ```
90//! extern crate preferences;
91//! extern crate serde;
92//! use preferences::{AppInfo, PreferencesMap, Preferences};
93//! use serde::{Serialize, Deserialize};
94//!
95//! const APP_INFO: AppInfo = AppInfo{name: "preferences", author: "Rust language community"};
96//!
97//! #[derive(Serialize, Deserialize, PartialEq, Debug)]
98//! struct Point(f32, f32);
99//!
100//! fn main() {
101//!
102//!     let mut places = PreferencesMap::new();
103//!     places.insert("treasure".into(), Point(1.0, 1.0));
104//!     places.insert("home".into(), Point(-1.0, 6.6));
105//!
106//!     let prefs_key = "tests/docs/custom-types-with-preferences-map";
107//!     let save_result = places.save(&APP_INFO, prefs_key);
108//!     assert!(save_result.is_ok());
109//!
110//!     let load_result = PreferencesMap::load(&APP_INFO, prefs_key);
111//!     assert!(load_result.is_ok());
112//!     assert_eq!(load_result.unwrap(), places);
113//!
114//! }
115//! ```
116//!
117//! # Using custom data types with serializable containers
118//! ```
119//! extern crate preferences;
120//! extern crate serde;
121//! use preferences::{AppInfo, Preferences};
122//! use serde::{Serialize, Deserialize};
123//!
124//! const APP_INFO: AppInfo = AppInfo{name: "preferences", author: "Rust language community"};
125//!
126//! #[derive(Serialize, Deserialize, PartialEq, Debug)]
127//! struct Point(usize, usize);
128//!
129//! fn main() {
130//!
131//!     let square = vec![
132//!         Point(0,0),
133//!         Point(1,0),
134//!         Point(1,1),
135//!         Point(0,1),
136//!     ];
137//!
138//!     let prefs_key = "tests/docs/custom-types-in-containers";
139//!     let save_result = square.save(&APP_INFO, prefs_key);
140//!     assert!(save_result.is_ok());
141//!
142//!     let load_result = Vec::<Point>::load(&APP_INFO, prefs_key);
143//!     assert!(load_result.is_ok());
144//!     assert_eq!(load_result.unwrap(), square);
145//!
146//! }
147//! ```
148//!
149//! # Under the hood
150//! Data is written to flat files under the active user's home directory in a location specific to
151//! the operating system. This location is decided by the `app_dirs` crate with the data type
152//! `UserConfig`. Within the data directory, the files are stored in a folder hierarchy that maps
153//! to a sanitized version of the preferences key passed to `save(..)`.
154//!
155//! The data is stored in JSON format. This has several advantages:
156//!
157//! * Human-readable and self-describing
158//! * More compact than e.g. XML
159//! * Better adoption rates and language compatibility than e.g. TOML
160//! * Not reliant on a consistent memory layout like e.g. binary
161//!
162//! You could, of course, implement `Preferences` yourself and store your user data in
163//! whatever location and format that you wanted. But that would defeat the purpose of this
164//! library. &#128522;
165//!
166//! [hashmap-api]: https://doc.rust-lang.org/std/collections/struct.HashMap.html
167//! [serde-api]: https://crates.io/crates/serde
168
169#![warn(missing_docs)]
170
171extern crate app_dirs;
172extern crate serde;
173extern crate serde_json;
174
175use app_dirs::{get_app_dir, get_data_root, AppDataType};
176pub use app_dirs::{AppDirsError, AppInfo};
177use serde::de::DeserializeOwned;
178use serde::Serialize;
179use std::collections::HashMap;
180use std::ffi::OsString;
181use std::fmt;
182use std::fs::{create_dir_all, File};
183use std::io::{self, ErrorKind, Read, Write};
184use std::path::PathBuf;
185use std::string::FromUtf8Error;
186
187const DATA_TYPE: AppDataType = AppDataType::UserConfig;
188static PREFS_FILE_EXTENSION: &str = ".prefs.json";
189static DEFAULT_PREFS_FILENAME: &str = "prefs.json";
190
191/// Generic key-value store for user data.
192///
193/// This is actually a wrapper type around [`std::collections::HashMap<String, T>`][hashmap-api]
194/// (with `T` defaulting to `String`), so use the `HashMap` API methods to access and change user
195/// data in memory.
196///
197/// To save or load user data, use the methods defined for the trait
198/// [`Preferences`](trait.Preferences.html), which will be automatically implemented for
199/// `PreferencesMap<T>` as long as `T` is serializable. (See the
200/// [module documentation](index.html) for examples and more details.)
201///
202/// [hashmap-api]: https://doc.rust-lang.org/std/collections/struct.HashMap.html
203pub type PreferencesMap<T = String> = HashMap<String, T>;
204
205/// Error type representing the errors that can occur when saving or loading user data.
206#[derive(Debug)]
207pub enum PreferencesError {
208    /// An error occurred during JSON serialization or deserialization.
209    Json(serde_json::Error),
210    /// An error occurred during preferences file I/O.
211    Io(io::Error),
212    /// Couldn't figure out where to put or find the serialized data.
213    Directory(AppDirsError),
214}
215
216impl fmt::Display for PreferencesError {
217    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
218        use PreferencesError::*;
219        match *self {
220            Json(ref e) => e.fmt(f),
221            Io(ref e) => e.fmt(f),
222            Directory(ref e) => e.fmt(f),
223        }
224    }
225}
226
227impl std::error::Error for PreferencesError {
228    fn cause(&self) -> Option<&dyn std::error::Error> {
229        use PreferencesError::*;
230        Some(match *self {
231            Json(ref e) => e,
232            Io(ref e) => e,
233            Directory(ref e) => e,
234        })
235    }
236}
237
238impl From<serde_json::Error> for PreferencesError {
239    fn from(e: serde_json::Error) -> Self {
240        Self::Json(e)
241    }
242}
243
244impl From<FromUtf8Error> for PreferencesError {
245    fn from(_: FromUtf8Error) -> Self {
246        let kind = ErrorKind::InvalidData;
247        let msg = "Preferences file contained invalid UTF-8";
248        let err = io::Error::new(kind, msg);
249        Self::Io(err)
250    }
251}
252
253impl From<std::io::Error> for PreferencesError {
254    fn from(e: std::io::Error) -> Self {
255        Self::Io(e)
256    }
257}
258
259impl From<AppDirsError> for PreferencesError {
260    fn from(e: AppDirsError) -> Self {
261        Self::Directory(e)
262    }
263}
264
265/// Trait for types that can be saved & loaded as user data.
266///
267/// This type is automatically implemented for any struct/enum `T` which implements both
268/// `Serialize` and `Deserialize` (from `serde`). (Trivially, you can annotate the type
269/// with `#[derive(Serialize, Deserialize)`). It is encouraged to use the provided
270/// type, [`PreferencesMap`](type.PreferencesMap.html), to bundle related user preferences.
271///
272/// For the `app` parameter of `save(..)` and `load(..)`, it's recommended that you use a single
273/// `const` instance of `AppInfo` that represents your program:
274///
275/// ```
276/// use preferences::AppInfo;
277/// const APP_INFO: AppInfo = AppInfo{name: "Awesome App", author: "Dedicated Dev"};
278/// ```
279///
280/// The `key` parameter of `save(..)` and `load(..)` should be used to uniquely identify different
281/// preferences data. It roughly maps to a platform-dependent directory hierarchy, with forward
282/// slashes used as separators on all platforms. Keys are sanitized to be valid paths; to ensure
283/// human-readable paths, use only letters, digits, spaces, hyphens, underscores, periods, and
284/// slashes.
285///
286/// # Example keys
287/// * `options/graphics`
288/// * `saves/quicksave`
289/// * `bookmarks/favorites`
290pub trait Preferences: Sized {
291    /// Saves the current state of this object. Implementation is platform-dependent, but the data
292    /// will be local to the active user.
293    ///
294    /// # Errors
295    /// If a serialization or file I/O error (e.g. permission denied) occurs.
296    fn save<S: AsRef<str>>(&self, app: &AppInfo, key: S) -> Result<(), PreferencesError>;
297    /// Loads this object's state from previously saved user data with the same `key`. This is
298    /// an instance method which completely overwrites the object's state with the serialized
299    /// data. Thus, it is recommended that you call this method immediately after instantiating
300    /// the preferences object.
301    ///
302    /// # Errors
303    /// If a deserialization or file I/O error (e.g. permission denied) occurs, or if no user data
304    /// exists at that `path`.
305    fn load<S: AsRef<str>>(app: &AppInfo, key: S) -> Result<Self, PreferencesError>;
306    /// Same as `save`, but writes the serialized preferences to an arbitrary writer.
307    ///
308    /// # Errors
309    /// If a write or serialization error occurs.
310    fn save_to<W: Write>(&self, writer: &mut W) -> Result<(), PreferencesError>;
311    /// Same as `load`, but reads the serialized preferences from an arbitrary writer.
312    ///
313    /// # Errors
314    /// If a read or deserialization error occurs.
315    fn load_from<R: Read>(reader: &mut R) -> Result<Self, PreferencesError>;
316}
317
318fn compute_file_path<S: AsRef<str>>(app: &AppInfo, key: S) -> Result<PathBuf, PreferencesError> {
319    let mut path = get_app_dir(DATA_TYPE, app, key.as_ref())?;
320    let new_name = match path.file_name() {
321        Some(name) if !name.is_empty() => {
322            let mut new_name = OsString::with_capacity(name.len() + PREFS_FILE_EXTENSION.len());
323            new_name.push(name);
324            new_name.push(PREFS_FILE_EXTENSION);
325            new_name
326        }
327        _ => DEFAULT_PREFS_FILENAME.into(),
328    };
329    path.set_file_name(new_name);
330    Ok(path)
331}
332
333impl<T> Preferences for T
334where
335    T: Serialize + DeserializeOwned + Sized,
336{
337    fn save<S>(&self, app: &AppInfo, key: S) -> Result<(), PreferencesError>
338    where
339        S: AsRef<str>,
340    {
341        let path = compute_file_path(app, key.as_ref())?;
342        path.parent().map(create_dir_all);
343        let mut file = File::create(path)?;
344        self.save_to(&mut file)
345    }
346    fn load<S: AsRef<str>>(app: &AppInfo, key: S) -> Result<Self, PreferencesError> {
347        let path = compute_file_path(app, key.as_ref())?;
348        let mut file = File::open(path)?;
349        Self::load_from(&mut file)
350    }
351    fn save_to<W: Write>(&self, writer: &mut W) -> Result<(), PreferencesError> {
352        serde_json::to_writer(writer, self).map_err(Into::into)
353    }
354    fn load_from<R: Read>(reader: &mut R) -> Result<Self, PreferencesError> {
355        serde_json::from_reader(reader).map_err(Into::into)
356    }
357}
358
359/// Get full path to the base directory for preferences.
360///
361/// This makes no guarantees that the specified directory path actually *exists* (though you can
362/// easily use `std::fs::create_dir_all(..)`). Returns `None` if the directory cannot be determined
363/// or is not available on the current platform.
364#[must_use]
365pub fn prefs_base_dir() -> Option<PathBuf> {
366    get_data_root(AppDataType::UserConfig).ok()
367}
368
369#[cfg(test)]
370mod tests {
371    use super::{AppInfo, Preferences, PreferencesMap};
372    const APP_INFO: AppInfo = AppInfo {
373        name: "preferences",
374        author: "Rust language community",
375    };
376    const TEST_PREFIX: &str = "tests/module";
377    fn gen_test_name(name: &str) -> String {
378        TEST_PREFIX.to_owned() + "/" + name
379    }
380    fn gen_sample_prefs() -> PreferencesMap<String> {
381        let mut prefs = PreferencesMap::new();
382        prefs.insert("foo".into(), "bar".into());
383        prefs.insert("age".into(), "23".into());
384        prefs.insert("PI".into(), "3.14".into());
385        prefs.insert("offset".into(), "-9".into());
386        prefs
387    }
388    #[test]
389    fn test_save_load() {
390        let sample_map = gen_sample_prefs();
391        let sample_other: i32 = 4;
392        let name_map = gen_test_name("save-load-map");
393        let name_other = gen_test_name("save-load-other");
394        let save_map_result = sample_map.save(&APP_INFO, &name_map);
395        let save_other_result = sample_other.save(&APP_INFO, &name_other);
396        assert!(save_map_result.is_ok());
397        assert!(save_other_result.is_ok());
398        let load_map_result = PreferencesMap::load(&APP_INFO, &name_map);
399        let load_other_result = i32::load(&APP_INFO, &name_other);
400        assert!(load_map_result.is_ok());
401        assert!(load_other_result.is_ok());
402        assert_eq!(load_map_result.unwrap(), sample_map);
403        assert_eq!(load_other_result.unwrap(), sample_other);
404    }
405}