use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::RwLock;
use crate::policy::Policy;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CachedPolicy {
pub id: compact_str::CompactString,
pub policy: Policy,
pub fetched_at_unix_secs: u64,
}
#[async_trait]
pub trait Cache: Send + Sync {
async fn get(&self, recipient_domain: &str) -> Option<CachedPolicy>;
async fn put(&self, recipient_domain: &str, entry: CachedPolicy);
async fn delete(&self, recipient_domain: &str);
}
#[derive(Debug, Clone, Default)]
pub struct InMemoryCache {
inner: Arc<RwLock<HashMap<String, CachedPolicy>>>,
}
impl InMemoryCache {
pub fn new() -> Self {
Self::default()
}
pub async fn len(&self) -> usize {
self.inner.read().await.len()
}
pub async fn is_empty(&self) -> bool {
self.inner.read().await.is_empty()
}
}
#[async_trait]
impl Cache for InMemoryCache {
async fn get(&self, recipient_domain: &str) -> Option<CachedPolicy> {
let key = recipient_domain.trim().to_ascii_lowercase();
self.inner.read().await.get(&key).cloned()
}
async fn put(&self, recipient_domain: &str, entry: CachedPolicy) {
let key = recipient_domain.trim().to_ascii_lowercase();
self.inner.write().await.insert(key, entry);
}
async fn delete(&self, recipient_domain: &str) {
let key = recipient_domain.trim().to_ascii_lowercase();
self.inner.write().await.remove(&key);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::{Policy, PolicyMode};
fn sample_policy() -> Policy {
Policy {
mode: PolicyMode::Enforce,
mx: vec!["mail.example.com".into()],
max_age: 86400,
}
}
#[tokio::test]
async fn in_memory_cache_round_trips() {
let cache = InMemoryCache::new();
let entry = CachedPolicy {
id: "20200101".into(),
policy: sample_policy(),
fetched_at_unix_secs: 1_700_000_000,
};
cache.put("example.com", entry.clone()).await;
let got = cache.get("example.com").await.unwrap();
assert_eq!(got.id, "20200101");
assert_eq!(got.policy.max_age, 86400);
}
#[tokio::test]
async fn in_memory_cache_key_is_case_insensitive() {
let cache = InMemoryCache::new();
let entry = CachedPolicy {
id: "abc".into(),
policy: sample_policy(),
fetched_at_unix_secs: 0,
};
cache.put("Example.COM", entry.clone()).await;
assert_eq!(cache.get("example.com").await.unwrap().id, "abc");
assert_eq!(cache.get("EXAMPLE.COM").await.unwrap().id, "abc");
}
#[tokio::test]
async fn in_memory_cache_delete_removes_entry() {
let cache = InMemoryCache::new();
cache
.put(
"example.com",
CachedPolicy {
id: "x".into(),
policy: sample_policy(),
fetched_at_unix_secs: 0,
},
)
.await;
assert!(cache.get("example.com").await.is_some());
cache.delete("example.com").await;
assert!(cache.get("example.com").await.is_none());
}
#[tokio::test]
async fn in_memory_cache_put_overwrites() {
let cache = InMemoryCache::new();
cache
.put(
"x.com",
CachedPolicy {
id: "v1".into(),
policy: sample_policy(),
fetched_at_unix_secs: 100,
},
)
.await;
cache
.put(
"x.com",
CachedPolicy {
id: "v2".into(),
policy: sample_policy(),
fetched_at_unix_secs: 200,
},
)
.await;
let got = cache.get("x.com").await.unwrap();
assert_eq!(got.id, "v2");
assert_eq!(got.fetched_at_unix_secs, 200);
}
#[tokio::test]
async fn in_memory_cache_len_and_is_empty() {
let cache = InMemoryCache::new();
assert!(cache.is_empty().await);
assert_eq!(cache.len().await, 0);
cache
.put(
"x.com",
CachedPolicy {
id: "v".into(),
policy: sample_policy(),
fetched_at_unix_secs: 0,
},
)
.await;
assert!(!cache.is_empty().await);
assert_eq!(cache.len().await, 1);
}
}