1use notify::{
9 event::{Event, EventKind},
10 RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher,
11};
12use std::path::{Path, PathBuf};
13use thiserror::Error;
14use tracing::{debug, error, info, warn};
15
16pub mod config;
17
18pub use config::{ConfigWatcher, HotConfig};
19
20#[derive(Error, Debug)]
21pub enum WatchError {
22 #[error("Failed to watch path: {path}")]
23 WatchFailed {
24 path: PathBuf,
25 #[source]
26 source: notify::Error,
27 },
28
29 #[error("Failed to read file: {0}")]
30 ReadFailed(#[from] std::io::Error),
31
32 #[error("Failed to parse config: {0}")]
33 ParseFailed(String),
34
35 #[error("Invalid path: {0}")]
36 InvalidPath(PathBuf),
37}
38
39pub type Result<T> = std::result::Result<T, WatchError>;
40
41pub struct FileWatcher {
43 _watcher: RecommendedWatcher,
44 path: PathBuf,
45}
46
47impl FileWatcher {
48 pub fn new<P, F>(path: P, mut callback: F) -> Result<Self>
50 where
51 P: AsRef<Path>,
52 F: FnMut(&Path) + Send + 'static,
53 {
54 let path = path.as_ref().to_path_buf();
55
56 if !path.exists() {
57 warn!("Watch path does not exist yet: {:?}", path);
58 }
59
60 let watch_path = path.clone();
61 let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
62 match res {
63 Ok(event) => {
64 if matches!(
65 event.kind,
66 EventKind::Modify(_) | EventKind::Create(_)
67 ) {
68 debug!("File change detected: {:?}", event.paths);
69 for p in &event.paths {
70 callback(p);
71 }
72 }
73 }
74 Err(e) => error!("Watch error: {:?}", e),
75 }
76 })
77 .map_err(|e| WatchError::WatchFailed {
78 path: path.clone(),
79 source: e,
80 })?;
81
82 watcher
83 .watch(&watch_path, RecursiveMode::NonRecursive)
84 .map_err(|e| WatchError::WatchFailed {
85 path: path.clone(),
86 source: e,
87 })?;
88
89 info!("Watching file: {:?}", path);
90
91 Ok(Self {
92 _watcher: watcher,
93 path,
94 })
95 }
96
97 pub fn path(&self) -> &Path {
98 &self.path
99 }
100}
101
102pub fn watch<P, F>(path: P, callback: F) -> Result<FileWatcher>
104where
105 P: AsRef<Path>,
106 F: FnMut(&Path) + Send + 'static,
107{
108 FileWatcher::new(path, callback)
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use std::sync::atomic::{AtomicUsize, Ordering};
115 use std::sync::Arc;
116 use tempfile::NamedTempFile;
117
118 #[test]
119 fn test_watcher_creation() {
120 let temp = NamedTempFile::new().unwrap();
121 let count = Arc::new(AtomicUsize::new(0));
122 let count_clone = count.clone();
123
124 let _watcher = FileWatcher::new(temp.path(), move |_| {
125 count_clone.fetch_add(1, Ordering::SeqCst);
126 });
127
128 assert!(_watcher.is_ok());
130 }
131}