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