use rustlavel_core::events::{self, Event};
use rustlavel_core::{Json, Result};
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait Cache: Send + Sync + 'static {
fn driver(&self) -> &'static str;
fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Json>>>;
fn put<'a>(&'a self, key: &'a str, value: Json, ttl: Duration) -> BoxFuture<'a, Result<()>>;
fn forever<'a>(&'a self, key: &'a str, value: Json) -> BoxFuture<'a, Result<()>>;
fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>>;
fn flush(&self) -> BoxFuture<'_, Result<()>>;
fn has<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
Box::pin(async move { Ok(self.get(key).await?.is_some()) })
}
fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>>;
fn decrement<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>> {
Box::pin(async move { self.increment(key, -by).await })
}
fn increment_within<'a>(
&'a self,
key: &'a str,
by: i64,
ttl: Duration,
) -> BoxFuture<'a, Result<i64>>;
fn ttl<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Duration>>>;
fn pull<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Json>>> {
Box::pin(async move {
let value = self.get(key).await?;
if value.is_some() {
self.forget(key).await?;
}
Ok(value)
})
}
}
pub trait CacheExt: Cache {
fn remember<'a, F, Fut>(
&'a self,
key: &'a str,
ttl: Duration,
compute: F,
) -> impl Future<Output = Result<Json>> + Send + 'a
where
F: FnOnce() -> Fut + Send + 'a,
Fut: Future<Output = Result<Json>> + Send + 'a,
{
async move {
if let Some(hit) = self.get(key).await? {
return Ok(hit);
}
let value = compute().await?;
self.put(key, value.clone(), ttl).await?;
Ok(value)
}
}
fn remember_forever<'a, F, Fut>(
&'a self,
key: &'a str,
compute: F,
) -> impl Future<Output = Result<Json>> + Send + 'a
where
F: FnOnce() -> Fut + Send + 'a,
Fut: Future<Output = Result<Json>> + Send + 'a,
{
async move {
if let Some(hit) = self.get(key).await? {
return Ok(hit);
}
let value = compute().await?;
self.forever(key, value.clone()).await?;
Ok(value)
}
}
}
impl<T: Cache + ?Sized> CacheExt for T {}
pub(crate) fn record(hit: bool, driver: &'static str, key: &str) {
if !events::has_subscribers() {
return;
}
let kind = if hit { "cache.hit" } else { "cache.miss" };
Event::new(kind).with("key", key).with("store", driver).dispatch();
}
pub(crate) fn decode(payload: &str) -> Option<Json> {
Json::parse(payload).ok()
}
pub(crate) fn counter_value(value: Option<&Json>) -> i64 {
match value {
Some(Json::Number(n)) => *n as i64,
Some(Json::String(s)) => s.trim().parse().unwrap_or(0),
_ => 0,
}
}
pub(crate) fn prefixed(prefix: &str, key: &str) -> String {
if prefix.is_empty() {
return key.to_string();
}
format!("{prefix}{key}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_counter_reads_numbers_strings_and_nothing_at_all() {
assert_eq!(counter_value(Some(&Json::from(7))), 7);
assert_eq!(counter_value(Some(&Json::from("12"))), 12);
assert_eq!(counter_value(Some(&Json::Null)), 0);
assert_eq!(counter_value(None), 0);
assert_eq!(counter_value(Some(&Json::from("not a number"))), 0);
}
#[test]
fn a_corrupt_payload_decodes_as_a_miss_rather_than_an_error() {
assert_eq!(decode("42"), Some(Json::from(42)));
assert_eq!(decode("{oops"), None);
}
#[test]
fn an_empty_prefix_leaves_the_key_untouched() {
assert_eq!(prefixed("", "users:1"), "users:1");
assert_eq!(prefixed("app:", "users:1"), "app:users:1");
}
}