#[allow(unused_imports)]
use if_chain::if_chain;
use notify::{
event::{DataChange, ModifyKind},
Error, Event, EventKind, RecommendedWatcher, Watcher,
};
use serde::de::DeserializeOwned;
#[cfg(feature = "xml")]
use serde_xml_rs as serde_xml;
#[cfg(any(
feature = "json",
feature = "yaml",
feature = "toml",
feature = "xml",
feature = "ini"
))]
use std::fs;
use std::{
any::Any,
collections::HashMap,
path::{Path, PathBuf},
sync::{
mpsc::{channel, Receiver},
Arc, RwLock,
},
time::Duration,
};
#[cfg(feature = "toml")]
use toml as serde_toml;
#[derive(Clone, Debug)]
pub enum Format {
#[cfg(feature = "json")]
Json,
#[cfg(feature = "yaml")]
Yaml,
#[cfg(feature = "toml")]
Toml,
#[cfg(feature = "xml")]
Xml,
#[cfg(feature = "ini")]
Ini,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
pub struct ConfigId(String);
impl ConfigId {
pub fn new<S: Into<String>>(id: S) -> Self {
Self(id.into())
}
}
struct Config {
value: Box<dyn Any + Send + Sync>,
_watcher: RecommendedWatcher,
}
#[derive(Clone)]
pub struct Reloadify(Arc<RwLock<HashMap<ConfigId, Config>>>);
#[derive(Debug, thiserror::Error)]
pub enum ReloadifyError {
#[error("Failed to acquire lock")]
GetLockError,
#[error("Failed to load config: {0}")]
LoadConfigError(#[from] std::io::Error),
#[error("Failed to deserialize config: {0}")]
DeserializeError(String),
#[error("Failed to watch: {0}")]
WatchError(#[from] notify::Error),
#[error("Failed to downcast")]
DowncastError,
#[error("Config does not exist")]
ConfigNotExist,
#[error("Failed to send config")]
SendError,
}
#[derive(Debug, Clone)]
pub struct ReloadableConfig {
pub id: ConfigId,
pub path: PathBuf,
pub format: Format,
pub poll_interval: Duration,
}
impl Reloadify {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self(Arc::new(RwLock::new(HashMap::new())))
}
#[allow(unreachable_code, unused_variables)]
pub fn add<C>(&self, reloadable_config: ReloadableConfig) -> Result<Receiver<C>, ReloadifyError>
where
C: DeserializeOwned + Send + Sync + Clone + 'static,
{
let initial_cfg =
self.load::<C>(reloadable_config.path.as_path(), &reloadable_config.format)?;
let (tx, rx) = channel();
tx.send(initial_cfg.clone())
.map_err(|_| ReloadifyError::SendError)?;
let c = reloadable_config.clone();
let s = self.clone();
let mut watcher = RecommendedWatcher::new(
move |r: Result<Event, Error>| {
if_chain!(
if let Ok(event) = r;
if let EventKind::Modify(ModifyKind::Data(chg)) = event.kind;
if chg == DataChange::Content;
if let Ok(latest_cfg) = s.load::<C>(c.path.as_path(), &c.format);
if let Ok(mut guard) = s.0.write();
if let Some(current_cfg) = guard.get_mut(&c.id);
then {
current_cfg.value = Box::new(latest_cfg.clone());
let _ = tx.send(latest_cfg);
}
);
},
notify::Config::default().with_poll_interval(reloadable_config.poll_interval),
)
.map_err(ReloadifyError::WatchError)?;
watcher
.watch(
reloadable_config.path.as_path(),
notify::RecursiveMode::NonRecursive,
)
.map_err(ReloadifyError::WatchError)?;
let mut guard = self.0.write().map_err(|_| ReloadifyError::GetLockError)?;
guard.entry(reloadable_config.id).or_insert(Config {
value: Box::new(initial_cfg),
_watcher: watcher,
});
Ok(rx)
}
pub fn get<C>(&self, config_id: ConfigId) -> Result<C, ReloadifyError>
where
C: DeserializeOwned + Send + Sync + Clone + 'static,
{
match self.0.read() {
Err(_) => Err(ReloadifyError::GetLockError),
Ok(guard) => Ok(guard
.get(&config_id)
.ok_or_else(|| ReloadifyError::ConfigNotExist)?
.value
.downcast_ref::<C>()
.cloned()
.ok_or_else(|| ReloadifyError::DowncastError)?),
}
}
#[allow(unused_variables)]
fn load<C: DeserializeOwned>(&self, path: &Path, format: &Format) -> Result<C, ReloadifyError> {
#[cfg(any(
feature = "json",
feature = "yaml",
feature = "toml",
feature = "xml",
feature = "ini"
))]
{
let content = fs::read_to_string(path).map_err(ReloadifyError::LoadConfigError)?;
match format {
#[cfg(feature = "json")]
Format::Json => serde_json::from_str::<C>(&content)
.map_err(|err| ReloadifyError::DeserializeError(err.to_string())),
#[cfg(feature = "yaml")]
Format::Yaml => serde_yaml::from_str::<C>(&content)
.map_err(|err| ReloadifyError::DeserializeError(err.to_string())),
#[cfg(feature = "toml")]
Format::Toml => serde_toml::from_str::<C>(&content)
.map_err(|err| ReloadifyError::DeserializeError(err.to_string())),
#[cfg(feature = "xml")]
Format::Xml => serde_xml::from_str::<C>(&content)
.map_err(|err| ReloadifyError::DeserializeError(err.to_string())),
#[cfg(feature = "ini")]
Format::Ini => serde_ini::from_str::<C>(&content)
.map_err(|err| ReloadifyError::DeserializeError(err.to_string())),
}
}
#[cfg(not(any(
feature = "json",
feature = "yaml",
feature = "toml",
feature = "xml",
feature = "ini"
)))]
Err(ReloadifyError::DeserializeError(
"No format feature enabled".to_string(),
))
}
}