1use crate::{Record, Storage, StorageKey};
2use origin_domain::{AppError, Clock, Result};
3use serde::Serialize;
4use serde::de::DeserializeOwned;
5use std::sync::Arc;
6use time::Duration;
7
8#[derive(Debug, Clone)]
14pub struct Cache {
15 storage: Arc<dyn Storage>,
16 clock: Arc<dyn Clock>,
17}
18
19impl Cache {
20 pub fn new(storage: Arc<dyn Storage>, clock: Arc<dyn Clock>) -> Self {
21 Self { storage, clock }
22 }
23
24 pub async fn get<T: DeserializeOwned>(&self, key: &StorageKey) -> Result<Option<T>> {
26 let Some(record) = self.storage.get(key).await? else {
27 return Ok(None);
28 };
29
30 if record.is_expired_at(self.clock.now()) {
31 return Ok(None);
32 }
33
34 let value = serde_json::from_str(&record.value)
35 .map_err(|error| AppError::storage(format!("cannot decode {key:?}: {error}")))?;
36 Ok(Some(value))
37 }
38
39 pub async fn put<T: Serialize>(
41 &self,
42 key: &StorageKey,
43 value: &T,
44 ttl: Option<Duration>,
45 ) -> Result<()> {
46 let encoded = serde_json::to_string(value)
47 .map_err(|error| AppError::storage(format!("cannot encode {key:?}: {error}")))?;
48
49 let now = self.clock.now();
50 let mut record = Record::new(encoded, now);
51 if let Some(ttl) = ttl {
52 record = record.expiring_at(now + ttl);
53 }
54
55 self.storage.put(key, record).await
56 }
57
58 pub async fn invalidate(&self, key: &StorageKey) -> Result<()> {
59 self.storage.delete(key).await
60 }
61
62 pub async fn invalidate_namespace(&self, namespace: &str) -> Result<()> {
63 self.storage.clear(namespace).await
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use crate::MemoryStorage;
71 use origin_domain::testing::FakeClock;
72 use time::macros::datetime;
73
74 fn cache() -> (Cache, Arc<FakeClock>) {
75 let clock = Arc::new(FakeClock::new(datetime!(2026-08-23 10:00 UTC)));
76 let cache = Cache::new(Arc::new(MemoryStorage::new()), clock.clone());
77 (cache, clock)
78 }
79
80 #[tokio::test]
81 async fn a_value_survives_until_its_ttl_expires() {
82 let (cache, clock) = cache();
83 let key = StorageKey::new("github", "notifications");
84
85 cache
86 .put(&key, &vec!["a", "b"], Some(Duration::minutes(5)))
87 .await
88 .unwrap();
89
90 clock.advance(Duration::minutes(4));
91 let fresh: Option<Vec<String>> = cache.get(&key).await.unwrap();
92 assert_eq!(
93 fresh.as_deref(),
94 Some(&["a".to_string(), "b".to_string()][..])
95 );
96
97 clock.advance(Duration::minutes(2));
98 let stale: Option<Vec<String>> = cache.get(&key).await.unwrap();
99 assert_eq!(
100 stale, None,
101 "the value must be treated as stale after its TTL"
102 );
103 }
104
105 #[tokio::test]
106 async fn a_value_without_ttl_never_expires() {
107 let (cache, clock) = cache();
108 let key = StorageKey::new("settings", "theme");
109
110 cache.put(&key, &"dark", None).await.unwrap();
111 clock.advance(Duration::days(365));
112
113 assert_eq!(
114 cache.get::<String>(&key).await.unwrap().as_deref(),
115 Some("dark")
116 );
117 }
118
119 #[tokio::test]
120 async fn decoding_a_value_as_the_wrong_type_is_a_storage_error() {
121 let (cache, _clock) = cache();
122 let key = StorageKey::new("github", "count");
123 cache.put(&key, &"not-a-number", None).await.unwrap();
124
125 let error = cache.get::<u32>(&key).await.unwrap_err();
126 assert_eq!(error.kind(), origin_domain::ErrorKind::Storage);
127 }
128}