dynpatch_watcher/
config.rs1use crate::{FileWatcher, Result, WatchError};
4use arc_swap::ArcSwap;
5use serde::de::DeserializeOwned;
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10use tracing::{error, info, warn};
11
12pub trait HotConfig: DeserializeOwned + Send + Sync + 'static {
14 fn validate(&self) -> std::result::Result<(), String> {
16 Ok(())
17 }
18}
19
20pub struct ConfigWatcher<T: HotConfig> {
29 config: Arc<ArcSwap<T>>,
30 path: PathBuf,
31 _watcher: FileWatcher,
32 last_reload: Arc<Mutex<Instant>>,
33 debounce_duration: Duration,
34}
35
36impl<T: HotConfig> ConfigWatcher<T> {
37 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
39 Self::with_debounce(path, Duration::from_millis(500))
40 }
41
42 pub fn with_debounce<P: AsRef<Path>>(path: P, debounce: Duration) -> Result<Self> {
47 let path = path.as_ref().to_path_buf();
48 let initial_config = Self::load_config(&path)?;
49
50 initial_config
52 .validate()
53 .map_err(|e| WatchError::ParseFailed(format!("Initial config validation failed: {}", e)))?;
54
55 info!("Initial config loaded and validated from: {:?}", path);
56
57 let config = Arc::new(ArcSwap::new(Arc::new(initial_config)));
58 let config_clone = config.clone();
59 let path_clone = path.clone();
60 let last_reload = Arc::new(Mutex::new(Instant::now()));
61 let last_reload_clone = last_reload.clone();
62 let debounce_clone = debounce;
63
64 let watcher = FileWatcher::new(&path, move |_| {
65 {
67 let mut last = last_reload_clone.lock().unwrap();
68 let now = Instant::now();
69 if now.duration_since(*last) < debounce_clone {
70 return; }
72 *last = now;
73 }
74
75 info!("Config file changed, reloading: {:?}", path_clone);
76
77 match Self::load_config(&path_clone) {
78 Ok(new_config) => {
79 if let Err(e) = new_config.validate() {
81 error!("Config validation failed, keeping previous config: {}", e);
82 return;
83 }
84
85 config_clone.store(Arc::new(new_config));
86 info!("Config reloaded and validated successfully");
87 }
88 Err(e) => {
89 error!("Failed to reload config (keeping previous): {}", e);
90 }
91 }
92 })?;
93
94 Ok(Self {
95 config,
96 path,
97 _watcher: watcher,
98 last_reload,
99 debounce_duration: debounce,
100 })
101 }
102
103 pub fn get(&self) -> Arc<T> {
105 self.config.load_full()
106 }
107
108 pub fn reload(&self) -> Result<()> {
110 let new_config = Self::load_config(&self.path)?;
111 new_config
112 .validate()
113 .map_err(|e| WatchError::ParseFailed(e))?;
114 self.config.store(Arc::new(new_config));
115 info!("Config manually reloaded");
116 Ok(())
117 }
118
119 fn load_config(path: &Path) -> Result<T> {
120 let content = fs::read_to_string(path)?;
121 let extension = path.extension().and_then(|e| e.to_str());
122
123 match extension {
124 #[cfg(feature = "json")]
125 Some("json") => serde_json::from_str(&content)
126 .map_err(|e| WatchError::ParseFailed(e.to_string())),
127
128 #[cfg(feature = "toml")]
129 Some("toml") => toml::from_str(&content)
130 .map_err(|e| WatchError::ParseFailed(e.to_string())),
131
132 #[cfg(feature = "yaml")]
133 Some("yaml") | Some("yml") => serde_yaml::from_str(&content)
134 .map_err(|e| WatchError::ParseFailed(e.to_string())),
135
136 _ => Err(WatchError::ParseFailed(format!(
137 "Unsupported file extension: {:?}",
138 extension
139 ))),
140 }
141 }
142
143 pub fn path(&self) -> &Path {
144 &self.path
145 }
146
147 pub fn debounce_duration(&self) -> Duration {
149 self.debounce_duration
150 }
151
152 pub fn time_since_last_reload(&self) -> Duration {
154 let last = self.last_reload.lock().unwrap();
155 Instant::now().duration_since(*last)
156 }
157}
158
159pub fn watch<T: HotConfig, P: AsRef<Path>>(path: P) -> Result<ConfigWatcher<T>> {
161 ConfigWatcher::new(path)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use serde::{Deserialize, Serialize};
168 use tempfile::Builder;
169
170 #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
171 struct TestConfig {
172 value: i32,
173 name: String,
174 }
175
176 impl HotConfig for TestConfig {}
177
178 #[test]
179 #[cfg(feature = "json")]
180 fn test_config_watcher_json() {
181 let temp = Builder::new()
182 .suffix(".json")
183 .tempfile()
184 .unwrap();
185
186 let config = TestConfig {
187 value: 42,
188 name: "test".to_string(),
189 };
190
191 std::fs::write(temp.path(), serde_json::to_string(&config).unwrap()).unwrap();
192
193 let watcher = ConfigWatcher::<TestConfig>::new(temp.path()).unwrap();
194 let loaded = watcher.get();
195
196 assert_eq!(loaded.value, 42);
197 assert_eq!(loaded.name, "test");
198 }
199}