preferences_serde1/
lib.rs

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