#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![warn(clippy::dbg_macro, clippy::use_debug)]
#![warn(missing_docs, missing_debug_implementations)]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
use std::{convert::identity, future::Future, time::Duration};
use backend::AsyncBackend;
use format::Formatter;
pub use postcard;
pub use serde;
use serde::{de::DeserializeOwned, Serialize};
#[cfg(feature = "serde_json")]
pub use serde_json;
use sha2::{Digest, Sha512};
pub mod backend;
pub mod format;
#[derive(Debug, Clone)]
pub struct AsyncCache<B, S> {
backend: B,
formatter: S,
default_ttl: Duration,
}
impl<B, S> AsyncCache<B, S>
where
B: AsyncBackend<S::Serialized>,
S: Formatter,
{
pub fn new(backend: B, formatter: S, default_ttl: Duration) -> Self {
Self {
backend,
formatter,
default_ttl,
}
}
pub fn with_formatter<T: Formatter>(&self, formatter: T) -> AsyncCache<B, T>
where
B: Clone,
{
AsyncCache {
backend: self.backend.clone(),
formatter,
default_ttl: self.default_ttl,
}
}
pub async fn cached<T, K, F>(
&self,
key: K,
tags: &[&str],
ttl: Option<Duration>,
func: impl FnOnce() -> F,
) -> Result<T, Error<B, S>>
where
F: Future<Output = T>,
T: Serialize + DeserializeOwned,
K: Serialize,
{
self.cached_filter_map(key, tags, ttl, func, |x| Some(x), identity)
.await
}
pub async fn cached_option<T, K, F>(
&self,
key: K,
tags: &[&str],
ttl: Option<Duration>,
func: impl FnOnce() -> F,
) -> Result<Option<T>, Error<B, S>>
where
F: Future<Output = Option<T>>,
T: Serialize + DeserializeOwned,
K: Serialize,
{
self.cached_filter_map(key, tags, ttl, func, Option::as_ref, Some)
.await
}
pub async fn cached_result<T, E, K, F>(
&self,
key: K,
tags: &[&str],
ttl: Option<Duration>,
func: impl FnOnce() -> F,
) -> Result<Result<T, E>, Error<B, S>>
where
F: Future<Output = Result<T, E>>,
T: Serialize + DeserializeOwned,
K: Serialize,
{
self.cached_filter_map(key, tags, ttl, func, |x| x.as_ref().ok(), Ok)
.await
}
pub async fn cached_filter_map<T, R, K, F>(
&self,
key: K,
tags: &[&str],
ttl: Option<Duration>,
func: impl FnOnce() -> F,
to_cache: impl FnOnce(&T) -> Option<&R>,
from_cache: impl FnOnce(R) -> T,
) -> Result<T, Error<B, S>>
where
F: Future<Output = T>,
R: Serialize + DeserializeOwned,
K: Serialize,
{
if let Some(value) = self.get(&key).await?.map(from_cache) {
return Ok(value);
}
let value = func().await;
if let Some(cache_value) = to_cache(&value) {
self.put(&key, cache_value, tags, ttl).await?;
}
Ok(value)
}
pub async fn get<T, K>(&self, key: K) -> Result<Option<T>, Error<B, S>>
where
T: Serialize + DeserializeOwned,
K: Serialize,
{
self.backend
.get(&make_key::<S>(key)?)
.await
.map_err(Error::BackendError)?
.map(|x| self.formatter.deserialize(&x))
.transpose()
.map_err(Error::FormatterError)
}
pub async fn put<T, K>(
&self,
key: K,
value: T,
tags: &[&str],
ttl: Option<Duration>,
) -> Result<(), Error<B, S>>
where
T: Serialize,
K: Serialize,
{
let serialized = self
.formatter
.serialize(&value)
.map_err(Error::FormatterError)?;
self.backend
.put(
&make_key::<S>(key)?,
&serialized,
tags,
ttl.unwrap_or(self.default_ttl),
)
.await
.map_err(Error::BackendError)
}
pub async fn pop_key<K>(&self, key: K) -> Result<(), Error<B, S>>
where
K: Serialize,
{
self.backend
.pop_key(&make_key::<S>(key)?)
.await
.map_err(Error::BackendError)
}
pub async fn pop_tag(&self, tag: &str) -> Result<(), Error<B, S>> {
self.backend.pop_tag(tag).await.map_err(Error::BackendError)
}
pub async fn pop_tags(&self, tags: &[&str]) -> Result<(), Error<B, S>> {
self.backend
.pop_tags(tags)
.await
.map_err(Error::BackendError)
}
}
#[allow(missing_docs)]
#[derive(Debug, thiserror::Error)]
pub enum Error<B, S>
where
B: AsyncBackend<S::Serialized>,
S: Formatter,
{
#[error("postcard error: {0}")]
PostcardError(#[from] postcard::Error),
#[error("backend error: {0}")]
BackendError(B::Error),
#[error("formatter error: {0}")]
FormatterError(S::Error),
}
fn make_key<F: Formatter>(key: impl Serialize) -> Result<String, postcard::Error> {
Ok(format!(
"{:x}:{}",
Sha512::new()
.chain_update(postcard::to_stdvec(&key)?)
.finalize(),
F::ID
))
}
#[macro_export]
macro_rules! key {
($($x:expr),*$(,)?) => {
(
::std::env!("CARGO_PKG_NAME"),
::std::env!("CARGO_PKG_VERSION"),
::std::module_path!(),
::std::file!(),
::std::line!(),
::std::column!(),
$($x),*
)
};
}
#[macro_export]
macro_rules! keyfn {
($vis:vis $name:ident($($arg:ident: $ty:ty),*$(,)?)) => {
$vis fn $name($($arg: $ty),*) -> (
&'static str, &'static str, &'static str, &'static str, u32, u32, $($ty),*
) {
$crate::key!($($arg),*)
}
};
}