ggen_core/codegen/
watch.rs1use ggen_utils::error::{Error, Result};
20use std::path::{Path, PathBuf};
21use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
22use std::time::Duration;
23
24pub struct FileWatcher {
26 watch_paths: Vec<PathBuf>,
28 pub debounce_ms: u64,
30 pub queue_capacity: usize,
32}
33
34#[derive(Debug, Clone)]
36pub struct WatchEvent {
37 pub path: PathBuf,
39 pub timestamp: std::time::Instant,
41}
42
43impl FileWatcher {
44 pub fn new<P: AsRef<Path>>(watch_paths: Vec<P>) -> Self {
49 Self {
50 watch_paths: watch_paths
51 .iter()
52 .map(|p| p.as_ref().to_path_buf())
53 .collect(),
54 debounce_ms: 300,
55 queue_capacity: 10,
56 }
57 }
58
59 pub fn with_debounce_ms(mut self, debounce_ms: u64) -> Self {
61 self.debounce_ms = debounce_ms;
62 self
63 }
64
65 pub fn with_queue_capacity(mut self, capacity: usize) -> Self {
67 self.queue_capacity = capacity;
68 self
69 }
70
71 pub fn start(self) -> Result<Receiver<WatchEvent>> {
80 let (_tx, rx) = channel();
81
82 for path in &self.watch_paths {
84 if !path.exists() {
85 return Err(Error::new(&format!(
86 "Watch path does not exist: {}",
87 path.display()
88 )));
89 }
90 }
91
92 std::thread::spawn(move || {
103 loop {
106 std::thread::sleep(Duration::from_millis(1000));
107 }
109 });
110
111 Ok(rx)
112 }
113
114 pub fn wait_for_change(
127 rx: &Receiver<WatchEvent>, timeout: Duration,
128 ) -> Result<Option<WatchEvent>> {
129 match rx.recv_timeout(timeout) {
130 Ok(event) => Ok(Some(event)),
131 Err(RecvTimeoutError::Timeout) => Ok(None),
132 Err(RecvTimeoutError::Disconnected) => Err(Error::new("Watch channel disconnected")),
133 }
134 }
135}
136
137pub fn collect_watch_paths(
146 manifest_path: &Path, manifest: &crate::manifest::GgenManifest, base_path: &Path,
147) -> Vec<PathBuf> {
148 use crate::manifest::{QuerySource, TemplateSource};
149
150 let mut paths = Vec::new();
151
152 paths.push(manifest_path.to_path_buf());
154
155 paths.push(base_path.join(&manifest.ontology.source));
157
158 for import in &manifest.ontology.imports {
160 paths.push(base_path.join(import));
161 }
162
163 for rule in &manifest.generation.rules {
165 if let QuerySource::File { file } = &rule.query {
166 paths.push(base_path.join(file));
167 }
168 }
169
170 for rule in &manifest.generation.rules {
172 if let TemplateSource::File { file } = &rule.template {
173 paths.push(base_path.join(file));
174 }
175 }
176
177 paths
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_file_watcher_creation() {
186 let paths = vec![PathBuf::from(".")];
187 let watcher = FileWatcher::new(paths);
188 assert_eq!(watcher.debounce_ms, 300);
189 assert_eq!(watcher.queue_capacity, 10);
190 }
191
192 #[test]
193 fn test_file_watcher_configuration() {
194 let paths = vec![PathBuf::from(".")];
195 let watcher = FileWatcher::new(paths)
196 .with_debounce_ms(500)
197 .with_queue_capacity(20);
198
199 assert_eq!(watcher.debounce_ms, 500);
200 assert_eq!(watcher.queue_capacity, 20);
201 }
202
203 #[test]
204 fn test_collect_watch_paths_empty() {
205 use crate::manifest::{
206 GenerationConfig, GgenManifest, InferenceConfig, OntologyConfig, ProjectConfig,
207 ValidationConfig,
208 };
209 use std::collections::BTreeMap;
210 use std::path::PathBuf;
211
212 let manifest = GgenManifest {
213 project: ProjectConfig {
214 name: "test".to_string(),
215 version: "1.0.0".to_string(),
216 description: None,
217 },
218 ontology: OntologyConfig {
219 source: PathBuf::from("ontology.ttl"),
220 imports: vec![],
221 base_iri: None,
222 prefixes: BTreeMap::new(),
223 },
224 inference: InferenceConfig {
225 rules: vec![],
226 max_reasoning_timeout_ms: 5000,
227 },
228 generation: GenerationConfig {
229 rules: vec![],
230 max_sparql_timeout_ms: 5000,
231 require_audit_trail: false,
232 determinism_salt: None,
233 output_dir: PathBuf::from("generated"),
234 },
235 validation: ValidationConfig::default(),
236 };
237
238 let manifest_path = Path::new("ggen.toml");
239 let base_path = Path::new(".");
240 let paths = collect_watch_paths(manifest_path, &manifest, base_path);
241
242 assert!(paths.len() >= 2);
244 assert!(paths.contains(&PathBuf::from("ggen.toml")));
245 assert!(
247 paths
248 .iter()
249 .any(|p| p.to_string_lossy().ends_with("ontology.ttl")),
250 "Should contain ontology.ttl path (possibly joined with base_path)"
251 );
252 }
253}