dig_wallet/
file_cache.rs

1use crate::error::WalletError;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::marker::PhantomData;
5use std::path::{Path, PathBuf};
6
7/// A simple file-based cache implementation similar to the TypeScript FileCache
8pub struct FileCache<T>
9where
10    T: Serialize + for<'de> Deserialize<'de>,
11{
12    cache_dir: PathBuf,
13    _phantom: PhantomData<T>,
14}
15
16impl<T> FileCache<T>
17where
18    T: Serialize + for<'de> Deserialize<'de>,
19{
20    /// Create a new FileCache instance
21    pub fn new(relative_file_path: &str, base_dir: Option<&Path>) -> Result<Self, WalletError> {
22        let base_path = match base_dir {
23            Some(dir) => dir.to_path_buf(),
24            None => dirs::home_dir()
25                .ok_or_else(|| {
26                    WalletError::FileSystemError("Could not find home directory".to_string())
27                })?
28                .join(".dig"),
29        };
30
31        let cache_dir = base_path.join(relative_file_path);
32
33        let cache = Self {
34            cache_dir,
35            _phantom: PhantomData,
36        };
37        cache.ensure_directory_exists()?;
38
39        Ok(cache)
40    }
41
42    /// Ensure the cache directory exists
43    fn ensure_directory_exists(&self) -> Result<(), WalletError> {
44        if !self.cache_dir.exists() {
45            fs::create_dir_all(&self.cache_dir).map_err(|e| {
46                WalletError::FileSystemError(format!("Failed to create cache directory: {}", e))
47            })?;
48        }
49        Ok(())
50    }
51
52    /// Get the cache file path for a given key
53    fn get_cache_file_path(&self, key: &str) -> PathBuf {
54        self.cache_dir.join(format!("{}.json", key))
55    }
56
57    /// Retrieve cached data by key
58    pub fn get(&self, key: &str) -> Result<Option<T>, WalletError> {
59        let cache_file_path = self.get_cache_file_path(key);
60
61        if !cache_file_path.exists() {
62            return Ok(None);
63        }
64
65        let raw_data = fs::read_to_string(&cache_file_path).map_err(|e| {
66            WalletError::FileSystemError(format!("Failed to read cache file: {}", e))
67        })?;
68
69        let data: T = serde_json::from_str(&raw_data).map_err(|e| {
70            WalletError::SerializationError(format!("Failed to deserialize cache data: {}", e))
71        })?;
72
73        Ok(Some(data))
74    }
75
76    /// Save data to the cache
77    pub fn set(&self, key: &str, data: &T) -> Result<(), WalletError> {
78        let cache_file_path = self.get_cache_file_path(key);
79
80        let serialized_data = serde_json::to_string_pretty(data).map_err(|e| {
81            WalletError::SerializationError(format!("Failed to serialize cache data: {}", e))
82        })?;
83
84        fs::write(&cache_file_path, serialized_data).map_err(|e| {
85            WalletError::FileSystemError(format!("Failed to write cache file: {}", e))
86        })?;
87
88        Ok(())
89    }
90
91    /// Delete cached data by key
92    pub fn delete(&self, key: &str) -> Result<(), WalletError> {
93        let cache_file_path = self.get_cache_file_path(key);
94
95        if cache_file_path.exists() {
96            fs::remove_file(&cache_file_path).map_err(|e| {
97                WalletError::FileSystemError(format!("Failed to delete cache file: {}", e))
98            })?;
99        }
100
101        Ok(())
102    }
103
104    /// Retrieve all cached keys in the directory
105    pub fn get_cached_keys(&self) -> Result<Vec<String>, WalletError> {
106        if !self.cache_dir.exists() {
107            return Ok(vec![]);
108        }
109
110        let entries = fs::read_dir(&self.cache_dir).map_err(|e| {
111            WalletError::FileSystemError(format!("Failed to read cache directory: {}", e))
112        })?;
113
114        let mut keys = Vec::new();
115
116        for entry in entries {
117            let entry = entry.map_err(|e| {
118                WalletError::FileSystemError(format!("Failed to read directory entry: {}", e))
119            })?;
120
121            if let Some(file_name) = entry.file_name().to_str() {
122                if file_name.ends_with(".json") {
123                    let key = file_name.strip_suffix(".json").unwrap_or(file_name);
124                    keys.push(key.to_string());
125                }
126            }
127        }
128
129        Ok(keys)
130    }
131
132    /// Clear all cached data
133    pub fn clear(&self) -> Result<(), WalletError> {
134        let keys = self.get_cached_keys()?;
135
136        for key in keys {
137            self.delete(&key)?;
138        }
139
140        Ok(())
141    }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ReservedCoinCache {
146    pub coin_id: String,
147    pub expiry: u64,
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use serde::{Deserialize, Serialize};
154    use tempfile::TempDir;
155
156    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
157    struct TestData {
158        value: String,
159        number: i32,
160    }
161
162    #[test]
163    fn test_file_cache_operations() {
164        let temp_dir = TempDir::new().unwrap();
165        let cache = FileCache::<TestData>::new("test_cache", Some(temp_dir.path())).unwrap();
166
167        let test_data = TestData {
168            value: "test".to_string(),
169            number: 42,
170        };
171
172        // Test set and get
173        cache.set("test_key", &test_data).unwrap();
174        let retrieved = cache.get("test_key").unwrap().unwrap();
175        assert_eq!(retrieved, test_data);
176
177        // Test get non-existent key
178        let non_existent = cache.get("non_existent").unwrap();
179        assert!(non_existent.is_none());
180
181        // Test get_cached_keys
182        let keys = cache.get_cached_keys().unwrap();
183        assert_eq!(keys, vec!["test_key"]);
184
185        // Test delete
186        cache.delete("test_key").unwrap();
187        let deleted = cache.get("test_key").unwrap();
188        assert!(deleted.is_none());
189    }
190}