Skip to main content

configfs/
lib.rs

1//! # configfs
2//! A small lightweight filesystem config manager
3//! `configfs` provides an api to load, deserialize and write
4//! config files for your application.
5//! This currently only supports the TOML format, however I shall
6//! introduce more formats in the future.
7//!
8//! ## Usage
9//!
10//! ```rust
11//! use configfs::{Config, ConfigDirectory};
12//! use serde::{Deserialize, Serialize};
13//!
14//! #[derive(Debug, Serialize, Deserialize)]
15//! struct AppSettings {
16//!     port: u16,
17//!     verbose: bool
18//! }
19//! impl Default for AppSettings {
20//!     fn default() -> Self {
21//!         Self {
22//!             port: 9000,
23//!             verbose: true
24//!         }
25//!     }
26//! }
27//!
28//! fn main() -> Result<(), Box<dyn std::error::Error>> {
29//!     let dir = tempfile::tempdir()?;
30//!     let config = Config::new(ConfigDirectory::Custom(dir.path().to_path_buf()))?;
31//!     let settings = config.read_or_default::<AppSettings>()?;
32//!
33//!     if settings.verbose {
34//!         println!("using port: {}", settings.port);
35//!     }
36//!
37//!     Ok(())
38//! }
39//! ```
40//!
41//! ## Writing Config
42//!
43//! ```rust
44//! use configfs::{Config, ConfigDirectory};
45//! use serde::Serialize;
46//!
47//! #[derive(Serialize)]
48//! struct AppSettings {
49//!     username: String
50//! }
51//!
52//! fn main() -> Result<(), Box<dyn std::error::Error>> {
53//!     let dir = tempfile::tempdir()?;
54//!     let config = Config::new(ConfigDirectory::Custom(dir.path().to_path_buf()))?;
55//!     let settings = AppSettings {
56//!         username: "jimmy".into()
57//!     };
58//!
59//!     config.write(&settings)?;
60//!
61//!     Ok(())
62//! }
63//! ```
64
65use std::{
66    fs, io,
67    path::PathBuf,
68    sync::{Arc, RwLock},
69};
70
71pub use serde::{Deserialize, Serialize, de::DeserializeOwned};
72
73mod error;
74mod shared;
75
76pub use error::*;
77pub use shared::*;
78
79/// Target directory of the configuration files
80pub enum ConfigDirectory {
81    /// System default configuration directory (`~/.config/app_name`)
82    #[cfg(feature = "system-dirs")]
83    System(&'static str),
84
85    /// Custom file path
86    Custom(PathBuf),
87}
88
89#[derive(Debug, Clone)]
90pub struct Config {
91    /// Filename of the current configuration file (default `config.toml`)
92    pub file: PathBuf,
93    /// Directory that contains the configuration files
94    pub path: PathBuf,
95}
96
97impl Config {
98    pub fn new(dir: ConfigDirectory) -> Result<Self, ConfigError> {
99        #[allow(clippy::infallible_destructuring_match)]
100        let config_path = match dir {
101            #[cfg(feature = "system-dirs")]
102            ConfigDirectory::System(app_name) => dirs::config_dir()
103                .map(|p| p.join(app_name))
104                .ok_or(ConfigError::SystemConfigNotFound)?,
105
106            ConfigDirectory::Custom(config_path) => config_path,
107        };
108
109        if !config_path.is_dir() {
110            fs::create_dir_all(&config_path).map_err(|err| ConfigError::Io {
111                path: config_path.clone(),
112                source: err,
113            })?;
114        }
115
116        Ok(Self {
117            file: config_path.join(PathBuf::from("config.toml")),
118            path: config_path,
119        })
120    }
121
122    /// Changes the current configuration file
123    pub fn set_file(&mut self, file: impl Into<PathBuf>) -> &mut Self {
124        self.file = self.path.join(file.into());
125        self
126    }
127
128    /// Builder pattern to set the current configuration file
129    pub fn with_file(mut self, file: impl Into<PathBuf>) -> Self {
130        self.file = self.path.join(file.into());
131        self
132    }
133
134    /// Reads and deserializes the configuration file into type `T`
135    pub fn read<T: DeserializeOwned>(&self) -> Result<T, ConfigError> {
136        let content = fs::read_to_string(&self.file).map_err(|err| ConfigError::Io {
137            path: self.file.clone(),
138            source: err,
139        })?;
140
141        Ok(toml::from_str::<T>(&content)?)
142    }
143
144    /// Reads and deserializes the configuration file into type `T`. If missing config is written
145    /// and returns `T::default()`
146    pub fn read_or_default<T: Serialize + DeserializeOwned + Default>(
147        &self,
148    ) -> Result<T, ConfigError> {
149        match self.read::<T>() {
150            Ok(data) => Ok(data),
151            Err(ConfigError::Io { source, .. }) if source.kind() == io::ErrorKind::NotFound => {
152                let default_conf = T::default();
153                self.write(&default_conf)?;
154                Ok(default_conf)
155            }
156            Err(err) => Err(err),
157        }
158    }
159
160    /// Serializes and writes data `T` to disk as pretty TOML file.
161    pub fn write<T: Serialize>(&self, data: &T) -> Result<(), ConfigError> {
162        if let Some(parent) = &self.file.parent()
163            && !parent.exists()
164        {
165            fs::create_dir_all(parent).map_err(|err| ConfigError::Io {
166                path: parent.to_path_buf(),
167                source: err,
168            })?;
169        }
170
171        let content = toml::to_string_pretty(data)?;
172        fs::write(&self.file, content).map_err(|err| ConfigError::Io {
173            path: self.file.clone(),
174            source: err,
175        })?;
176
177        Ok(())
178    }
179
180    /// Loads the config and stores it in a thread safe `SharedConfig`
181    pub fn load_shared<T: Serialize + DeserializeOwned>(
182        self,
183    ) -> Result<SharedConfig<T>, ConfigError> {
184        let data = self.read::<T>()?;
185        Ok(SharedConfig {
186            data: Arc::new(RwLock::new(data)),
187            storage: Arc::new(self),
188
189            #[cfg(feature = "watcher")]
190            on_reload: Arc::new(RwLock::new(None)),
191        })
192    }
193
194    /// Loads the config, writes and returns `T::default()` if missing, and stores it in a thread safe `SharedConfig`
195    pub fn load_shared_or_default<T: Serialize + DeserializeOwned + Default>(
196        self,
197    ) -> Result<SharedConfig<T>, ConfigError> {
198        let data = match self.read::<T>() {
199            Ok(data) => data,
200            Err(ConfigError::Io { source, .. }) if source.kind() == io::ErrorKind::NotFound => {
201                let default_conf = T::default();
202                self.write(&default_conf)?;
203                default_conf
204            }
205            Err(err) => return Err(err),
206        };
207
208        Ok(SharedConfig {
209            data: Arc::new(RwLock::new(data)),
210            storage: Arc::new(self),
211
212            #[cfg(feature = "watcher")]
213            on_reload: Arc::new(RwLock::new(None)),
214        })
215    }
216}