use serde::Serialize;
use std::{borrow::Cow, fmt, future::Future};
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CacheKey {
name: Cow<'static, str>,
args: String,
}
impl CacheKey {
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self {
name: name.into(),
args: String::new(),
}
}
pub fn with(mut self, part: impl Serialize) -> Self {
let json = serde_json::to_string(&part)
.unwrap_or_else(|err| panic!("g3-kit: a key argument must serialize to JSON: {err}"));
if !self.args.is_empty() {
self.args.push(',');
}
self.args.push_str(&json);
self
}
pub fn of<F, Args>(server_fn: &F, args: &Args) -> Self
where
F: CacheableFn<Args>,
Args: Serialize,
{
let _ = server_fn;
let args = serde_json::to_string(args).unwrap_or_else(|err| {
panic!("g3-kit: server function arguments must serialize to JSON: {err}")
});
Self {
name: Cow::Borrowed(fn_name::<F>()),
args,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub(crate) fn storage_key(&self) -> String {
format!("{}\u{1f}{}", self.name, self.args)
}
}
impl fmt::Debug for CacheKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CacheKey({}({}))", self.name, self.args)
}
}
pub(crate) fn fn_name<F>() -> &'static str {
let name = std::any::type_name::<F>();
assert!(
!name.contains("{{closure}}"),
"g3-kit: `{name}` is a closure. Pass the server function itself, with its \
arguments as a tuple: `use_cached(get_media, (id,))`, not \
`use_cached(|| get_media(id), ())`. For a read that is not one \
server function call, use `use_cached_key` with a `CacheKey`."
);
name
}
pub trait CacheableFn<Args>: 'static {
type Output;
type Future: Future<Output = dioxus::Result<Self::Output>> + 'static;
fn call_with(&self, args: Args) -> Self::Future;
}
macro_rules! impl_server_fn {
($($arg:ident),*) => {
impl<F, Fut, T, $($arg),*> CacheableFn<($($arg,)*)> for F
where
F: Fn($($arg),*) -> Fut + 'static,
Fut: Future<Output = dioxus::Result<T>> + 'static,
{
type Output = T;
type Future = Fut;
#[allow(non_snake_case)]
fn call_with(&self, ($($arg,)*): ($($arg,)*)) -> Fut {
(self)($($arg),*)
}
}
};
}
impl_server_fn!();
impl_server_fn!(A);
impl_server_fn!(A, B);
impl_server_fn!(A, B, C);
impl_server_fn!(A, B, C, D);
impl_server_fn!(A, B, C, D, E);
impl_server_fn!(A, B, C, D, E, G);
impl_server_fn!(A, B, C, D, E, G, H);
impl_server_fn!(A, B, C, D, E, G, H, I);
#[cfg(test)]
mod tests {
use super::*;
async fn get_bookmarks() -> dioxus::Result<Vec<u8>> {
Ok(vec![])
}
async fn get_media(_id: String) -> dioxus::Result<u8> {
Ok(1)
}
async fn get_reviews(_id: String, _offset: u32, _limit: u32) -> dioxus::Result<u8> {
Ok(1)
}
#[test]
fn server_fns_name_their_own_keys() {
let key = CacheKey::of(&get_bookmarks, &());
assert!(key.name().ends_with("tests::get_bookmarks"));
let a = CacheKey::of(&get_media, &("tt1".to_string(),));
let b = CacheKey::of(&get_media, &("tt2".to_string(),));
assert_eq!(a.name(), b.name());
assert_ne!(a, b);
let reviews = CacheKey::of(&get_reviews, &("tt1".to_string(), 0, 21));
assert_eq!(reviews.args, r#"["tt1",0,21]"#);
}
#[test]
fn different_functions_never_share_a_key() {
let media = CacheKey::of(&get_media, &("tt1".to_string(),));
let reviews = CacheKey::of(&get_reviews, &("tt1".to_string(), 0, 21));
assert_ne!(media.name(), reviews.name());
}
#[test]
#[should_panic(expected = "is a closure")]
fn closures_are_refused() {
let id = "tt1".to_string();
let closure = move || get_media(id.clone());
let _ = CacheKey::of(&closure, &());
}
#[test]
fn hand_built_keys_add_arguments_in_order() {
let key = CacheKey::new("shelf").with("movie").with(Some(3));
assert_eq!(key.args, r#""movie",3"#);
assert_ne!(key, CacheKey::new("shelf").with(Some(3)).with("movie"));
}
}