1use std::path::PathBuf;
16
17#[cfg(feature = "watch")]
18use notify::{
19 Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher,
20};
21#[cfg(feature = "watch")]
22use std::path::Path;
23#[cfg(feature = "watch")]
24use std::sync::mpsc::{Receiver, Sender, channel};
25#[cfg(feature = "watch")]
26use std::time::Duration;
27
28#[derive(Debug, Clone)]
30pub enum FileChange {
31 ConfigChanged(PathBuf),
33 RuleFileChanged(PathBuf),
35 RuleFileCreated(PathBuf),
37 RuleFileDeleted(PathBuf),
39}
40#[cfg(feature = "watch")]
42pub struct DxWatcher {
43 watcher: RecommendedWatcher,
44 receiver: Receiver<FileChange>,
45 watched_paths: Vec<PathBuf>,
46}
47
48#[cfg(feature = "watch")]
49impl DxWatcher {
50 pub fn new(debounce_ms: u64) -> Result<Self, notify::Error> {
67 let (tx, rx) = channel();
68
69 let watcher = Self::create_watcher(tx, debounce_ms)?;
70
71 Ok(Self {
72 watcher,
73 receiver: rx,
74 watched_paths: Vec::new(),
75 })
76 }
77
78 fn create_watcher(
80 tx: Sender<FileChange>,
81 debounce_ms: u64,
82 ) -> Result<RecommendedWatcher, notify::Error> {
83 let mut config = Config::default();
84 config = config.with_poll_interval(Duration::from_millis(debounce_ms));
85
86 RecommendedWatcher::new(
87 move |res: Result<Event, notify::Error>| {
88 if let Ok(event) = res {
89 if let Some(change) = Self::process_event(event) {
90 let _ = tx.send(change);
91 }
92 }
93 },
94 config,
95 )
96 }
97
98 fn process_event(event: Event) -> Option<FileChange> {
100 let path = event.paths.first()?;
101
102 if path.file_name()?.to_str()? == "dx" {
104 return Some(match event.kind {
105 EventKind::Create(_) | EventKind::Modify(_) => {
106 FileChange::ConfigChanged(path.clone())
107 }
108 _ => return None,
109 });
110 }
111
112 if path.extension()?.to_str()? == "sr" {
114 return Some(match event.kind {
115 EventKind::Create(_) => FileChange::RuleFileCreated(path.clone()),
116 EventKind::Modify(_) => FileChange::RuleFileChanged(path.clone()),
117 EventKind::Remove(_) => FileChange::RuleFileDeleted(path.clone()),
118 _ => return None,
119 });
120 }
121
122 None
123 }
124
125 pub fn watch_directory<P: AsRef<Path>>(&mut self, path: P) -> Result<(), notify::Error> {
129 let path = path.as_ref();
130 self.watcher.watch(path, RecursiveMode::Recursive)?;
131 self.watched_paths.push(path.to_path_buf());
132 Ok(())
133 }
134
135 pub fn watch_file<P: AsRef<Path>>(&mut self, path: P) -> Result<(), notify::Error> {
137 let path = path.as_ref();
138 self.watcher.watch(path, RecursiveMode::NonRecursive)?;
139 self.watched_paths.push(path.to_path_buf());
140 Ok(())
141 }
142
143 pub fn unwatch<P: AsRef<Path>>(&mut self, path: P) -> Result<(), notify::Error> {
145 let path = path.as_ref();
146 self.watcher.unwatch(path)?;
147 self.watched_paths.retain(|p| p != path);
148 Ok(())
149 }
150
151 pub fn changes(&self) -> impl Iterator<Item = FileChange> + '_ {
155 std::iter::from_fn(move || self.receiver.recv().ok())
156 }
157
158 pub fn try_recv(&self) -> Option<FileChange> {
160 self.receiver.try_recv().ok()
161 }
162
163 pub fn watched_paths(&self) -> &[PathBuf] {
165 &self.watched_paths
166 }
167}
168
169#[cfg(feature = "watch")]
171pub fn find_dxs_files<P: AsRef<Path>>(dir: P) -> Result<Vec<PathBuf>, std::io::Error> {
172 use std::fs;
173
174 let mut dxs_files = Vec::new();
175
176 for entry in fs::read_dir(dir)? {
177 let entry = entry?;
178 let path = entry.path();
179
180 if path.is_file() {
181 if let Some(ext) = path.extension() {
182 if ext == "sr" {
183 dxs_files.push(path);
184 }
185 }
186 } else if path.is_dir() {
187 dxs_files.extend(find_dxs_files(&path)?);
189 }
190 }
191
192 Ok(dxs_files)
193}
194
195#[cfg(feature = "watch")]
197pub fn find_dx_config<P: AsRef<Path>>(start_dir: P) -> Option<PathBuf> {
198 let mut current = start_dir.as_ref().to_path_buf();
199
200 loop {
201 let config_path = current.join("dx");
202 if config_path.exists() && config_path.is_file() {
203 return Some(config_path);
204 }
205
206 if !current.pop() {
208 break;
209 }
210 }
211
212 None
213}
214
215#[cfg(not(feature = "watch"))]
217pub struct DxWatcher;
218
219#[cfg(not(feature = "watch"))]
220impl DxWatcher {
221 pub fn new(_debounce_ms: u64) -> Result<Self, &'static str> {
223 Err("Watch feature not enabled. Enable with --features watch")
224 }
225}
226
227#[cfg(test)]
228#[cfg(feature = "watch")]
229mod tests {
230 use super::*;
231 use std::fs;
232 use tempfile::tempdir;
233
234 #[test]
235 fn test_find_dxs_files() {
236 let temp = tempdir().unwrap();
237 let rules_dir = temp.path().join("rules");
238 fs::create_dir(&rules_dir).unwrap();
239
240 fs::write(rules_dir.join("js-rules.sr"), "# JS rules").unwrap();
242 fs::write(rules_dir.join("py-rules.sr"), "# Python rules").unwrap();
243 fs::write(rules_dir.join("other.txt"), "not a rule file").unwrap();
244
245 let dxs_files = find_dxs_files(&rules_dir).unwrap();
246 assert_eq!(dxs_files.len(), 2);
247
248 let names: Vec<_> = dxs_files
249 .iter()
250 .filter_map(|p| p.file_name()?.to_str())
251 .collect();
252 assert!(names.contains(&"js-rules.sr"));
253 assert!(names.contains(&"py-rules.sr"));
254 }
255
256 #[test]
257 fn test_find_dx_config() {
258 let temp = tempdir().unwrap();
259 let config_path = temp.path().join("dx");
260 fs::write(&config_path, "# dx config").unwrap();
261
262 let found = find_dx_config(temp.path()).unwrap();
263 assert_eq!(found, config_path);
264 }
265
266 #[test]
267 fn test_find_dx_config_in_parent() {
268 let temp = tempdir().unwrap();
269 let config_path = temp.path().join("dx");
270 fs::write(&config_path, "# dx config").unwrap();
271
272 let sub_dir = temp.path().join("src");
274 fs::create_dir(&sub_dir).unwrap();
275
276 let found = find_dx_config(&sub_dir).unwrap();
278 assert_eq!(found, config_path);
279 }
280
281 #[test]
282 fn test_watcher_creation() {
283 let watcher = DxWatcher::new(250);
284 assert!(watcher.is_ok());
285 }
286}