1use {
2 crate::error::{PolymarketError, Result},
3 serde::{Deserialize, Serialize},
4 std::{
5 fs,
6 path::{Path, PathBuf},
7 time::{SystemTime, UNIX_EPOCH},
8 },
9};
10
11#[derive(Clone)]
13pub struct FileCache {
14 cache_dir: PathBuf,
15 default_ttl_seconds: Option<u64>,
16}
17
18#[derive(Debug, Serialize, Deserialize)]
19struct CacheEntry<T> {
20 data: T,
21 cached_at: u64,
22 ttl_seconds: Option<u64>,
23}
24
25impl FileCache {
26 pub fn new<P: AsRef<Path>>(cache_dir: P) -> Result<Self> {
28 let cache_dir = cache_dir.as_ref().to_path_buf();
29 fs::create_dir_all(&cache_dir).map_err(|e| {
30 PolymarketError::InvalidData(format!("Failed to create cache directory: {}", e))
31 })?;
32
33 Ok(Self {
34 cache_dir,
35 default_ttl_seconds: None,
36 })
37 }
38
39 pub fn with_default_ttl(mut self, ttl_seconds: u64) -> Self {
41 self.default_ttl_seconds = Some(ttl_seconds);
42 self
43 }
44
45 pub fn get<T>(&self, key: &str) -> Result<Option<T>>
47 where
48 T: for<'de> Deserialize<'de>,
49 {
50 let cache_file = self.cache_file_path(key);
51
52 if !cache_file.exists() {
53 return Ok(None);
54 }
55
56 let content = fs::read_to_string(&cache_file).map_err(|e| {
57 PolymarketError::InvalidData(format!("Failed to read cache file: {}", e))
58 })?;
59
60 let entry: CacheEntry<T> =
61 serde_json::from_str(&content).map_err(PolymarketError::Serialization)?;
62
63 if let Some(ttl) = entry.ttl_seconds {
65 let now = SystemTime::now()
66 .duration_since(UNIX_EPOCH)
67 .map_err(|e| PolymarketError::InvalidData(format!("System time error: {}", e)))?
68 .as_secs();
69
70 if now.saturating_sub(entry.cached_at) > ttl {
71 let _ = fs::remove_file(&cache_file);
73 return Ok(None);
74 }
75 }
76
77 Ok(Some(entry.data))
78 }
79
80 pub fn set<T>(&self, key: &str, data: T) -> Result<()>
82 where
83 T: Serialize,
84 {
85 let cache_file = self.cache_file_path(key);
86
87 let cached_at = SystemTime::now()
88 .duration_since(UNIX_EPOCH)
89 .map_err(|e| PolymarketError::InvalidData(format!("System time error: {}", e)))?
90 .as_secs();
91
92 let entry = CacheEntry {
93 data,
94 cached_at,
95 ttl_seconds: self.default_ttl_seconds,
96 };
97
98 let json = serde_json::to_string_pretty(&entry).map_err(PolymarketError::Serialization)?;
99
100 let temp_file = cache_file.with_extension("tmp");
102 fs::write(&temp_file, json).map_err(|e| {
103 PolymarketError::InvalidData(format!("Failed to write cache file: {}", e))
104 })?;
105
106 fs::rename(&temp_file, &cache_file).map_err(|e| {
107 PolymarketError::InvalidData(format!("Failed to rename cache file: {}", e))
108 })?;
109
110 Ok(())
111 }
112
113 pub fn remove(&self, key: &str) -> Result<()> {
115 let cache_file = self.cache_file_path(key);
116 if cache_file.exists() {
117 fs::remove_file(&cache_file).map_err(|e| {
118 PolymarketError::InvalidData(format!("Failed to remove cache file: {}", e))
119 })?;
120 }
121 Ok(())
122 }
123
124 pub fn clear(&self) -> Result<()> {
126 if self.cache_dir.exists() {
127 for entry in fs::read_dir(&self.cache_dir).map_err(|e| {
128 PolymarketError::InvalidData(format!("Failed to read cache directory: {}", e))
129 })? {
130 let entry = entry.map_err(|e| {
131 PolymarketError::InvalidData(format!("Failed to read directory entry: {}", e))
132 })?;
133 let path = entry.path();
134 if path.is_file() && path.extension().map(|e| e == "json").unwrap_or(false) {
135 fs::remove_file(&path).map_err(|e| {
136 PolymarketError::InvalidData(format!("Failed to remove cache file: {}", e))
137 })?;
138 }
139 }
140 }
141 Ok(())
142 }
143
144 pub fn cache_dir(&self) -> &Path {
146 &self.cache_dir
147 }
148
149 fn cache_file_path(&self, key: &str) -> PathBuf {
150 let sanitized = key
152 .chars()
153 .map(|c| {
154 if c.is_alphanumeric() || c == '-' || c == '_' {
155 c
156 } else {
157 '_'
158 }
159 })
160 .collect::<String>();
161 self.cache_dir.join(format!("{}.json", sanitized))
162 }
163}
164
165pub fn default_cache_dir() -> PathBuf {
167 dirs::cache_dir()
168 .map(|d| d.join("polymarket-api"))
169 .unwrap_or_else(|| PathBuf::from(".cache"))
170}