use std::sync::Arc;
use moka::future::Cache;
use crate::flat::webtemplate::model::WebTemplate;
#[derive(Debug, Clone)]
pub struct WebTemplateCache {
inner: Cache<String, Arc<WebTemplate>>,
}
impl WebTemplateCache {
#[must_use]
pub fn new(capacity: u64) -> Self {
Self {
inner: Cache::builder().max_capacity(capacity).build(),
}
}
pub async fn get_or_build<F>(
&self,
template_id: &str,
build: F,
) -> Result<Arc<WebTemplate>, Arc<crate::flat::error::FlatError>>
where
F: FnOnce() -> Result<WebTemplate, crate::flat::error::FlatError>,
{
self.inner
.try_get_with_by_ref(template_id, async { build().map(Arc::new) })
.await
}
#[must_use]
pub async fn get(&self, template_id: &str) -> Option<Arc<WebTemplate>> {
self.inner.get(template_id).await
}
pub async fn invalidate(&self, template_id: &str) {
self.inner.invalidate(template_id).await;
}
}
impl Default for WebTemplateCache {
fn default() -> Self {
Self::new(256)
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::flat::webtemplate::model::WebTemplate;
fn stub_template(id: &str) -> WebTemplate {
let mut tree = crate::flat::webtemplate::model::WebTemplateNode::new(
"COMPOSITION".to_owned(),
String::new(),
);
tree.id = "root".to_owned();
tree.min = Some(1);
tree.max = 1;
WebTemplate {
template_id: id.to_owned(),
sem_ver: None,
version: "2.3".to_owned(),
default_language: "en".to_owned(),
languages: vec!["en".to_owned()],
tree,
other_details: indexmap::IndexMap::new(),
archetype_refs: None,
}
}
#[tokio::test]
async fn builds_once_then_serves_from_cache() {
let cache = super::WebTemplateCache::default();
let builds = AtomicUsize::new(0);
let build = || {
builds.fetch_add(1, Ordering::SeqCst);
Ok(stub_template("t1"))
};
let first = cache.get_or_build("t1", build).await.expect("first build");
assert_eq!(first.template_id, "t1");
let second = cache
.get_or_build("t1", || {
builds.fetch_add(1, Ordering::SeqCst);
Ok(stub_template("t1"))
})
.await
.expect("cache hit");
assert_eq!(second.template_id, "t1");
assert_eq!(builds.load(Ordering::SeqCst), 1, "build ran more than once");
cache.invalidate("t1").await;
let _third = cache.get_or_build("t1", build).await.expect("rebuild");
assert_eq!(
builds.load(Ordering::SeqCst),
2,
"invalidation should rebuild"
);
}
#[tokio::test]
async fn get_is_a_nonbuilding_peek() {
let cache = super::WebTemplateCache::default();
assert!(cache.get("t1").await.is_none(), "cold `get` is a miss");
let _built = cache
.get_or_build("t1", || Ok(stub_template("t1")))
.await
.expect("build");
assert!(cache.get("t1").await.is_some(), "warm `get` is a hit");
cache.invalidate("t1").await;
assert!(
cache.get("t1").await.is_none(),
"`get` misses after invalidation"
);
}
}