use std::{
fs, io,
path::PathBuf,
sync::{Arc, RwLock},
};
pub use serde::{Deserialize, Serialize, de::DeserializeOwned};
mod error;
mod shared;
pub use error::*;
pub use shared::*;
pub enum ConfigDirectory {
#[cfg(feature = "system-dirs")]
System(&'static str),
Custom(PathBuf),
}
#[derive(Debug, Clone)]
pub struct Config {
pub file: PathBuf,
pub path: PathBuf,
}
impl Config {
pub fn new(dir: ConfigDirectory) -> Result<Self, ConfigError> {
#[allow(clippy::infallible_destructuring_match)]
let config_path = match dir {
#[cfg(feature = "system-dirs")]
ConfigDirectory::System(app_name) => dirs::config_dir()
.map(|p| p.join(app_name))
.ok_or(ConfigError::SystemConfigNotFound)?,
ConfigDirectory::Custom(config_path) => config_path,
};
if !config_path.is_dir() {
fs::create_dir_all(&config_path).map_err(|err| ConfigError::Io {
path: config_path.clone(),
source: err,
})?;
}
Ok(Self {
file: config_path.join(PathBuf::from("config.toml")),
path: config_path,
})
}
pub fn set_file(&mut self, file: impl Into<PathBuf>) -> &mut Self {
self.file = self.path.join(file.into());
self
}
pub fn with_file(mut self, file: impl Into<PathBuf>) -> Self {
self.file = self.path.join(file.into());
self
}
pub fn read<T: DeserializeOwned>(&self) -> Result<T, ConfigError> {
let content = fs::read_to_string(&self.file).map_err(|err| ConfigError::Io {
path: self.file.clone(),
source: err,
})?;
Ok(toml::from_str::<T>(&content)?)
}
pub fn read_or_default<T: Serialize + DeserializeOwned + Default>(
&self,
) -> Result<T, ConfigError> {
match self.read::<T>() {
Ok(data) => Ok(data),
Err(ConfigError::Io { source, .. }) if source.kind() == io::ErrorKind::NotFound => {
let default_conf = T::default();
self.write(&default_conf)?;
Ok(default_conf)
}
Err(err) => Err(err),
}
}
pub fn write<T: Serialize>(&self, data: &T) -> Result<(), ConfigError> {
if let Some(parent) = &self.file.parent()
&& !parent.exists()
{
fs::create_dir_all(parent).map_err(|err| ConfigError::Io {
path: parent.to_path_buf(),
source: err,
})?;
}
let content = toml::to_string_pretty(data)?;
fs::write(&self.file, content).map_err(|err| ConfigError::Io {
path: self.file.clone(),
source: err,
})?;
Ok(())
}
pub fn load_shared<T: Serialize + DeserializeOwned>(
self,
) -> Result<SharedConfig<T>, ConfigError> {
let data = self.read::<T>()?;
Ok(SharedConfig {
data: Arc::new(RwLock::new(data)),
storage: Arc::new(self),
#[cfg(feature = "watcher")]
on_reload: Arc::new(RwLock::new(None)),
})
}
pub fn load_shared_or_default<T: Serialize + DeserializeOwned + Default>(
self,
) -> Result<SharedConfig<T>, ConfigError> {
let data = match self.read::<T>() {
Ok(data) => data,
Err(ConfigError::Io { source, .. }) if source.kind() == io::ErrorKind::NotFound => {
let default_conf = T::default();
self.write(&default_conf)?;
default_conf
}
Err(err) => return Err(err),
};
Ok(SharedConfig {
data: Arc::new(RwLock::new(data)),
storage: Arc::new(self),
#[cfg(feature = "watcher")]
on_reload: Arc::new(RwLock::new(None)),
})
}
}