1use std::path::{Path, PathBuf};
16use std::time::{SystemTime, UNIX_EPOCH};
17
18use serde::{Deserialize, Serialize};
19
20use crate::server_config::{ConfigSnapshot, ConfigUpdate, RateLimitUpdate, RequestLogUpdate};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct PersistedConfig {
27 pub version: u32,
29 pub saved_at: u64,
31 pub axon_version: String,
33 pub save_count: u64,
35 pub config: ConfigSnapshot,
37}
38
39#[derive(Debug, Clone, Serialize)]
41pub struct SaveResult {
42 pub success: bool,
43 pub path: String,
44 pub save_count: u64,
45 pub error: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize)]
50pub struct LoadResult {
51 pub success: bool,
52 pub path: String,
53 pub saved_at: Option<u64>,
54 pub save_count: Option<u64>,
55 pub error: Option<String>,
56}
57
58pub const DEFAULT_CONFIG_FILE: &str = "axon-server-config.json";
62
63pub fn resolve_path(custom: Option<&str>) -> PathBuf {
66 match custom {
67 Some(p) => PathBuf::from(p),
68 None => PathBuf::from(DEFAULT_CONFIG_FILE),
69 }
70}
71
72pub fn save(snapshot: &ConfigSnapshot, path: &Path, axon_version: &str) -> SaveResult {
79 let prev_count = match std::fs::read_to_string(path) {
81 Ok(content) => {
82 serde_json::from_str::<PersistedConfig>(&content)
83 .map(|p| p.save_count)
84 .unwrap_or(0)
85 }
86 Err(_) => 0,
87 };
88
89 let persisted = PersistedConfig {
90 version: 1,
91 saved_at: now_secs(),
92 axon_version: axon_version.to_string(),
93 save_count: prev_count + 1,
94 config: snapshot.clone(),
95 };
96
97 let json = match serde_json::to_string_pretty(&persisted) {
98 Ok(j) => j,
99 Err(e) => {
100 return SaveResult {
101 success: false,
102 path: path.display().to_string(),
103 save_count: prev_count,
104 error: Some(format!("serialize error: {e}")),
105 };
106 }
107 };
108
109 let tmp_path = path.with_extension("json.tmp");
111 if let Err(e) = std::fs::write(&tmp_path, &json) {
112 return SaveResult {
113 success: false,
114 path: path.display().to_string(),
115 save_count: prev_count,
116 error: Some(format!("write error: {e}")),
117 };
118 }
119
120 if let Err(e) = std::fs::rename(&tmp_path, path) {
121 if let Err(e2) = std::fs::write(path, &json) {
123 return SaveResult {
124 success: false,
125 path: path.display().to_string(),
126 save_count: prev_count,
127 error: Some(format!("rename error: {e}, write fallback error: {e2}")),
128 };
129 }
130 let _ = std::fs::remove_file(&tmp_path);
132 }
133
134 SaveResult {
135 success: true,
136 path: path.display().to_string(),
137 save_count: persisted.save_count,
138 error: None,
139 }
140}
141
142pub fn load(path: &Path) -> Result<PersistedConfig, String> {
147 let content = std::fs::read_to_string(path)
148 .map_err(|e| format!("read error: {e}"))?;
149
150 let persisted: PersistedConfig = serde_json::from_str(&content)
151 .map_err(|e| format!("parse error: {e}"))?;
152
153 if persisted.version != 1 {
154 return Err(format!("unsupported config version: {}", persisted.version));
155 }
156
157 Ok(persisted)
158}
159
160pub fn exists(path: &Path) -> bool {
162 path.is_file()
163}
164
165pub fn remove(path: &Path) -> bool {
167 std::fs::remove_file(path).is_ok()
168}
169
170pub fn snapshot_to_update(snapshot: &ConfigSnapshot) -> ConfigUpdate {
175 ConfigUpdate {
176 rate_limit: Some(RateLimitUpdate {
177 max_requests: Some(snapshot.rate_limit.max_requests),
178 window_secs: Some(snapshot.rate_limit.window_secs),
179 enabled: Some(snapshot.rate_limit.enabled),
180 }),
181 request_log: Some(RequestLogUpdate {
182 capacity: Some(snapshot.request_log.capacity),
183 enabled: Some(snapshot.request_log.enabled),
184 }),
185 }
186}
187
188fn now_secs() -> u64 {
191 SystemTime::now()
192 .duration_since(UNIX_EPOCH)
193 .unwrap_or_default()
194 .as_secs()
195}
196
197#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::server_config::{AuthSection, RateLimitSection, RequestLogSection};
203 use std::fs;
204
205 fn sample_snapshot() -> ConfigSnapshot {
206 ConfigSnapshot {
207 rate_limit: RateLimitSection {
208 max_requests: 200,
209 window_secs: 120,
210 enabled: true,
211 },
212 request_log: RequestLogSection {
213 capacity: 500,
214 enabled: false,
215 },
216 auth: AuthSection {
217 enabled: true,
218 active_keys: 2,
219 total_keys: 3,
220 },
221 }
222 }
223
224 fn temp_path(name: &str) -> PathBuf {
225 let dir = std::env::temp_dir();
226 dir.join(format!("axon_test_{name}_{}.json", std::process::id()))
227 }
228
229 #[test]
230 fn save_and_load_roundtrip() {
231 let path = temp_path("roundtrip");
232 let snap = sample_snapshot();
233
234 let result = save(&snap, &path, "0.30.0-test");
235 assert!(result.success);
236 assert_eq!(result.save_count, 1);
237
238 let loaded = load(&path).unwrap();
239 assert_eq!(loaded.version, 1);
240 assert_eq!(loaded.axon_version, "0.30.0-test");
241 assert_eq!(loaded.save_count, 1);
242 assert_eq!(loaded.config.rate_limit.max_requests, 200);
243 assert_eq!(loaded.config.rate_limit.window_secs, 120);
244 assert_eq!(loaded.config.request_log.capacity, 500);
245 assert!(!loaded.config.request_log.enabled);
246
247 fs::remove_file(&path).ok();
248 }
249
250 #[test]
251 fn save_increments_count() {
252 let path = temp_path("increment");
253 let snap = sample_snapshot();
254
255 let r1 = save(&snap, &path, "v1");
256 assert_eq!(r1.save_count, 1);
257
258 let r2 = save(&snap, &path, "v1");
259 assert_eq!(r2.save_count, 2);
260
261 let r3 = save(&snap, &path, "v1");
262 assert_eq!(r3.save_count, 3);
263
264 let loaded = load(&path).unwrap();
265 assert_eq!(loaded.save_count, 3);
266
267 fs::remove_file(&path).ok();
268 }
269
270 #[test]
271 fn load_nonexistent_file() {
272 let path = temp_path("nonexistent_98765");
273 let result = load(&path);
274 assert!(result.is_err());
275 assert!(result.unwrap_err().contains("read error"));
276 }
277
278 #[test]
279 fn load_invalid_json() {
280 let path = temp_path("invalid");
281 fs::write(&path, "not json at all").unwrap();
282
283 let result = load(&path);
284 assert!(result.is_err());
285 assert!(result.unwrap_err().contains("parse error"));
286
287 fs::remove_file(&path).ok();
288 }
289
290 #[test]
291 fn load_wrong_version() {
292 let path = temp_path("wrong_ver");
293 let json = serde_json::json!({
294 "version": 99,
295 "saved_at": 0,
296 "axon_version": "test",
297 "save_count": 1,
298 "config": {
299 "rate_limit": { "max_requests": 100, "window_secs": 60, "enabled": true },
300 "request_log": { "capacity": 1000, "enabled": true },
301 "auth": { "enabled": false, "active_keys": 0, "total_keys": 0 }
302 }
303 });
304 fs::write(&path, serde_json::to_string(&json).unwrap()).unwrap();
305
306 let result = load(&path);
307 assert!(result.is_err());
308 assert!(result.unwrap_err().contains("unsupported config version"));
309
310 fs::remove_file(&path).ok();
311 }
312
313 #[test]
314 fn exists_and_remove() {
315 let path = temp_path("exists_test");
316 assert!(!exists(&path));
317
318 let snap = sample_snapshot();
319 save(&snap, &path, "test");
320 assert!(exists(&path));
321
322 assert!(remove(&path));
323 assert!(!exists(&path));
324 assert!(!remove(&path)); }
326
327 #[test]
328 fn snapshot_to_update_conversion() {
329 let snap = sample_snapshot();
330 let update = snapshot_to_update(&snap);
331
332 let rl = update.rate_limit.unwrap();
333 assert_eq!(rl.max_requests, Some(200));
334 assert_eq!(rl.window_secs, Some(120));
335 assert_eq!(rl.enabled, Some(true));
336
337 let log = update.request_log.unwrap();
338 assert_eq!(log.capacity, Some(500));
339 assert_eq!(log.enabled, Some(false));
340 }
341
342 #[test]
343 fn resolve_path_default() {
344 let p = resolve_path(None);
345 assert_eq!(p, PathBuf::from(DEFAULT_CONFIG_FILE));
346 }
347
348 #[test]
349 fn resolve_path_custom() {
350 let p = resolve_path(Some("/tmp/my-config.json"));
351 assert_eq!(p, PathBuf::from("/tmp/my-config.json"));
352 }
353
354 #[test]
355 fn persisted_config_serializes() {
356 let snap = sample_snapshot();
357 let persisted = PersistedConfig {
358 version: 1,
359 saved_at: 1700000000,
360 axon_version: "0.30.0".into(),
361 save_count: 5,
362 config: snap,
363 };
364
365 let json = serde_json::to_value(&persisted).unwrap();
366 assert_eq!(json["version"], 1);
367 assert_eq!(json["save_count"], 5);
368 assert_eq!(json["config"]["rate_limit"]["max_requests"], 200);
369 }
370
371 #[test]
372 fn save_result_serializes() {
373 let result = SaveResult {
374 success: true,
375 path: "/tmp/test.json".into(),
376 save_count: 3,
377 error: None,
378 };
379 let json = serde_json::to_value(&result).unwrap();
380 assert_eq!(json["success"], true);
381 assert_eq!(json["save_count"], 3);
382 }
383}