ggen_core/codegen/
watch_mode.rs1use crate::codegen::{SyncExecutor, SyncOptions};
2use crate::manifest::ManifestParser;
3use ggen_utils::error::{Error, Result};
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::Duration;
7use tokio::sync::RwLock;
8use tokio::time::sleep;
9
10pub struct WatchConfig {
11 pub debounce_ms: u64,
12 pub check_interval_ms: u64,
13 pub max_retries: usize,
14}
15
16impl Default for WatchConfig {
17 fn default() -> Self {
18 Self {
19 debounce_ms: 500,
20 check_interval_ms: 1000,
21 max_retries: 3,
22 }
23 }
24}
25
26pub struct WatchMode {
27 options: SyncOptions,
28 config: WatchConfig,
29 watched_paths: Arc<RwLock<Vec<PathBuf>>>,
30}
31
32impl WatchMode {
33 pub fn new(options: SyncOptions, config: WatchConfig) -> Self {
34 let watched_paths = Arc::new(RwLock::new(vec![options.manifest_path.clone()]));
35
36 Self {
37 options,
38 config,
39 watched_paths,
40 }
41 }
42
43 pub async fn start(&mut self) -> Result<()> {
44 let base_path = self
45 .options
46 .manifest_path
47 .parent()
48 .unwrap_or(Path::new("."));
49
50 let mut manifest_data = ManifestParser::parse(&self.options.manifest_path)
52 .map_err(|e| Error::new(&format!("Failed to parse manifest: {}", e)))?;
53
54 let ontology_path = base_path.join(&manifest_data.ontology.source);
56 self.watched_paths.write().await.push(ontology_path);
57
58 eprintln!("Watch mode started. Press Ctrl+C to exit.");
59 eprintln!(
60 "Watching {} files for changes...",
61 self.watched_paths.read().await.len()
62 );
63
64 let mut file_hashes = self.compute_file_hashes().await?;
66
67 loop {
68 sleep(Duration::from_millis(self.config.check_interval_ms)).await;
69
70 let current_hashes = match self.compute_file_hashes().await {
72 Ok(h) => h,
73 Err(_) => continue,
74 };
75
76 let mut changed = false;
78 for (path, new_hash) in ¤t_hashes {
79 if let Some(old_hash) = file_hashes.get(path) {
80 if old_hash != new_hash {
81 eprintln!("Changed: {}", path.display());
82 changed = true;
83 }
84 }
85 }
86
87 if current_hashes.len() != file_hashes.len() {
89 changed = true;
90 }
91
92 if changed {
93 eprintln!("Debouncing changes for {}ms...", self.config.debounce_ms);
94 sleep(Duration::from_millis(self.config.debounce_ms)).await;
95
96 if let Ok(new_manifest) = ManifestParser::parse(&self.options.manifest_path) {
98 manifest_data = new_manifest;
99
100 self.watched_paths.write().await.clear();
102 self.watched_paths
103 .write()
104 .await
105 .push(self.options.manifest_path.clone());
106
107 let ontology_path = base_path.join(&manifest_data.ontology.source);
108 self.watched_paths.write().await.push(ontology_path);
109 }
110
111 eprintln!("Triggering sync...");
113 let mut retry_count = 0;
114 loop {
115 let executor = SyncExecutor::new(self.options.clone());
116 match executor.execute() {
117 Ok(result) => {
118 eprintln!(
119 "✓ Sync complete: {} files in {}ms",
120 result.files_synced, result.duration_ms
121 );
122 break;
123 }
124 Err(e) => {
125 retry_count += 1;
126 if retry_count >= self.config.max_retries {
127 eprintln!(
128 "✗ Sync failed after {} retries: {}",
129 self.config.max_retries, e
130 );
131 break;
132 }
133 eprintln!(
134 "⚠ Sync failed (retry {}/{}): {}",
135 retry_count, self.config.max_retries, e
136 );
137 sleep(Duration::from_millis(self.config.debounce_ms)).await;
138 }
139 }
140 }
141
142 file_hashes = self.compute_file_hashes().await?;
143 eprintln!("Watching for more changes...");
144 }
145 }
146 }
147
148 async fn compute_file_hashes(&self) -> Result<std::collections::HashMap<PathBuf, String>> {
149 let mut hashes = std::collections::HashMap::new();
150
151 for path in self.watched_paths.read().await.iter() {
152 if path.exists() {
153 match std::fs::read_to_string(path) {
154 Ok(content) => {
155 let hash = Self::hash_file(&content);
156 hashes.insert(path.clone(), hash);
157 }
158 Err(_) => {
159 }
161 }
162 }
163 }
164
165 Ok(hashes)
166 }
167
168 fn hash_file(content: &str) -> String {
169 use sha2::{Digest, Sha256};
170 let mut hasher = Sha256::new();
171 hasher.update(content.as_bytes());
172 format!("{:x}", hasher.finalize())
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[tokio::test]
181 async fn test_watch_config_defaults() {
182 let config = WatchConfig::default();
183 assert_eq!(config.debounce_ms, 500);
184 assert_eq!(config.check_interval_ms, 1000);
185 assert_eq!(config.max_retries, 3);
186 }
187
188 #[tokio::test]
189 async fn test_watch_mode_creation() {
190 let options = SyncOptions::default();
191 let config = WatchConfig::default();
192 let watch = WatchMode::new(options.clone(), config);
193
194 let paths = watch.watched_paths.read().await;
195 assert!(paths.contains(&options.manifest_path));
196 }
197
198 #[test]
199 fn test_hash_file_consistency() {
200 let content1 = "test content";
201 let content2 = "test content";
202 let content3 = "different content";
203
204 assert_eq!(
205 WatchMode::hash_file(content1),
206 WatchMode::hash_file(content2)
207 );
208 assert_ne!(
209 WatchMode::hash_file(content1),
210 WatchMode::hash_file(content3)
211 );
212 }
213}