1use std::time::Duration;
36
37use apiplant_core::CacheConfig;
38use redis::AsyncCommands;
39use serde::{Deserialize, Serialize};
40use serde_json::{json, Value};
41
42#[derive(Debug, thiserror::Error)]
44pub enum CacheError {
45 #[error("cache configuration: {0}")]
47 Config(String),
48
49 #[error("invalid cache request: {0}")]
51 Request(String),
52
53 #[error("cache: {0}")]
55 Backend(String),
56}
57
58#[derive(Clone)]
64pub struct Cache {
65 connection: redis::aio::ConnectionManager,
66 prefix: String,
67 default_ttl_secs: u64,
68 timeout: Duration,
69}
70
71impl std::fmt::Debug for Cache {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 f.debug_struct("Cache")
75 .field("prefix", &self.prefix)
76 .field("default_ttl_secs", &self.default_ttl_secs)
77 .finish()
78 }
79}
80
81impl Cache {
82 pub async fn connect(config: &CacheConfig) -> Result<Option<Cache>, CacheError> {
89 if !config.is_active() {
90 return Ok(None);
91 }
92 let url = config.url.trim();
93 let client =
94 redis::Client::open(url).map_err(|e| CacheError::Config(format!("{url}: {e}")))?;
95 let timeout = Duration::from_secs(config.timeout_secs.max(1));
96
97 let connection = tokio::time::timeout(timeout, redis::aio::ConnectionManager::new(client))
98 .await
99 .map_err(|_| CacheError::Backend(format!("timed out connecting to {url}")))?
100 .map_err(|e| CacheError::Backend(format!("{url}: {e}")))?;
101
102 Ok(Some(Cache {
103 connection,
104 prefix: config.prefix.clone(),
105 default_ttl_secs: config.default_ttl_secs,
106 timeout,
107 }))
108 }
109
110 fn key(&self, key: &str) -> Result<String, CacheError> {
112 prefixed(&self.prefix, key)
113 }
114
115 pub async fn execute(&self, request: &str) -> Result<Value, CacheError> {
123 let op: Op = serde_json::from_str(request)
124 .map_err(|e| CacheError::Request(format!("{e}; expected {}", Op::grammar())))?;
125 match op {
126 Op::Get { key } => {
127 let value = self.get(&key).await?;
128 Ok(json!({ "hit": value.is_some(), "value": value }))
129 }
130 Op::Set { key, value, ttl } => {
131 self.set(&key, &value, ttl).await?;
132 Ok(json!({ "ok": true }))
133 }
134 Op::Delete { key } => Ok(json!({ "deleted": self.delete(&key).await? })),
135 Op::Exists { key } => Ok(json!({ "exists": self.exists(&key).await? })),
136 Op::Incr { key, by, ttl } => {
137 let value = self.incr(&key, by, ttl).await?;
138 Ok(json!({ "value": value }))
139 }
140 Op::Ttl { key } => Ok(json!({ "ttl": self.ttl(&key).await? })),
141 }
142 }
143
144 pub async fn get(&self, key: &str) -> Result<Option<Value>, CacheError> {
150 let key = self.key(key)?;
151 let stored: Option<String> = self.run("get", self.connection.clone().get(&key)).await?;
152 Ok(stored.map(|text| serde_json::from_str(&text).unwrap_or(Value::String(text))))
153 }
154
155 pub async fn set(&self, key: &str, value: &Value, ttl: Option<u64>) -> Result<(), CacheError> {
158 let key = self.key(key)?;
159 let payload =
160 serde_json::to_string(value).map_err(|e| CacheError::Request(e.to_string()))?;
161 let ttl = ttl.unwrap_or(self.default_ttl_secs);
162 let mut connection = self.connection.clone();
163 if ttl == 0 {
164 self.run::<()>("set", connection.set(&key, payload)).await
165 } else {
166 self.run::<()>("setex", connection.set_ex(&key, payload, ttl))
167 .await
168 }
169 }
170
171 pub async fn delete(&self, key: &str) -> Result<bool, CacheError> {
173 let key = self.key(key)?;
174 let removed: i64 = self.run("del", self.connection.clone().del(&key)).await?;
175 Ok(removed > 0)
176 }
177
178 pub async fn exists(&self, key: &str) -> Result<bool, CacheError> {
180 let key = self.key(key)?;
181 self.run("exists", self.connection.clone().exists(&key))
182 .await
183 }
184
185 pub async fn incr(&self, key: &str, by: i64, ttl: Option<u64>) -> Result<i64, CacheError> {
191 let key = self.key(key)?;
192 let mut connection = self.connection.clone();
193 let value: i64 = self.run("incrby", connection.incr(&key, by)).await?;
194
195 let ttl = ttl.unwrap_or(self.default_ttl_secs);
198 if ttl > 0 && value == by {
199 self.run::<bool>("expire", connection.expire(&key, ttl as i64))
200 .await?;
201 }
202 Ok(value)
203 }
204
205 pub async fn ttl(&self, key: &str) -> Result<Option<i64>, CacheError> {
207 let key = self.key(key)?;
208 let seconds: i64 = self.run("ttl", self.connection.clone().ttl(&key)).await?;
209 Ok((seconds >= 0).then_some(seconds))
212 }
213
214 async fn run<T>(
220 &self,
221 command: &str,
222 future: impl std::future::Future<Output = redis::RedisResult<T>>,
223 ) -> Result<T, CacheError> {
224 match tokio::time::timeout(self.timeout, future).await {
225 Ok(Ok(value)) => Ok(value),
226 Ok(Err(e)) => Err(CacheError::Backend(format!("{command}: {e}"))),
227 Err(_) => Err(CacheError::Backend(format!(
228 "{command}: timed out after {:?}",
229 self.timeout
230 ))),
231 }
232 }
233}
234
235fn prefixed(prefix: &str, key: &str) -> Result<String, CacheError> {
241 let key = key.trim();
242 if key.is_empty() {
243 return Err(CacheError::Request("a cache key cannot be empty".into()));
244 }
245 Ok(format!("{prefix}{key}"))
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250#[serde(tag = "op", rename_all = "lowercase")]
251pub enum Op {
252 Get {
253 key: String,
254 },
255 Set {
256 key: String,
257 value: Value,
258 #[serde(default)]
259 ttl: Option<u64>,
260 },
261 #[serde(alias = "del")]
262 Delete {
263 key: String,
264 },
265 Exists {
266 key: String,
267 },
268 #[serde(alias = "increment")]
269 Incr {
270 key: String,
271 #[serde(default = "one")]
272 by: i64,
273 #[serde(default)]
274 ttl: Option<u64>,
275 },
276 Ttl {
277 key: String,
278 },
279}
280
281fn one() -> i64 {
282 1
283}
284
285impl Op {
286 pub fn grammar() -> &'static str {
288 r#"{"op":"get"|"set"|"delete"|"exists"|"incr"|"ttl","key":"…", …}"#
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn a_cache_is_only_connected_when_one_is_configured() {
298 assert!(!CacheConfig::default().is_active());
301 assert!(CacheConfig {
302 url: "redis://127.0.0.1:6379".into(),
303 ..CacheConfig::default()
304 }
305 .is_active());
306 }
307
308 #[test]
309 fn operations_parse_from_the_json_a_function_sends() {
310 let get: Op = serde_json::from_str(r#"{"op":"get","key":"k"}"#).unwrap();
311 assert!(matches!(get, Op::Get { key } if key == "k"));
312
313 let set: Op =
314 serde_json::from_str(r#"{"op":"set","key":"k","value":{"a":1},"ttl":60}"#).unwrap();
315 match set {
316 Op::Set { key, value, ttl } => {
317 assert_eq!(key, "k");
318 assert_eq!(value["a"], 1);
319 assert_eq!(ttl, Some(60));
320 }
321 other => panic!("expected a set, got {other:?}"),
322 }
323
324 let no_ttl: Op = serde_json::from_str(r#"{"op":"set","key":"k","value":null}"#).unwrap();
327 assert!(matches!(no_ttl, Op::Set { ttl: None, .. }));
328 let never: Op =
329 serde_json::from_str(r#"{"op":"set","key":"k","value":1,"ttl":0}"#).unwrap();
330 assert!(matches!(never, Op::Set { ttl: Some(0), .. }));
331
332 let incr: Op = serde_json::from_str(r#"{"op":"incr","key":"hits"}"#).unwrap();
334 assert!(matches!(incr, Op::Incr { by: 1, .. }));
335 assert!(matches!(
336 serde_json::from_str::<Op>(r#"{"op":"del","key":"k"}"#).unwrap(),
337 Op::Delete { .. }
338 ));
339 }
340
341 #[test]
342 fn an_unknown_operation_is_rejected() {
343 assert!(serde_json::from_str::<Op>(r#"{"op":"flushall"}"#).is_err());
344 assert!(serde_json::from_str::<Op>(r#"{"op":"get"}"#).is_err());
345 assert!(serde_json::from_str::<Op>("not json").is_err());
346 }
347
348 #[test]
349 fn keys_are_prefixed_and_never_empty() {
350 assert_eq!(prefixed("app:", "rates").unwrap(), "app:rates");
351 assert_eq!(prefixed("", " rates ").unwrap(), "rates");
352 assert!(prefixed("app:", "").is_err());
354 assert!(prefixed("app:", " ").is_err());
355 }
356}