rok-cache 0.1.0

Cache façade for the rok ecosystem — Memory/Redis drivers, Cache::get/set/remember
Documentation
pub mod driver;
pub mod drivers;

mod cache;
mod error;

#[cfg(feature = "axum")]
mod cache_layer;

pub use cache::{scope_cache, Cache};
pub use driver::{build_driver, CacheHandle, Driver};
pub use error::CacheError;

#[cfg(feature = "axum")]
pub use cache_layer::CacheLayer;

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use std::sync::Arc;
    use std::time::Duration;

    use crate::driver::{CacheHandle, Driver};
    use crate::drivers::MemoryDriver;
    use crate::CacheError;
    use crate::{scope_cache, Cache};

    fn handle(prefix: &str) -> Arc<CacheHandle> {
        Arc::new(CacheHandle::new(
            Driver::Memory(MemoryDriver::new()),
            prefix,
        ))
    }

    #[tokio::test]
    async fn set_and_get_string() {
        scope_cache(handle(""), async {
            Cache::set("greeting", &"hello", None).await.unwrap();
            let v: Option<String> = Cache::get("greeting").await.unwrap();
            assert_eq!(v.as_deref(), Some("hello"));
        })
        .await;
    }

    #[tokio::test]
    async fn get_missing_returns_none() {
        scope_cache(handle(""), async {
            let v: Option<i32> = Cache::get("nothing").await.unwrap();
            assert_eq!(v, None);
        })
        .await;
    }

    #[tokio::test]
    async fn set_and_get_struct() {
        use serde::{Deserialize, Serialize};
        #[derive(Serialize, Deserialize, PartialEq, Debug)]
        struct User {
            name: String,
            age: u32,
        }

        scope_cache(handle(""), async {
            let u = User {
                name: "Alice".into(),
                age: 30,
            };
            Cache::set("user", &u, None).await.unwrap();
            let got: Option<User> = Cache::get("user").await.unwrap();
            assert_eq!(
                got,
                Some(User {
                    name: "Alice".into(),
                    age: 30
                })
            );
        })
        .await;
    }

    #[tokio::test]
    async fn forget_removes_key() {
        scope_cache(handle(""), async {
            Cache::set("tmp", &99i32, None).await.unwrap();
            Cache::forget("tmp").await.unwrap();
            let v: Option<i32> = Cache::get("tmp").await.unwrap();
            assert_eq!(v, None);
        })
        .await;
    }

    #[tokio::test]
    async fn flush_clears_everything() {
        scope_cache(handle(""), async {
            Cache::set("a", &1i32, None).await.unwrap();
            Cache::set("b", &2i32, None).await.unwrap();
            Cache::flush().await.unwrap();
            let a: Option<i32> = Cache::get("a").await.unwrap();
            let b: Option<i32> = Cache::get("b").await.unwrap();
            assert!(a.is_none() && b.is_none());
        })
        .await;
    }

    #[tokio::test]
    async fn ttl_expiry_returns_none_after_expiry() {
        scope_cache(handle(""), async {
            Cache::set("exp", &"value", Some(Duration::from_millis(50)))
                .await
                .unwrap();
            tokio::time::sleep(Duration::from_millis(100)).await;
            let v: Option<String> = Cache::get("exp").await.unwrap();
            assert_eq!(v, None, "expired key should return None");
        })
        .await;
    }

    #[tokio::test]
    async fn no_expiry_persists() {
        scope_cache(handle(""), async {
            Cache::set("perm", &"stay", None).await.unwrap();
            tokio::time::sleep(Duration::from_millis(20)).await;
            let v: Option<String> = Cache::get("perm").await.unwrap();
            assert_eq!(v.as_deref(), Some("stay"));
        })
        .await;
    }

    #[tokio::test]
    async fn namespace_isolation() {
        let h_a = handle("ns_a:");
        let h_b = handle("ns_b:");

        scope_cache(Arc::clone(&h_a), async {
            Cache::set("key", &"from_a", None).await.unwrap();
        })
        .await;

        scope_cache(Arc::clone(&h_b), async {
            let v: Option<String> = Cache::get("key").await.unwrap();
            assert_eq!(v, None, "namespace B should not see A's keys");
        })
        .await;
    }

    #[tokio::test]
    async fn remember_calls_fetcher_once() {
        use std::sync::atomic::{AtomicU32, Ordering};
        let calls = Arc::new(AtomicU32::new(0));
        let c = Arc::clone(&calls);

        scope_cache(handle(""), async move {
            for _ in 0..4 {
                let _: String = Cache::remember("expensive", Duration::from_secs(60), || {
                    let c = Arc::clone(&c);
                    async move {
                        c.fetch_add(1, Ordering::SeqCst);
                        Ok::<_, String>("computed".to_string())
                    }
                })
                .await
                .unwrap();
            }
        })
        .await;

        assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
    }

    #[tokio::test]
    async fn get_without_layer_returns_not_configured() {
        let res: Result<Option<String>, CacheError> = Cache::get("k").await;
        assert!(matches!(res, Err(CacheError::NotConfigured)));
    }
}