use std::hash::{Hash, Hasher};
use dashmap::DashMap;
use crate::types::FileSource;
pub struct FileIdCache {
cache: DashMap<u64, String>,
}
impl FileIdCache {
pub fn new() -> Self {
Self {
cache: DashMap::new(),
}
}
#[must_use]
pub fn get(&self, source: &FileSource) -> Option<FileSource> {
if matches!(source, FileSource::FileId(_)) {
return Some(source.clone());
}
let h = hash_source(source);
self.cache
.get(&h)
.map(|entry| FileSource::FileId(entry.value().clone()))
}
pub fn put(&self, source: &FileSource, file_id: String) {
if matches!(source, FileSource::FileId(_)) {
return;
}
let h = hash_source(source);
self.cache.insert(h, file_id);
}
#[must_use]
pub fn len(&self) -> usize {
self.cache.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn clear(&self) {
self.cache.clear();
}
pub fn remove(&self, source: &FileSource) -> Option<String> {
let h = hash_source(source);
self.cache.remove(&h).map(|(_, v)| v)
}
}
impl Default for FileIdCache {
fn default() -> Self {
Self::new()
}
}
fn hash_source(source: &FileSource) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
source.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_new_cache_is_empty() {
let c = FileIdCache::new();
assert_eq!(c.len(), 0);
assert!(c.is_empty());
}
#[test]
fn test_put_and_get_url() {
let c = FileIdCache::new();
let src = FileSource::Url("https://example.com/img.png".into());
c.put(&src, "AgACAgI_abc".into());
let hit = c.get(&src);
assert!(hit.is_some());
match hit.unwrap() {
FileSource::FileId(id) => assert_eq!(id, "AgACAgI_abc"),
other => panic!("expected FileId, got {:?}", other),
}
assert_eq!(c.len(), 1);
}
#[test]
fn test_put_and_get_local_path() {
let c = FileIdCache::new();
let src = FileSource::LocalPath(PathBuf::from("/tmp/photo.jpg"));
c.put(&src, "file123".into());
assert!(c.get(&src).is_some());
}
#[test]
fn test_put_and_get_bytes() {
let c = FileIdCache::new();
let src = FileSource::Bytes {
data: vec![0xFF, 0xD8, 0xFF],
filename: "pic.jpg".into(),
};
c.put(&src, "bytes_fid".into());
let hit = c.get(&src).unwrap();
assert!(matches!(hit, FileSource::FileId(ref id) if id == "bytes_fid"));
}
#[test]
fn test_miss_returns_none() {
let c = FileIdCache::new();
let src = FileSource::Url("https://example.com/nope.png".into());
assert!(c.get(&src).is_none());
}
#[test]
fn test_file_id_passthrough() {
let c = FileIdCache::new();
let src = FileSource::FileId("already_cached".into());
let hit = c.get(&src).unwrap();
assert!(matches!(hit, FileSource::FileId(ref id) if id == "already_cached"));
assert_eq!(c.len(), 0); }
#[test]
fn test_put_file_id_is_noop() {
let c = FileIdCache::new();
let src = FileSource::FileId("fid".into());
c.put(&src, "other".into());
assert_eq!(c.len(), 0);
}
#[test]
fn test_overwrite() {
let c = FileIdCache::new();
let src = FileSource::Url("https://example.com/img.png".into());
c.put(&src, "v1".into());
c.put(&src, "v2".into());
let hit = c.get(&src).unwrap();
assert!(matches!(hit, FileSource::FileId(ref id) if id == "v2"));
assert_eq!(c.len(), 1);
}
#[test]
fn test_clear() {
let c = FileIdCache::new();
c.put(&FileSource::Url("https://a.com".into()), "a".into());
c.put(&FileSource::Url("https://b.com".into()), "b".into());
assert_eq!(c.len(), 2);
c.clear();
assert_eq!(c.len(), 0);
assert!(c.is_empty());
}
#[test]
fn test_remove() {
let c = FileIdCache::new();
let src = FileSource::Url("https://example.com/img.png".into());
c.put(&src, "fid".into());
assert_eq!(c.len(), 1);
let removed = c.remove(&src);
assert_eq!(removed.as_deref(), Some("fid"));
assert!(c.is_empty());
}
#[test]
fn test_remove_missing() {
let c = FileIdCache::new();
let src = FileSource::Url("https://example.com/nope.png".into());
assert!(c.remove(&src).is_none());
}
#[test]
fn test_different_sources_different_keys() {
let c = FileIdCache::new();
let url = FileSource::Url("https://example.com/img.png".into());
let path = FileSource::LocalPath(PathBuf::from("https://example.com/img.png"));
c.put(&url, "url_fid".into());
c.put(&path, "path_fid".into());
let url_hit = c.get(&url).unwrap();
let path_hit = c.get(&path).unwrap();
assert!(matches!(url_hit, FileSource::FileId(ref id) if id == "url_fid"));
assert!(matches!(path_hit, FileSource::FileId(ref id) if id == "path_fid"));
}
}