1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10use serde::{Deserialize, Serialize};
11
12use crate::types::RomList;
13
14const DEFAULT_CACHE_FILE: &str = "romm-cache.json";
16
17#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
22pub enum RomCacheKey {
23 Platform(u64),
24 Collection(u64),
26 SmartCollection(u64),
28 VirtualCollection(String),
30}
31
32#[derive(Serialize, Deserialize)]
37struct CacheFile {
38 version: u32,
39 entries: Vec<CacheEntry>,
40}
41
42#[derive(Serialize, Deserialize)]
43struct CacheEntry {
44 key: RomCacheKey,
45 expected_count: u64,
49 data: RomList,
50}
51
52pub struct RomCache {
62 entries: HashMap<RomCacheKey, (u64, RomList)>, path: PathBuf,
64}
65
66impl RomCache {
67 pub fn load() -> Self {
69 let path = PathBuf::from(
70 std::env::var("ROMM_CACHE_PATH").unwrap_or_else(|_| DEFAULT_CACHE_FILE.to_string()),
71 );
72 Self::load_from(path)
73 }
74
75 fn load_from(path: PathBuf) -> Self {
76 let entries = Self::read_file(&path).unwrap_or_default();
77 Self { entries, path }
78 }
79
80 fn read_file(path: &Path) -> Option<HashMap<RomCacheKey, (u64, RomList)>> {
81 let data = std::fs::read_to_string(path).ok()?;
82 let file: CacheFile = serde_json::from_str(&data).ok()?;
83 if file.version != 1 {
84 return None;
85 }
86 let map = file
87 .entries
88 .into_iter()
89 .map(|e| (e.key, (e.expected_count, e.data)))
90 .collect();
91 Some(map)
92 }
93
94 pub fn save(&self) {
96 let file = CacheFile {
97 version: 1,
98 entries: self
99 .entries
100 .iter()
101 .map(|(k, (ec, v))| CacheEntry {
102 key: k.clone(),
103 expected_count: *ec,
104 data: v.clone(),
105 })
106 .collect(),
107 };
108 match serde_json::to_string(&file) {
109 Ok(json) => {
110 if let Err(err) = std::fs::write(&self.path, json) {
111 eprintln!(
112 "warning: failed to write ROM cache file {:?}: {}",
113 self.path, err
114 );
115 }
116 }
117 Err(err) => {
118 eprintln!(
119 "warning: failed to serialize ROM cache file {:?}: {}",
120 self.path, err
121 );
122 }
123 }
124 }
125
126 pub fn get_valid(&self, key: &RomCacheKey, expected_count: u64) -> Option<&RomList> {
131 self.entries
132 .get(key)
133 .filter(|(stored_count, _)| *stored_count == expected_count)
134 .map(|(_, list)| list)
135 }
136
137 pub fn insert(&mut self, key: RomCacheKey, data: RomList, expected_count: u64) {
140 self.entries.insert(key, (expected_count, data));
141 self.save();
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::types::Rom;
149 use std::time::{SystemTime, UNIX_EPOCH};
150
151 fn sample_rom_list() -> RomList {
152 RomList {
153 items: vec![Rom {
154 id: 1,
155 platform_id: 10,
156 platform_slug: None,
157 platform_fs_slug: None,
158 platform_custom_name: Some("NES".to_string()),
159 platform_display_name: Some("NES".to_string()),
160 fs_name: "Mario (USA).zip".to_string(),
161 fs_name_no_tags: "Mario".to_string(),
162 fs_name_no_ext: "Mario".to_string(),
163 fs_extension: "zip".to_string(),
164 fs_path: "/roms/mario.zip".to_string(),
165 fs_size_bytes: 1234,
166 name: "Mario".to_string(),
167 slug: Some("mario".to_string()),
168 summary: Some("A platform game".to_string()),
169 path_cover_small: None,
170 path_cover_large: None,
171 url_cover: None,
172 is_unidentified: false,
173 is_identified: true,
174 }],
175 total: 1,
176 limit: 50,
177 offset: 0,
178 }
179 }
180
181 fn temp_cache_path() -> PathBuf {
182 let ts = SystemTime::now()
183 .duration_since(UNIX_EPOCH)
184 .expect("time")
185 .as_nanos();
186 std::env::temp_dir().join(format!("romm-cache-test-{}.json", ts))
187 }
188
189 #[test]
190 fn returns_cache_only_for_matching_expected_count() {
191 let path = temp_cache_path();
192 let mut cache = RomCache::load_from(path.clone());
193 let key = RomCacheKey::Platform(42);
194 let list = sample_rom_list();
195 cache.insert(key.clone(), list.clone(), 7);
196
197 assert!(cache.get_valid(&key, 7).is_some());
198 assert!(cache.get_valid(&key, 8).is_none());
199
200 let _ = std::fs::remove_file(path);
201 }
202
203 #[test]
204 fn persists_and_reloads_entries_from_disk() {
205 let path = temp_cache_path();
206 let mut cache = RomCache::load_from(path.clone());
207 let key = RomCacheKey::Collection(9);
208 let list = sample_rom_list();
209 cache.insert(key.clone(), list.clone(), 3);
210
211 let loaded = RomCache::load_from(path.clone());
212 let cached = loaded.get_valid(&key, 3).expect("cached value");
213 assert_eq!(cached.items.len(), 1);
214 assert_eq!(cached.items[0].name, "Mario");
215
216 let _ = std::fs::remove_file(path);
217 }
218
219 #[test]
220 fn persists_virtual_collection_key() {
221 let path = temp_cache_path();
222 let mut cache = RomCache::load_from(path.clone());
223 let key = RomCacheKey::VirtualCollection("recent".to_string());
224 let list = sample_rom_list();
225 cache.insert(key.clone(), list.clone(), 5);
226
227 let loaded = RomCache::load_from(path.clone());
228 let cached = loaded.get_valid(&key, 5).expect("cached value");
229 assert_eq!(cached.items.len(), 1);
230
231 let _ = std::fs::remove_file(path);
232 }
233}