opencode_provider_manager/discovery/
cache.rs1use super::error::{DiscoveryError, Result};
11use serde::{Deserialize, Serialize};
12use std::path::PathBuf;
13use std::time::{Duration, SystemTime, UNIX_EPOCH};
14
15const DEFAULT_CACHE_TTL_SECS: u64 = 24 * 60 * 60;
17
18#[derive(Debug, Serialize, Deserialize)]
21pub struct CacheEntry {
22 pub data: serde_json::Value,
24 pub created_at: u64,
26 pub ttl_secs: u64,
28}
29
30impl CacheEntry {
31 pub fn new<T: Serialize>(data: T, ttl_secs: u64) -> Result<Self> {
33 let data = serde_json::to_value(&data)
34 .map_err(|e| DiscoveryError::Cache(format!("Failed to serialize cache data: {e}")))?;
35 Ok(Self {
36 data,
37 created_at: SystemTime::now()
38 .duration_since(UNIX_EPOCH)
39 .unwrap_or_default()
40 .as_secs(),
41 ttl_secs,
42 })
43 }
44
45 pub fn is_expired(&self) -> bool {
47 let now = SystemTime::now()
48 .duration_since(UNIX_EPOCH)
49 .unwrap_or_default()
50 .as_secs();
51 now > self.created_at + self.ttl_secs
52 }
53}
54
55pub struct CacheManager {
57 cache_dir: PathBuf,
58 default_ttl: Duration,
59}
60
61impl CacheManager {
62 pub fn new() -> Result<Self> {
64 let cache_dir = dirs::cache_dir()
65 .ok_or_else(|| DiscoveryError::Cache("Cannot determine cache directory".to_string()))?
66 .join("opencode-provider-manager");
67
68 std::fs::create_dir_all(&cache_dir)
70 .map_err(|e| DiscoveryError::Cache(format!("Failed to create cache dir: {e}")))?;
71
72 Ok(Self {
73 cache_dir,
74 default_ttl: Duration::from_secs(DEFAULT_CACHE_TTL_SECS),
75 })
76 }
77
78 pub fn with_dir(cache_dir: PathBuf) -> Result<Self> {
80 std::fs::create_dir_all(&cache_dir)
81 .map_err(|e| DiscoveryError::Cache(format!("Failed to create cache dir: {e}")))?;
82
83 Ok(Self {
84 cache_dir,
85 default_ttl: Duration::from_secs(DEFAULT_CACHE_TTL_SECS),
86 })
87 }
88
89 pub fn with_default_ttl(mut self, ttl: Duration) -> Self {
91 self.default_ttl = ttl;
92 self
93 }
94
95 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
97 let path = self.cache_dir.join(format!("{}.json", key));
98 if !path.exists() {
99 return Ok(None);
100 }
101
102 let content = std::fs::read_to_string(&path)
103 .map_err(|e| DiscoveryError::Cache(format!("Failed to read cache: {e}")))?;
104
105 let entry: CacheEntry = serde_json::from_str(&content)
106 .map_err(|e| DiscoveryError::Cache(format!("Failed to parse cache: {e}")))?;
107
108 if entry.is_expired() {
109 let _ = std::fs::remove_file(&path);
111 Ok(None)
112 } else {
113 let value: T = serde_json::from_value(entry.data).map_err(|e| {
114 DiscoveryError::Cache(format!("Failed to deserialize cache data: {e}"))
115 })?;
116 Ok(Some(value))
117 }
118 }
119
120 pub fn set<T: Serialize>(&self, key: &str, data: T) -> Result<()> {
122 self.set_with_ttl(key, data, self.default_ttl.as_secs())
123 }
124
125 pub fn set_with_ttl<T: Serialize>(&self, key: &str, data: T, ttl_secs: u64) -> Result<()> {
127 let path = self.cache_dir.join(format!("{}.json", key));
128 let entry = CacheEntry::new(data, ttl_secs)?;
129
130 let content = serde_json::to_string_pretty(&entry)
131 .map_err(|e| DiscoveryError::Cache(format!("Failed to serialize cache: {e}")))?;
132
133 std::fs::write(&path, content)
134 .map_err(|e| DiscoveryError::Cache(format!("Failed to write cache: {e}")))?;
135
136 Ok(())
137 }
138
139 pub fn remove(&self, key: &str) -> Result<()> {
141 let path = self.cache_dir.join(format!("{}.json", key));
142 if path.exists() {
143 std::fs::remove_file(&path)
144 .map_err(|e| DiscoveryError::Cache(format!("Failed to remove cache: {e}")))?;
145 }
146 Ok(())
147 }
148
149 pub fn clear(&self) -> Result<()> {
151 if self.cache_dir.exists() {
152 for entry in std::fs::read_dir(&self.cache_dir)
153 .map_err(|e| DiscoveryError::Cache(format!("Failed to read cache dir: {e}")))?
154 {
155 let entry = entry
156 .map_err(|e| DiscoveryError::Cache(format!("Failed to read dir entry: {e}")))?;
157 if entry.path().extension().is_some_and(|ext| ext == "json") {
158 let _ = std::fs::remove_file(entry.path());
159 }
160 }
161 }
162 Ok(())
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_cache_entry_new() {
172 let entry = CacheEntry::new("test_data", 3600).unwrap();
173 assert!(!entry.is_expired());
174 assert_eq!(
175 entry.data,
176 serde_json::Value::String("test_data".to_string())
177 );
178 }
179
180 #[test]
181 fn test_cache_entry_expired() {
182 let mut entry = CacheEntry::new("test_data", 1).unwrap();
183 entry.created_at = 0; assert!(entry.is_expired());
185 }
186
187 #[test]
188 fn test_cache_manager_crud() {
189 let dir = std::env::temp_dir().join("opm-test-cache");
190 let manager = CacheManager::with_dir(dir.clone()).unwrap();
191
192 manager.set("test_key", "test_value").unwrap();
193 let value: Option<String> = manager.get("test_key").unwrap();
194 assert_eq!(value, Some("test_value".to_string()));
195
196 manager.remove("test_key").unwrap();
197 let value: Option<String> = manager.get("test_key").unwrap();
198 assert_eq!(value, None);
199
200 let _ = std::fs::remove_dir_all(&dir);
202 }
203}