use anyhow::{Context, Result};
use chrono::serde::ts_seconds;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use crate::guest::store;
use crate::guest::store::Bucket;
pub async fn open(bucket: &str) -> Result<Cache> {
let bucket = store::open(bucket.to_string()).await.context("opening bucket")?;
Ok(Cache { bucket })
}
#[derive(Debug)]
pub struct Cache {
bucket: Bucket,
}
impl Cache {
pub async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
let Some(entry) = self.bucket.get(key.to_string()).await.context("reading state")? else {
return Ok(None);
};
let Ok(ttl_val) = Cacheable::try_from(&entry) else {
tracing::debug!("Not serialized using Cacheable");
return Ok(Some(entry));
};
if ttl_val.is_expired() {
self.bucket.delete(key.to_string()).await.context("deleting expired state")?;
return Ok(None);
}
Ok(Some(ttl_val.value))
}
pub async fn set(
&self, key: &str, value: &[u8], ttl_secs: Option<u64>,
) -> Result<Option<Vec<u8>>> {
let value = if let Some(ttl) = ttl_secs.map(|secs| Duration::seconds(secs.cast_signed())) {
let envelope = Cacheable::new(value, ttl);
&<Cacheable as TryInto<Vec<u8>>>::try_into(envelope)?
} else {
value
};
let previous = self.get(key).await?;
self.bucket.set(key.to_string(), value.to_vec()).await.context("setting state with ttl")?;
Ok(previous)
}
pub async fn delete(&self, key: &str) -> Result<()> {
self.bucket.delete(key.to_string()).await.context("deleting entry")
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Cacheable {
pub value: Vec<u8>,
#[serde(with = "ts_seconds")]
pub expires_at: DateTime<Utc>,
}
impl Cacheable {
#[must_use]
pub fn new(value: &[u8], expires_in: Duration) -> Self {
Self {
value: value.to_vec(),
expires_at: Utc::now() + expires_in,
}
}
#[must_use]
pub(crate) fn is_expired(&self) -> bool {
Utc::now() >= self.expires_at
}
}
impl TryFrom<&Vec<u8>> for Cacheable {
type Error = anyhow::Error;
fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
serde_json::from_slice(value).context("issue deserializing Cacheable")
}
}
impl TryFrom<Cacheable> for Vec<u8> {
type Error = anyhow::Error;
fn try_from(value: Cacheable) -> Result<Self, Self::Error> {
serde_json::to_vec(&value).context("issue serializing Cacheable")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid() {
let value = vec![1, 2, 3, 4];
let expires_at = Utc::now() + Duration::seconds(60);
let cacheable = Cacheable {
value: value.clone(),
expires_at,
};
let bytes = serde_json::to_vec(&cacheable).unwrap();
let parsed = Cacheable::try_from(&bytes).unwrap();
assert_eq!(parsed.value, value);
assert_eq!(parsed.expires_at.timestamp(), expires_at.timestamp());
}
#[test]
fn invalid_json() {
let invalid = b"not a json".to_vec();
let result = Cacheable::try_from(&invalid);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("issue deserializing Cacheable"));
}
#[test]
fn wrong_type() {
let bytes = serde_json::to_vec(&"just a string").unwrap();
let result = Cacheable::try_from(&bytes);
result.unwrap_err();
}
}