Skip to main content

rust_serv/config_reloader/
watcher.rs

1//! Configuration file watcher
2
3use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
4use std::path::Path;
5use std::sync::mpsc::{channel, Receiver};
6
7/// Watches configuration files for changes
8pub struct ConfigWatcher {
9    watcher: RecommendedWatcher,
10    rx: Receiver<ConfigEvent>,
11}
12
13/// Configuration change events
14#[derive(Debug, Clone)]
15pub enum ConfigEvent {
16    /// Configuration file changed
17    Changed(String),
18    /// Configuration file removed
19    Removed(String),
20    /// Watcher error
21    Error(String),
22}
23
24impl ConfigWatcher {
25    /// Create a new config watcher for a file path
26    pub fn new<P: AsRef<Path>>(_path: P) -> Result<Self, Box<dyn std::error::Error>> {
27        let (tx, rx) = channel();
28        
29        let watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
30            match res {
31                Ok(event) => {
32                    for path in event.paths {
33                        if let Some(path_str) = path.to_str() {
34                            let event_type = match event.kind {
35                                notify::EventKind::Modify(_) => ConfigEvent::Changed(path_str.to_string()),
36                                notify::EventKind::Remove(_) => ConfigEvent::Removed(path_str.to_string()),
37                                _ => continue,
38                            };
39                            let _ = tx.send(event_type);
40                        }
41                    }
42                }
43                Err(e) => {
44                    let _ = tx.send(ConfigEvent::Error(e.to_string()));
45                }
46            }
47        })?;
48        
49        Ok(Self { watcher, rx })
50    }
51    
52    /// Start watching a path
53    pub fn watch<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Box<dyn std::error::Error>> {
54        self.watcher.watch(path.as_ref(), RecursiveMode::NonRecursive)?;
55        Ok(())
56    }
57    
58    /// Stop watching a path
59    pub fn unwatch<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Box<dyn std::error::Error>> {
60        self.watcher.unwatch(path.as_ref())?;
61        Ok(())
62    }
63    
64    /// Check for events (non-blocking)
65    pub fn try_recv(&self) -> Option<ConfigEvent> {
66        match self.rx.try_recv() {
67            Ok(event) => Some(event),
68            Err(_) => None,
69        }
70    }
71    
72    /// Wait for next event (blocking)
73    pub fn recv(&self) -> Option<ConfigEvent> {
74        match self.rx.recv() {
75            Ok(event) => Some(event),
76            Err(_) => None,
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::fs;
85    use std::io::Write;
86    use tempfile::TempDir;
87    
88    #[test]
89    fn test_watcher_creation() {
90        let dir = TempDir::new().unwrap();
91        let config_path = dir.path().join("config.toml");
92        fs::write(&config_path, "port = 8080").unwrap();
93        
94        let result = ConfigWatcher::new(&config_path);
95        assert!(result.is_ok());
96    }
97    
98    #[test]
99    fn test_watcher_watch_unwatch() {
100        let dir = TempDir::new().unwrap();
101        let config_path = dir.path().join("config.toml");
102        fs::write(&config_path, "port = 8080").unwrap();
103        
104        let mut watcher = ConfigWatcher::new(&config_path).unwrap();
105        
106        // Should be able to watch the file
107        assert!(watcher.watch(&config_path).is_ok());
108        
109        // Should be able to unwatch
110        assert!(watcher.unwatch(&config_path).is_ok());
111    }
112
113    #[test]
114    fn test_watcher_try_recv_no_event() {
115        let dir = TempDir::new().unwrap();
116        let config_path = dir.path().join("config.toml");
117        fs::write(&config_path, "port = 8080").unwrap();
118        
119        let mut watcher = ConfigWatcher::new(&config_path).unwrap();
120        watcher.watch(&config_path).unwrap();
121        
122        // No events yet
123        let result = watcher.try_recv();
124        assert!(result.is_none());
125    }
126
127    #[test]
128    fn test_watcher_nonexistent_file() {
129        let dir = TempDir::new().unwrap();
130        let config_path = dir.path().join("nonexistent.toml");
131        
132        // Should still be able to create watcher even if file doesn't exist
133        let result = ConfigWatcher::new(&config_path);
134        assert!(result.is_ok());
135    }
136
137    #[test]
138    fn test_watcher_recv_timeout() {
139        let dir = TempDir::new().unwrap();
140        let config_path = dir.path().join("config.toml");
141        fs::write(&config_path, "port = 8080").unwrap();
142        
143        let mut watcher = ConfigWatcher::new(&config_path).unwrap();
144        watcher.watch(&config_path).unwrap();
145        
146        // Use try_recv which is non-blocking
147        let result = watcher.try_recv();
148        // Initially no events
149        assert!(result.is_none());
150    }
151
152    #[test] 
153    fn test_config_event_debug() {
154        let event = ConfigEvent::Changed("/test/path.toml".to_string());
155        let debug_str = format!("{:?}", event);
156        assert!(debug_str.contains("Changed"));
157        
158        let event = ConfigEvent::Removed("/test/path.toml".to_string());
159        let debug_str = format!("{:?}", event);
160        assert!(debug_str.contains("Removed"));
161        
162        let event = ConfigEvent::Error("test error".to_string());
163        let debug_str = format!("{:?}", event);
164        assert!(debug_str.contains("Error"));
165    }
166}