1use std::collections::HashMap;
16use std::sync::Arc;
17
18use async_trait::async_trait;
19use tokio::sync::RwLock;
20
21use crate::policy::Policy;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct CachedPolicy {
26 pub id: compact_str::CompactString,
31 pub policy: Policy,
33 pub fetched_at_unix_secs: u64,
37}
38
39#[async_trait]
45pub trait Cache: Send + Sync {
46 async fn get(&self, recipient_domain: &str) -> Option<CachedPolicy>;
50 async fn put(&self, recipient_domain: &str, entry: CachedPolicy);
52 async fn delete(&self, recipient_domain: &str);
55}
56
57#[derive(Debug, Clone, Default)]
63pub struct InMemoryCache {
64 inner: Arc<RwLock<HashMap<String, CachedPolicy>>>,
65}
66
67impl InMemoryCache {
68 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub async fn len(&self) -> usize {
75 self.inner.read().await.len()
76 }
77
78 pub async fn is_empty(&self) -> bool {
80 self.inner.read().await.is_empty()
81 }
82}
83
84#[async_trait]
85impl Cache for InMemoryCache {
86 async fn get(&self, recipient_domain: &str) -> Option<CachedPolicy> {
87 let key = recipient_domain.trim().to_ascii_lowercase();
88 self.inner.read().await.get(&key).cloned()
89 }
90 async fn put(&self, recipient_domain: &str, entry: CachedPolicy) {
91 let key = recipient_domain.trim().to_ascii_lowercase();
92 self.inner.write().await.insert(key, entry);
93 }
94 async fn delete(&self, recipient_domain: &str) {
95 let key = recipient_domain.trim().to_ascii_lowercase();
96 self.inner.write().await.remove(&key);
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::policy::{Policy, PolicyMode};
104
105 fn sample_policy() -> Policy {
106 Policy {
107 mode: PolicyMode::Enforce,
108 mx: vec!["mail.example.com".into()],
109 max_age: 86400,
110 }
111 }
112
113 #[tokio::test]
114 async fn in_memory_cache_round_trips() {
115 let cache = InMemoryCache::new();
116 let entry = CachedPolicy {
117 id: "20200101".into(),
118 policy: sample_policy(),
119 fetched_at_unix_secs: 1_700_000_000,
120 };
121 cache.put("example.com", entry.clone()).await;
122 let got = cache.get("example.com").await.unwrap();
123 assert_eq!(got.id, "20200101");
124 assert_eq!(got.policy.max_age, 86400);
125 }
126
127 #[tokio::test]
128 async fn in_memory_cache_key_is_case_insensitive() {
129 let cache = InMemoryCache::new();
130 let entry = CachedPolicy {
131 id: "abc".into(),
132 policy: sample_policy(),
133 fetched_at_unix_secs: 0,
134 };
135 cache.put("Example.COM", entry.clone()).await;
136 assert_eq!(cache.get("example.com").await.unwrap().id, "abc");
137 assert_eq!(cache.get("EXAMPLE.COM").await.unwrap().id, "abc");
138 }
139
140 #[tokio::test]
141 async fn in_memory_cache_delete_removes_entry() {
142 let cache = InMemoryCache::new();
143 cache
144 .put(
145 "example.com",
146 CachedPolicy {
147 id: "x".into(),
148 policy: sample_policy(),
149 fetched_at_unix_secs: 0,
150 },
151 )
152 .await;
153 assert!(cache.get("example.com").await.is_some());
154 cache.delete("example.com").await;
155 assert!(cache.get("example.com").await.is_none());
156 }
157
158 #[tokio::test]
159 async fn in_memory_cache_put_overwrites() {
160 let cache = InMemoryCache::new();
161 cache
162 .put(
163 "x.com",
164 CachedPolicy {
165 id: "v1".into(),
166 policy: sample_policy(),
167 fetched_at_unix_secs: 100,
168 },
169 )
170 .await;
171 cache
172 .put(
173 "x.com",
174 CachedPolicy {
175 id: "v2".into(),
176 policy: sample_policy(),
177 fetched_at_unix_secs: 200,
178 },
179 )
180 .await;
181 let got = cache.get("x.com").await.unwrap();
182 assert_eq!(got.id, "v2");
183 assert_eq!(got.fetched_at_unix_secs, 200);
184 }
185
186 #[tokio::test]
187 async fn in_memory_cache_len_and_is_empty() {
188 let cache = InMemoryCache::new();
189 assert!(cache.is_empty().await);
190 assert_eq!(cache.len().await, 0);
191 cache
192 .put(
193 "x.com",
194 CachedPolicy {
195 id: "v".into(),
196 policy: sample_policy(),
197 fetched_at_unix_secs: 0,
198 },
199 )
200 .await;
201 assert!(!cache.is_empty().await);
202 assert_eq!(cache.len().await, 1);
203 }
204}