1use 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
79pub enum ConfigDirectory {
81 #[cfg(feature = "system-dirs")]
83 System(&'static str),
84
85 Custom(PathBuf),
87}
88
89#[derive(Debug, Clone)]
90pub struct Config {
91 pub file: PathBuf,
93 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 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 pub fn with_file(mut self, file: impl Into<PathBuf>) -> Self {
130 self.file = self.path.join(file.into());
131 self
132 }
133
134 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 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 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 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 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}