ricecoder_hooks/config/
reloader.rs1use crate::error::{HooksError, Result};
8use crate::types::Hook;
9use std::collections::HashMap;
10use std::fs;
11use std::path::{Path, PathBuf};
12use std::time::SystemTime;
13
14pub struct ConfigReloader {
19 project_config_mtime: Option<SystemTime>,
21
22 user_config_mtime: Option<SystemTime>,
24
25 hook_state: HashMap<String, bool>,
27}
28
29impl ConfigReloader {
30 pub fn new() -> Self {
32 Self {
33 project_config_mtime: None,
34 user_config_mtime: None,
35 hook_state: HashMap::new(),
36 }
37 }
38
39 pub fn has_changed(&mut self) -> Result<bool> {
43 let project_path = PathBuf::from(".ricecoder/hooks.yaml");
44 let project_mtime = Self::get_file_mtime(&project_path)?;
45
46 let user_path = Self::get_user_config_path()?;
47 let user_mtime = Self::get_file_mtime(&user_path)?;
48
49 let changed =
50 project_mtime != self.project_config_mtime || user_mtime != self.user_config_mtime;
51
52 if changed {
53 self.project_config_mtime = project_mtime;
54 self.user_config_mtime = user_mtime;
55 }
56
57 Ok(changed)
58 }
59
60 pub fn save_hook_state(&mut self, hooks: &HashMap<String, Hook>) {
65 self.hook_state.clear();
66 for (id, hook) in hooks {
67 self.hook_state.insert(id.clone(), hook.enabled);
68 }
69 }
70
71 pub fn restore_hook_state(&self, hooks: &mut HashMap<String, Hook>) {
77 for (id, hook) in hooks.iter_mut() {
78 if let Some(&enabled) = self.hook_state.get(id) {
79 hook.enabled = enabled;
80 }
81 }
82 }
83
84 fn get_file_mtime(path: &Path) -> Result<Option<SystemTime>> {
88 if !path.exists() {
89 return Ok(None);
90 }
91
92 let metadata = fs::metadata(path)
93 .map_err(|e| HooksError::StorageError(format!("Failed to get file metadata: {}", e)))?;
94
95 let mtime = metadata.modified().map_err(|e| {
96 HooksError::StorageError(format!("Failed to get modification time: {}", e))
97 })?;
98
99 Ok(Some(mtime))
100 }
101
102 fn get_user_config_path() -> Result<PathBuf> {
104 use ricecoder_storage::PathResolver;
105
106 let global_path = PathResolver::resolve_global_path()
107 .map_err(|e| HooksError::StorageError(e.to_string()))?;
108 Ok(global_path.join("hooks.yaml"))
109 }
110}
111
112impl Default for ConfigReloader {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::io::Write;
122 use std::thread;
123 use std::time::Duration;
124 use tempfile::NamedTempFile;
125
126 #[test]
127 fn test_new_reloader() {
128 let reloader = ConfigReloader::new();
129 assert!(reloader.project_config_mtime.is_none());
130 assert!(reloader.user_config_mtime.is_none());
131 assert_eq!(reloader.hook_state.len(), 0);
132 }
133
134 #[test]
135 fn test_save_and_restore_hook_state() {
136 let mut reloader = ConfigReloader::new();
137
138 let mut hooks = HashMap::new();
140 hooks.insert(
141 "hook1".to_string(),
142 Hook {
143 id: "hook1".to_string(),
144 name: "Hook 1".to_string(),
145 description: None,
146 event: "event1".to_string(),
147 action: crate::types::Action::Command(crate::types::CommandAction {
148 command: "cmd".to_string(),
149 args: vec![],
150 timeout_ms: None,
151 capture_output: false,
152 }),
153 enabled: true,
154 tags: vec![],
155 metadata: serde_json::json!({}),
156 condition: None,
157 },
158 );
159
160 hooks.insert(
161 "hook2".to_string(),
162 Hook {
163 id: "hook2".to_string(),
164 name: "Hook 2".to_string(),
165 description: None,
166 event: "event2".to_string(),
167 action: crate::types::Action::Command(crate::types::CommandAction {
168 command: "cmd".to_string(),
169 args: vec![],
170 timeout_ms: None,
171 capture_output: false,
172 }),
173 enabled: false,
174 tags: vec![],
175 metadata: serde_json::json!({}),
176 condition: None,
177 },
178 );
179
180 reloader.save_hook_state(&hooks);
182 assert_eq!(reloader.hook_state.len(), 2);
183 assert_eq!(reloader.hook_state.get("hook1"), Some(&true));
184 assert_eq!(reloader.hook_state.get("hook2"), Some(&false));
185
186 hooks.get_mut("hook1").unwrap().enabled = false;
188 hooks.get_mut("hook2").unwrap().enabled = true;
189
190 reloader.restore_hook_state(&mut hooks);
192 assert!(hooks.get("hook1").unwrap().enabled);
193 assert!(!hooks.get("hook2").unwrap().enabled);
194 }
195
196 #[test]
197 fn test_get_file_mtime_nonexistent() {
198 let path = PathBuf::from("/nonexistent/path/file.yaml");
199 let mtime = ConfigReloader::get_file_mtime(&path).expect("Should not error");
200 assert!(mtime.is_none());
201 }
202
203 #[test]
204 fn test_get_file_mtime_existing() {
205 let temp_file = NamedTempFile::new().expect("Should create temp file");
206 let path = temp_file.path();
207
208 let mtime = ConfigReloader::get_file_mtime(path).expect("Should get mtime");
209 assert!(mtime.is_some());
210 }
211
212 #[test]
213 fn test_has_changed_no_files() {
214 let mut reloader = ConfigReloader::new();
215 let changed = reloader.has_changed().expect("Should check for changes");
216 assert!(!changed);
218 }
219
220 #[test]
221 fn test_has_changed_detects_modification() {
222 let mut reloader = ConfigReloader::new();
223
224 let mut temp_file = NamedTempFile::new().expect("Should create temp file");
226 let path = temp_file.path().to_path_buf();
227
228 temp_file.write_all(b"initial").expect("Should write");
230 temp_file.flush().expect("Should flush");
231
232 let _changed1 = reloader.has_changed().expect("Should check");
234
235 thread::sleep(Duration::from_millis(100));
237
238 temp_file.write_all(b" modified").expect("Should write");
240 temp_file.flush().expect("Should flush");
241
242 let _changed2 = reloader.has_changed().expect("Should check");
244
245 drop(temp_file);
247 drop(path);
248
249 }
252}