use std::path::{Path, PathBuf};
use crate::model::{LoadError, Ruleset};
pub enum RuleSource {
Baked {
text: &'static str,
},
Disk {
path: PathBuf,
#[cfg(feature = "hot-reload")]
watcher: Option<watch::Watcher>,
},
}
impl RuleSource {
pub fn baked(text: &'static str) -> Self {
RuleSource::Baked { text }
}
pub fn from_path(path: impl Into<PathBuf>) -> Self {
let path = path.into();
#[cfg(feature = "hot-reload")]
{
let watcher = watch::Watcher::new(&path).ok();
RuleSource::Disk { path, watcher }
}
#[cfg(not(feature = "hot-reload"))]
{
RuleSource::Disk { path }
}
}
pub fn load(&self) -> Result<Ruleset, LoadError> {
match self {
RuleSource::Baked { text } => Ruleset::from_ron(text),
RuleSource::Disk { path, .. } => {
let text = read_to_string(path)?;
Ruleset::from_ron(&text)
}
}
}
pub fn poll_changed(&mut self) -> bool {
match self {
RuleSource::Baked { .. } => false,
#[cfg(feature = "hot-reload")]
RuleSource::Disk { watcher, .. } => {
watcher.as_mut().map(|w| w.poll_changed()).unwrap_or(false)
}
#[cfg(not(feature = "hot-reload"))]
RuleSource::Disk { .. } => false,
}
}
pub fn is_hot_reloadable(&self) -> bool {
match self {
RuleSource::Baked { .. } => false,
#[cfg(feature = "hot-reload")]
RuleSource::Disk { watcher, .. } => watcher.is_some(),
#[cfg(not(feature = "hot-reload"))]
RuleSource::Disk { .. } => false,
}
}
}
fn read_to_string(path: &Path) -> Result<String, LoadError> {
std::fs::read_to_string(path)
.map_err(|e| LoadError::Ron(format!("reading {}: {e}", path.display())))
}
#[cfg(feature = "hot-reload")]
pub mod watch {
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _};
pub struct Watcher {
_watcher: RecommendedWatcher,
rx: mpsc::Receiver<Result<notify::Event, notify::Error>>,
target: PathBuf,
}
impl Watcher {
pub fn new(path: &Path) -> Result<Self, String> {
let target = path.to_path_buf();
let dir = target
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let (tx, rx) = mpsc::channel();
let mut watcher = RecommendedWatcher::new(
move |res| {
let _ = tx.send(res);
},
Config::default(),
)
.map_err(|e| format!("failed to create rules.ron watcher: {e}"))?;
watcher
.watch(&dir, RecursiveMode::NonRecursive)
.map_err(|e| format!("failed to watch {}: {e}", dir.display()))?;
Ok(Self {
_watcher: watcher,
rx,
target,
})
}
pub fn poll_changed(&mut self) -> bool {
let mut changed = false;
while let Ok(Ok(event)) = self.rx.try_recv() {
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_))
&& event.paths.iter().any(|p| paths_match(p, &self.target))
{
changed = true;
}
}
changed
}
}
fn paths_match(event_path: &Path, target: &Path) -> bool {
if event_path == target {
return true;
}
match (event_path.canonicalize(), target.canonicalize()) {
(Ok(a), Ok(b)) if a == b => true,
_ => event_path.file_name() == target.file_name(),
}
}
}