subx_cli/config/
cache.rs

1//! Configuration cache manager module.
2
3use std::collections::HashMap;
4use std::time::{Duration, Instant};
5
6use serde_json;
7
8use crate::config::Config;
9#[cfg(test)]
10use crate::config::{AIConfig, FormatsConfig, GeneralConfig, SyncConfig};
11
12/// Cache entry storing serialized configuration value and its expiration.
13struct CacheEntry {
14    value: serde_json::Value,
15    created_at: Instant,
16    ttl: Duration,
17}
18
19/// Manager for caching configuration segments to avoid repetitive loads.
20pub struct ConfigCache {
21    entries: HashMap<String, CacheEntry>,
22    default_ttl: Duration,
23}
24
25impl ConfigCache {
26    /// Create a new configuration cache with default TTL of 5 minutes.
27    pub fn new() -> Self {
28        Self {
29            entries: HashMap::new(),
30            default_ttl: Duration::from_secs(300),
31        }
32    }
33
34    /// Attempt to retrieve a cached value, returning None if missing or expired.
35    pub fn get<T>(&self, key: &str) -> Option<T>
36    where
37        T: serde::de::DeserializeOwned,
38    {
39        let entry = self.entries.get(key)?;
40        if entry.created_at.elapsed() > entry.ttl {
41            return None;
42        }
43        serde_json::from_value(entry.value.clone()).ok()
44    }
45
46    /// Insert or update a cache entry with an optional TTL override.
47    pub fn set<T>(&mut self, key: String, value: T, ttl: Option<Duration>)
48    where
49        T: serde::Serialize,
50    {
51        let json_value = serde_json::to_value(value).unwrap();
52        let entry = CacheEntry {
53            value: json_value,
54            created_at: Instant::now(),
55            ttl: ttl.unwrap_or(self.default_ttl),
56        };
57        self.entries.insert(key, entry);
58    }
59
60    /// Update the cache with the full configuration and its segments.
61    pub fn update(&mut self, config: &Config) {
62        self.set("full_config".to_string(), config, None);
63        self.set("ai_config".to_string(), &config.ai, None);
64        self.set("formats_config".to_string(), &config.formats, None);
65        self.set("sync_config".to_string(), &config.sync, None);
66        self.set("general_config".to_string(), &config.general, None);
67    }
68
69    /// Clear all cache entries.
70    pub fn clear(&mut self) {
71        self.entries.clear();
72    }
73
74    /// Remove expired entries from the cache.
75    pub fn cleanup_expired(&mut self) {
76        self.entries
77            .retain(|_, entry| entry.created_at.elapsed() <= entry.ttl);
78    }
79}
80
81impl Default for ConfigCache {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use std::thread::sleep;
91    use std::time::Duration;
92
93    #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
94    struct Dummy {
95        x: u32,
96    }
97
98    #[test]
99    fn test_set_get() {
100        let mut cache = ConfigCache::new();
101        cache.set("dummy".to_string(), Dummy { x: 42 }, None);
102        let value: Option<Dummy> = cache.get("dummy");
103        assert_eq!(value, Some(Dummy { x: 42 }));
104    }
105
106    #[test]
107    fn test_ttl_expiration() {
108        let mut cache = ConfigCache::new();
109        cache.set(
110            "dummy".to_string(),
111            Dummy { x: 42 },
112            Some(Duration::from_millis(10)),
113        );
114        sleep(Duration::from_millis(20));
115        let value: Option<Dummy> = cache.get("dummy");
116        assert!(value.is_none());
117    }
118
119    #[test]
120    fn test_clear_and_cleanup() {
121        let mut cache = ConfigCache::new();
122        cache.set("a".to_string(), Dummy { x: 1 }, None);
123        cache.set(
124            "b".to_string(),
125            Dummy { x: 2 },
126            Some(Duration::from_secs(0)),
127        );
128        cache.cleanup_expired();
129        assert!(cache.get::<Dummy>("b").is_none());
130        assert!(cache.get::<Dummy>("a").is_some());
131        cache.clear();
132        assert!(cache.get::<Dummy>("a").is_none());
133    }
134
135    #[test]
136    fn test_update() {
137        let mut cache = ConfigCache::new();
138        let config = Config::default();
139        cache.update(&config);
140        assert!(cache.get::<Config>("full_config").is_some());
141        assert!(cache.get::<AIConfig>("ai_config").is_some());
142        assert!(cache.get::<FormatsConfig>("formats_config").is_some());
143        assert!(cache.get::<SyncConfig>("sync_config").is_some());
144        assert!(cache.get::<GeneralConfig>("general_config").is_some());
145    }
146}