use std::error::Error;
use std::future::Future;
use std::marker::PhantomData;
use std::time::Duration;
use hydracache::{CacheKeyBuilder, CacheOptions, HydraCache, PostcardCodec, TagSet};
use hydracache_core::CacheCodec;
use serde::{de::DeserializeOwned, Serialize};
use crate::{DbCacheError, Result};
#[derive(Debug, Clone)]
pub struct DbCache<C = PostcardCodec>
where
C: CacheCodec,
{
cache: HydraCache<C>,
namespace: String,
}
impl<C> DbCache<C>
where
C: CacheCodec,
{
pub fn new(cache: HydraCache<C>, namespace: impl Into<String>) -> Self {
Self {
cache,
namespace: namespace.into(),
}
}
pub fn namespace(&self) -> &str {
&self.namespace
}
pub fn cache(&self) -> &HydraCache<C> {
&self.cache
}
pub fn cached<T>(&self) -> DbQuery<T, C> {
self.named("unnamed")
}
pub fn named<T>(&self, name: impl Into<String>) -> DbQuery<T, C> {
DbQuery {
cache: self.cache.clone(),
namespace: self.namespace.clone(),
name: Some(name.into()),
key: None,
tags: TagSet::new(),
ttl: None,
value: PhantomData,
}
}
pub fn query_as<T>(&self, sql: impl Into<String>) -> DbQuery<T, C> {
self.named(sql)
}
}
#[derive(Debug, Clone)]
pub struct DbQuery<T, C = PostcardCodec>
where
C: CacheCodec,
{
cache: HydraCache<C>,
namespace: String,
name: Option<String>,
key: Option<String>,
tags: TagSet,
ttl: Option<Duration>,
value: PhantomData<fn() -> T>,
}
impl<T, C> DbQuery<T, C>
where
C: CacheCodec,
{
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn namespace(&self) -> &str {
&self.namespace
}
pub fn key_value(&self) -> Option<&str> {
self.key.as_deref()
}
pub fn physical_key(&self) -> Option<String> {
self.key
.as_deref()
.map(|key| physical_key(&self.namespace, key))
}
pub fn tags_value(&self) -> &[String] {
self.tags.as_slice()
}
pub fn ttl_value(&self) -> Option<Duration> {
self.ttl
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn key_builder(self, key: CacheKeyBuilder) -> Self {
self.key(key.build_string())
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags = self.tags.tag(tag);
self
}
pub fn tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.tags = self.tags.tags(tags);
self
}
pub fn tag_set(mut self, tags: TagSet) -> Self {
self.tags = tags;
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub async fn fetch_with<E, F, Fut>(self, loader: F) -> Result<T>
where
T: Serialize + DeserializeOwned + Send + 'static,
E: Error + Send + Sync + 'static,
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = std::result::Result<T, E>> + Send + 'static,
{
let Some(key) = self.physical_key() else {
return Err(DbCacheError::MissingKey {
operation: self.operation_label(),
});
};
self.cache
.get_or_load(&key, self.options(), loader)
.await
.map_err(DbCacheError::from)
}
fn options(&self) -> CacheOptions {
let mut options = CacheOptions::new().tag_set(self.tags.clone());
if let Some(ttl) = self.ttl {
options = options.ttl(ttl);
}
options
}
fn operation_label(&self) -> String {
match (&self.name, &self.key) {
(Some(name), _) => name.clone(),
(None, Some(key)) if self.namespace.is_empty() => key.clone(),
(None, Some(key)) => physical_key(&self.namespace, key),
(None, None) if self.namespace.is_empty() => "unnamed".to_owned(),
(None, None) => format!("{}:unnamed", self.namespace),
}
}
}
fn physical_key(namespace: &str, key: &str) -> String {
if namespace.is_empty() {
key.to_owned()
} else {
format!("{namespace}:{key}")
}
}