use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct Favorites {
path: PathBuf,
entries: HashSet<String>,
}
impl Favorites {
pub fn load(path: &Path) -> Self {
let entries = std::fs::read_to_string(path)
.ok()
.and_then(|data| serde_json::from_str::<Vec<String>>(&data).ok())
.unwrap_or_default()
.into_iter()
.collect();
Self {
path: path.to_path_buf(),
entries,
}
}
pub fn contains(&self, rom_path: &Path) -> bool {
self.entries
.contains(&rom_path.to_string_lossy().to_string())
}
pub fn toggle(&mut self, rom_path: &Path) -> bool {
let key = rom_path.to_string_lossy().to_string();
if self.entries.contains(&key) {
self.entries.remove(&key);
false
} else {
self.entries.insert(key);
true
}
}
pub fn save(&self) -> Result<(), String> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create favorites dir: {e}"))?;
}
let sorted: Vec<&String> = {
let mut v: Vec<&String> = self.entries.iter().collect();
v.sort();
v
};
let json = serde_json::to_string_pretty(&sorted).map_err(|e| format!("JSON error: {e}"))?;
std::fs::write(&self.path, json).map_err(|e| format!("Write error: {e}"))
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn load_returns_empty_when_file_missing() {
let fav = Favorites::load(Path::new("/nonexistent/favorites.json"));
assert!(fav.is_empty());
}
#[test]
fn load_parses_json_array() {
let tmp = tempfile::NamedTempFile::new().unwrap();
write!(tmp.as_file(), r#"["/roms/a.nes", "/roms/b.nes"]"#).unwrap();
let fav = Favorites::load(tmp.path());
assert_eq!(fav.len(), 2);
assert!(fav.contains(Path::new("/roms/a.nes")));
assert!(fav.contains(Path::new("/roms/b.nes")));
}
#[test]
fn load_handles_malformed_json() {
let tmp = tempfile::NamedTempFile::new().unwrap();
write!(tmp.as_file(), "not json at all").unwrap();
let fav = Favorites::load(tmp.path());
assert!(fav.is_empty());
}
#[test]
fn toggle_adds_then_removes() {
let fav_path = tempfile::NamedTempFile::new().unwrap();
let mut fav = Favorites::load(fav_path.path());
let rom = Path::new("/roms/test.nes");
assert!(!fav.contains(rom));
let added = fav.toggle(rom);
assert!(added);
assert!(fav.contains(rom));
let removed = fav.toggle(rom);
assert!(!removed);
assert!(!fav.contains(rom));
}
#[test]
fn save_and_reload_round_trips() {
let dir = tempfile::TempDir::new().unwrap();
let fav_path = dir.path().join("favorites.json");
let mut fav = Favorites::load(&fav_path);
fav.toggle(Path::new("/roms/zelda.nes"));
fav.toggle(Path::new("/roms/mario.nes"));
fav.save().unwrap();
let reloaded = Favorites::load(&fav_path);
assert_eq!(reloaded.len(), 2);
assert!(reloaded.contains(Path::new("/roms/zelda.nes")));
assert!(reloaded.contains(Path::new("/roms/mario.nes")));
}
#[test]
fn save_creates_parent_directories() {
let dir = tempfile::TempDir::new().unwrap();
let fav_path = dir.path().join("sub").join("dir").join("favorites.json");
let mut fav = Favorites::load(&fav_path);
fav.toggle(Path::new("/roms/test.nes"));
fav.save().unwrap();
assert!(fav_path.exists());
}
#[test]
fn saved_json_is_sorted() {
let dir = tempfile::TempDir::new().unwrap();
let fav_path = dir.path().join("favorites.json");
let mut fav = Favorites::load(&fav_path);
fav.toggle(Path::new("/roms/zelda.nes"));
fav.toggle(Path::new("/roms/contra.nes"));
fav.toggle(Path::new("/roms/mario.nes"));
fav.save().unwrap();
let data = std::fs::read_to_string(&fav_path).unwrap();
let list: Vec<String> = serde_json::from_str(&data).unwrap();
assert_eq!(
list,
vec!["/roms/contra.nes", "/roms/mario.nes", "/roms/zelda.nes"]
);
}
}