1pub mod config;
37pub mod file;
38pub mod idempotency;
39pub mod memory;
40pub mod rate_limit;
41pub mod redis;
42pub mod store;
43pub mod throttle;
44
45pub use config::{CacheConfig, CacheStore, Driver};
46pub use file::FileStore;
47pub use idempotency::Idempotency;
48pub use memory::MemoryStore;
49pub use rate_limit::{RateLimit, RateLimiter};
50pub use redis::{RedisConfig, RedisStore};
51pub use store::{BoxFuture, Cache, CacheExt};
52pub use throttle::Throttle;
53
54pub use rustlavel_core::{Error, Json, Result};
55
56pub mod prelude {
58 pub use crate::{Cache, CacheExt, CacheStore, Idempotency, RateLimiter, Throttle};
59 pub use rustlavel_core::{Json, Result};
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
73 use std::sync::Arc;
74 use std::time::Duration;
75
76 async fn assert_cache_contract(cache: &dyn Cache) {
78 cache.flush().await.unwrap();
79
80 assert_eq!(cache.get("absent").await.unwrap(), None);
82 assert!(!cache.has("absent").await.unwrap());
83 assert!(!cache.forget("absent").await.unwrap());
84
85 for value in [
87 Json::Null,
88 Json::from(true),
89 Json::from(-17),
90 Json::from(1.5),
91 Json::from("a string with \" and \\ and \n in it"),
92 Json::from(vec![1, 2, 3]),
93 Json::object([("nested", Json::object([("deep", Json::from(true))]))]),
94 ] {
95 cache.put("shape", value.clone(), Duration::from_secs(60)).await.unwrap();
96 assert_eq!(cache.get("shape").await.unwrap(), Some(value.clone()), "round trip failed");
97 }
98
99 cache.forever("immortal", Json::from("forever")).await.unwrap();
101 assert_eq!(cache.ttl("immortal").await.unwrap(), None);
102 assert!(cache.has("immortal").await.unwrap());
103
104 assert!(cache.forget("immortal").await.unwrap());
106 assert!(!cache.forget("immortal").await.unwrap());
107
108 cache.put("brief", Json::from("gone soon"), Duration::from_millis(120)).await.unwrap();
110 assert!(cache.has("brief").await.unwrap());
111 assert!(cache.ttl("brief").await.unwrap().is_some());
112 tokio::time::sleep(Duration::from_millis(220)).await;
113 assert_eq!(cache.get("brief").await.unwrap(), None, "the TTL did not expire the key");
114 assert!(!cache.has("brief").await.unwrap());
115
116 assert_eq!(cache.increment("counter", 1).await.unwrap(), 1);
118 assert_eq!(cache.increment("counter", 4).await.unwrap(), 5);
119 assert_eq!(cache.decrement("counter", 2).await.unwrap(), 3);
120 assert_eq!(cache.get("counter").await.unwrap(), Some(Json::from(3)));
121 assert_eq!(cache.decrement("fresh-counter", 3).await.unwrap(), -3);
122
123 assert_eq!(cache.increment_within("window", 1, Duration::from_secs(60)).await.unwrap(), 1);
125 assert_eq!(cache.increment_within("window", 1, Duration::from_secs(60)).await.unwrap(), 2);
126 let remaining = cache.ttl("window").await.unwrap().expect("a window has a deadline");
127 assert!(remaining <= Duration::from_secs(60));
128
129 let computed = cache
131 .remember("remembered", Duration::from_secs(60), || async { Ok(Json::from("first")) })
132 .await
133 .unwrap();
134 assert_eq!(computed, Json::from("first"));
135
136 let cached = cache
137 .remember("remembered", Duration::from_secs(60), || async {
138 panic!("remember must not recompute a hit")
139 })
140 .await
141 .unwrap();
142 assert_eq!(cached, Json::from("first"));
143
144 cache
146 .remember_forever("remembered-forever", || async { Ok(Json::from(7)) })
147 .await
148 .unwrap();
149 assert_eq!(cache.ttl("remembered-forever").await.unwrap(), None);
150
151 assert_eq!(cache.pull("remembered").await.unwrap(), Some(Json::from("first")));
153 assert_eq!(cache.pull("remembered").await.unwrap(), None);
154
155 cache.forever("doomed", Json::from(1)).await.unwrap();
157 cache.put("doomed", Json::from(2), Duration::ZERO).await.unwrap();
158 assert!(!cache.has("doomed").await.unwrap());
159
160 cache.forever("a", Json::from(1)).await.unwrap();
162 cache.forever("b", Json::from(2)).await.unwrap();
163 cache.flush().await.unwrap();
164 assert_eq!(cache.get("a").await.unwrap(), None);
165 assert_eq!(cache.get("b").await.unwrap(), None);
166 assert_eq!(cache.get("counter").await.unwrap(), None);
167 }
168
169 #[tokio::test]
170 async fn the_memory_driver_satisfies_the_cache_contract() {
171 assert_cache_contract(&MemoryStore::new()).await;
172 }
173
174 #[tokio::test]
175 async fn the_file_driver_satisfies_the_cache_contract() {
176 let directory = std::env::temp_dir()
179 .join(format!("rustlavel-cache-contract-{}", std::process::id()));
180 let _ = std::fs::remove_dir_all(&directory);
181
182 assert_cache_contract(&FileStore::new(&directory).unwrap()).await;
183
184 let _ = std::fs::remove_dir_all(&directory);
185 }
186
187 #[tokio::test]
188 async fn a_boxed_driver_satisfies_the_contract_too() {
189 let cache: Arc<dyn Cache> = Arc::new(MemoryStore::new());
192 assert_cache_contract(cache.as_ref()).await;
193 assert_eq!(cache.driver(), "memory");
194 }
195
196 #[tokio::test]
197 async fn a_lookup_dispatches_a_hit_or_a_miss_event() {
198 use rustlavel_core::events::{self, Event};
199 use std::sync::Mutex;
200
201 let marker = "event-probe:";
205 events::clear_subscribers();
206
207 let seen: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(Vec::new()));
208 let sink = Arc::clone(&seen);
209 events::subscribe(move |event: &Event| {
210 let key = event.field("key").and_then(Json::as_str).unwrap_or_default();
211 if event.kind.starts_with("cache.") && key.starts_with(marker) {
212 sink.lock().unwrap().push((event.kind.to_string(), key.to_string()));
213 }
214 });
215
216 let cache = MemoryStore::new();
217 cache.get("event-probe:missing").await.unwrap();
218 cache.forever("event-probe:present", Json::from(1)).await.unwrap();
219 cache.get("event-probe:present").await.unwrap();
220
221 let names: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
223 let slot = Arc::clone(&names);
224 events::subscribe(move |event: &Event| {
225 if event.kind == "cache.miss" {
226 *slot.lock().unwrap() =
227 event.field("store").and_then(Json::as_str).map(str::to_string);
228 }
229 });
230 cache.get("event-probe:another-miss").await.unwrap();
231 let store_name = names.lock().unwrap().clone();
232
233 let recorded = seen.lock().unwrap().clone();
234 events::clear_subscribers();
235
236 assert_eq!(store_name.as_deref(), Some("memory"));
237 assert_eq!(
238 recorded,
239 vec![
240 ("cache.miss".to_string(), "event-probe:missing".to_string()),
241 ("cache.hit".to_string(), "event-probe:present".to_string()),
242 ("cache.miss".to_string(), "event-probe:another-miss".to_string()),
243 ]
244 );
245 }
246}