1use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::Instant;
12
13#[derive(Debug, Clone)]
15pub struct ConfigEntry {
16 pub value: ConfigValue,
18 pub reloadable: bool,
20 pub description: String,
22}
23
24#[derive(Debug, Clone, PartialEq)]
26pub enum ConfigValue {
27 String(String),
29 Int(i64),
31 Float(f64),
33 Bool(bool),
35 DurationMs(u64),
37}
38
39impl std::fmt::Display for ConfigValue {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::String(s) => write!(f, "{}", s),
43 Self::Int(i) => write!(f, "{}", i),
44 Self::Float(v) => write!(f, "{}", v),
45 Self::Bool(b) => write!(f, "{}", b),
46 Self::DurationMs(ms) => write!(f, "{}ms", ms),
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct ConfigChange {
54 pub key: String,
56 pub old_value: Option<ConfigValue>,
58 pub new_value: ConfigValue,
60 pub version: u64,
62 pub changed_at: Instant,
64 pub changed_by: String,
66}
67
68pub struct HotReloadConfig {
70 entries: HashMap<String, ConfigEntry>,
72 version: AtomicU64,
74 changes: Vec<ConfigChange>,
76 max_audit_entries: usize,
78}
79
80impl HotReloadConfig {
81 pub fn new() -> Self {
83 Self {
84 entries: HashMap::new(),
85 version: AtomicU64::new(0),
86 changes: Vec::new(),
87 max_audit_entries: 1000,
88 }
89 }
90
91 pub fn register(
93 &mut self,
94 key: impl Into<String>,
95 value: ConfigValue,
96 reloadable: bool,
97 description: impl Into<String>,
98 ) {
99 self.entries.insert(
100 key.into(),
101 ConfigEntry {
102 value,
103 reloadable,
104 description: description.into(),
105 },
106 );
107 }
108
109 pub fn get(&self, key: &str) -> Option<&ConfigValue> {
111 self.entries.get(key).map(|e| &e.value)
112 }
113
114 pub fn get_string(&self, key: &str) -> Option<&str> {
116 match self.get(key)? {
117 ConfigValue::String(s) => Some(s),
118 _ => None,
119 }
120 }
121
122 pub fn get_int(&self, key: &str) -> Option<i64> {
124 match self.get(key)? {
125 ConfigValue::Int(i) => Some(*i),
126 _ => None,
127 }
128 }
129
130 pub fn get_bool(&self, key: &str) -> Option<bool> {
132 match self.get(key)? {
133 ConfigValue::Bool(b) => Some(*b),
134 _ => None,
135 }
136 }
137
138 pub fn update(
142 &mut self,
143 key: &str,
144 new_value: ConfigValue,
145 changed_by: impl Into<String>,
146 ) -> Result<u64, ConfigUpdateError> {
147 let entry = self
148 .entries
149 .get(key)
150 .ok_or_else(|| ConfigUpdateError::KeyNotFound(key.to_string()))?;
151
152 if !entry.reloadable {
153 return Err(ConfigUpdateError::NotReloadable(key.to_string()));
154 }
155
156 if std::mem::discriminant(&entry.value) != std::mem::discriminant(&new_value) {
158 return Err(ConfigUpdateError::TypeMismatch {
159 key: key.to_string(),
160 expected: format!("{:?}", std::mem::discriminant(&entry.value)),
161 got: format!("{:?}", std::mem::discriminant(&new_value)),
162 });
163 }
164
165 let old_value = entry.value.clone();
166 let version = self.version.fetch_add(1, Ordering::Relaxed) + 1;
167
168 let change = ConfigChange {
170 key: key.to_string(),
171 old_value: Some(old_value),
172 new_value: new_value.clone(),
173 version,
174 changed_at: Instant::now(),
175 changed_by: changed_by.into(),
176 };
177
178 self.entries
180 .get_mut(key)
181 .expect("config entry must exist after contains_key check")
182 .value = new_value;
183
184 self.changes.push(change);
186 while self.changes.len() > self.max_audit_entries {
187 self.changes.remove(0);
188 }
189
190 Ok(version)
191 }
192
193 pub fn version(&self) -> u64 {
195 self.version.load(Ordering::Relaxed)
196 }
197
198 pub fn recent_changes(&self, limit: usize) -> &[ConfigChange] {
200 let start = self.changes.len().saturating_sub(limit);
201 &self.changes[start..]
202 }
203
204 pub fn list_keys(&self) -> Vec<(&str, bool)> {
206 self.entries
207 .iter()
208 .map(|(k, v)| (k.as_str(), v.reloadable))
209 .collect()
210 }
211
212 pub fn len(&self) -> usize {
214 self.entries.len()
215 }
216
217 pub fn is_empty(&self) -> bool {
219 self.entries.is_empty()
220 }
221}
222
223impl Default for HotReloadConfig {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229#[derive(Debug, Clone)]
231pub enum ConfigUpdateError {
232 KeyNotFound(String),
234 NotReloadable(String),
236 TypeMismatch {
238 key: String,
240 expected: String,
242 got: String,
244 },
245 ValidationFailed(String),
247}
248
249impl std::fmt::Display for ConfigUpdateError {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 match self {
252 Self::KeyNotFound(k) => write!(f, "Config key not found: {}", k),
253 Self::NotReloadable(k) => write!(f, "Config key '{}' requires restart to change", k),
254 Self::TypeMismatch { key, expected, got } => {
255 write!(
256 f,
257 "Type mismatch for '{}': expected {}, got {}",
258 key, expected, got
259 )
260 }
261 Self::ValidationFailed(msg) => write!(f, "Validation failed: {}", msg),
262 }
263 }
264}
265
266impl std::error::Error for ConfigUpdateError {}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_register_and_get() {
274 let mut config = HotReloadConfig::new();
275 config.register(
276 "rate_limit",
277 ConfigValue::Int(1000),
278 true,
279 "Max requests/sec",
280 );
281 config.register("gpu_device", ConfigValue::Int(0), false, "GPU device index");
282
283 assert_eq!(config.get_int("rate_limit"), Some(1000));
284 assert_eq!(config.get_int("gpu_device"), Some(0));
285 }
286
287 #[test]
288 fn test_update_reloadable() {
289 let mut config = HotReloadConfig::new();
290 config.register("rate_limit", ConfigValue::Int(1000), true, "");
291
292 let version = config
293 .update("rate_limit", ConfigValue::Int(2000), "admin")
294 .unwrap();
295 assert_eq!(version, 1);
296 assert_eq!(config.get_int("rate_limit"), Some(2000));
297 }
298
299 #[test]
300 fn test_update_non_reloadable() {
301 let mut config = HotReloadConfig::new();
302 config.register("gpu_device", ConfigValue::Int(0), false, "");
303
304 let result = config.update("gpu_device", ConfigValue::Int(1), "admin");
305 assert!(matches!(result, Err(ConfigUpdateError::NotReloadable(_))));
306 }
307
308 #[test]
309 fn test_type_mismatch() {
310 let mut config = HotReloadConfig::new();
311 config.register("rate_limit", ConfigValue::Int(1000), true, "");
312
313 let result = config.update("rate_limit", ConfigValue::String("fast".into()), "admin");
314 assert!(matches!(
315 result,
316 Err(ConfigUpdateError::TypeMismatch { .. })
317 ));
318 }
319
320 #[test]
321 fn test_audit_trail() {
322 let mut config = HotReloadConfig::new();
323 config.register("rate_limit", ConfigValue::Int(1000), true, "");
324
325 config
326 .update("rate_limit", ConfigValue::Int(2000), "admin")
327 .unwrap();
328 config
329 .update("rate_limit", ConfigValue::Int(3000), "operator")
330 .unwrap();
331
332 let changes = config.recent_changes(10);
333 assert_eq!(changes.len(), 2);
334 assert_eq!(changes[0].changed_by, "admin");
335 assert_eq!(changes[1].changed_by, "operator");
336 }
337
338 #[test]
339 fn test_versioning() {
340 let mut config = HotReloadConfig::new();
341 config.register("a", ConfigValue::Bool(true), true, "");
342
343 assert_eq!(config.version(), 0);
344 config
345 .update("a", ConfigValue::Bool(false), "test")
346 .unwrap();
347 assert_eq!(config.version(), 1);
348 config.update("a", ConfigValue::Bool(true), "test").unwrap();
349 assert_eq!(config.version(), 2);
350 }
351}