use std::sync::Arc;
use std::time::Duration;
use chrono::Duration as ChronoDuration;
use rainier_cache::{Cache, CacheExt};
use rainier_support::{BoxFuture, Result};
use crate::session::SessionData;
use crate::store::SessionStore;
const PREFIX: &str = "session:";
pub struct CacheSessionStore {
cache: Arc<dyn Cache>,
lifetime: Duration,
}
impl CacheSessionStore {
pub fn new(cache: Arc<dyn Cache>) -> Self {
Self { cache, lifetime: Duration::from_secs(2 * 60 * 60) }
}
pub fn with_lifetime(mut self, lifetime: ChronoDuration) -> Self {
if let Ok(lifetime) = lifetime.to_std() {
self.lifetime = lifetime;
}
self
}
pub fn cache(&self) -> &Arc<dyn Cache> {
&self.cache
}
pub fn lifetime(&self) -> Duration {
self.lifetime
}
fn key(id: &str) -> String {
format!("{PREFIX}{id}")
}
}
impl SessionStore for CacheSessionStore {
fn name(&self) -> &str {
self.cache.name()
}
fn read<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<Option<SessionData>>> {
Box::pin(async move {
self.cache.get_json(&Self::key(id)).await
})
}
fn write<'a>(&'a self, id: &'a str, data: &'a SessionData) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.cache.put_json(&Self::key(id), data, Some(self.lifetime)).await
})
}
fn destroy<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.cache.forget(&Self::key(id)).await?;
Ok(())
})
}
fn gc(&self) -> BoxFuture<'_, Result<u64>> {
Box::pin(async { Ok(0) })
}
}
impl std::fmt::Debug for CacheSessionStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CacheSessionStore")
.field("cache", &self.cache.name())
.field("lifetime", &self.lifetime)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rainier_cache::MemoryCache;
fn store() -> (CacheSessionStore, Arc<MemoryCache>) {
let cache = Arc::new(MemoryCache::new());
(CacheSessionStore::new(Arc::clone(&cache) as Arc<dyn Cache>), cache)
}
fn data(user: u64) -> SessionData {
let mut values = serde_json::Map::new();
values.insert("user_id".into(), user.into());
SessionData { values, flash: Vec::new() }
}
#[tokio::test]
async fn a_written_session_reads_back() {
let (store, _) = store();
store.write("abc", &data(42)).await.unwrap();
assert_eq!(store.read("abc").await.unwrap().unwrap().values["user_id"], 42);
}
#[tokio::test]
async fn an_unknown_session_reads_as_none() {
let (store, _) = store();
assert!(store.read("nope").await.unwrap().is_none());
}
#[tokio::test]
async fn destroying_removes_it() {
let (store, _) = store();
store.write("abc", &data(42)).await.unwrap();
store.destroy("abc").await.unwrap();
assert!(store.read("abc").await.unwrap().is_none());
}
#[tokio::test]
async fn keys_are_prefixed_so_they_do_not_collide_with_cached_values() {
let (store, cache) = store();
store.write("abc", &data(42)).await.unwrap();
assert!(cache.has("session:abc").await.unwrap());
assert!(!cache.has("abc").await.unwrap(), "the bare key must be free for other uses");
}
#[tokio::test]
async fn a_ttl_is_set_on_every_write() {
let brief = CacheSessionStore::new(Arc::new(MemoryCache::new()))
.with_lifetime(ChronoDuration::nanoseconds(1));
brief.write("brief", &data(1)).await.unwrap();
assert!(brief.read("brief").await.unwrap().is_none());
}
#[tokio::test]
async fn an_unreadable_session_reads_as_a_fresh_one() {
let (store, cache) = store();
cache.put("session:abc", b"not json", None).await.unwrap();
assert!(store.read("abc").await.unwrap().is_none());
}
#[tokio::test]
async fn gc_does_not_flush_the_cache() {
let (store, cache) = store();
cache.put("something-else", b"v", None).await.unwrap();
assert_eq!(store.gc().await.unwrap(), 0);
assert!(cache.has("something-else").await.unwrap());
}
#[tokio::test]
async fn the_name_reports_the_driver_underneath() {
let (store, _) = store();
assert_eq!(store.name(), "memory");
}
#[test]
fn the_lifetime_is_configurable_and_ignores_a_negative_one() {
let (store, _) = store();
let store = store.with_lifetime(ChronoDuration::days(14));
assert_eq!(store.lifetime(), Duration::from_secs(14 * 24 * 60 * 60));
let store = store.with_lifetime(ChronoDuration::seconds(-1));
assert_eq!(store.lifetime(), Duration::from_secs(14 * 24 * 60 * 60));
}
#[tokio::test]
async fn it_is_not_client_side() {
let (store, _) = store();
assert!(!store.is_client_side());
assert!(store.decode("chosen-by-the-client").is_err());
}
}