oxcache 0.4.3

A high-performance multi-level cache library for Rust with L1 (memory) and L2 (Redis) caching.
Documentation
// Copyright (c) 2025-2026 Kirky.X
// SPDX-License-Identifier: MIT
//! `CacheBackendDecorator` — trait-kit decorator integration for cache backends.
//!
//! Provides a type alias and helper function for registering backend decorators
//! via `AsyncKit::decorate::<OxcacheModule>()`. Decorators transform the
//! `Arc<dyn CacheBackend + Send + Sync>` capability after module build,
//! enabling cross-cutting concerns (metrics, logging, access counting).

use std::sync::Arc;

use trait_kit::prelude::*;

use crate::backend::CacheBackend;
use crate::integrations::kit::OxcacheModule;

/// Type alias for a cache backend decorator function.
///
/// Matches the signature required by
/// `AsyncKit::decorate::<OxcacheModule>(decorator)`.
pub type CacheBackendDecorator =
    Arc<dyn Fn(Arc<dyn CacheBackend + Send + Sync>) -> Arc<dyn CacheBackend + Send + Sync> + Send + Sync>;

/// Register a decorator for the `OxcacheModule` capability.
///
/// The decorator transforms `Arc<dyn CacheBackend + Send + Sync>` after
/// the module builds, enabling cross-cutting concerns without modifying
/// backend implementations.
///
/// # Errors
///
/// This function does not fail at the call site but the decorator is
/// applied lazily during `kit.build()`. A type mismatch will panic
/// inside trait-kit (should never happen with `OxcacheModule`).
pub fn register_cache_decorator(
    kit: &AsyncKit,
    decorator: impl Fn(Arc<dyn CacheBackend + Send + Sync>) -> Arc<dyn CacheBackend + Send + Sync> + Send + Sync + 'static,
) {
    kit.decorate::<OxcacheModule>(decorator);
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;
    use std::sync::atomic::{AtomicUsize, Ordering};

    use crate::backend::{BackendKind, CacheConnector, CacheReader, CacheWriter};

    /// Mock backend that returns a fixed value for `get`.
    struct DecoratorTestBackend;

    #[async_trait::async_trait]
    impl CacheReader for DecoratorTestBackend {
        async fn get(&self, _key: &str) -> crate::error::OxCacheResult<Option<Vec<u8>>> {
            Ok(Some(b"original".to_vec()))
        }
        async fn exists(&self, _key: &str) -> crate::error::OxCacheResult<bool> {
            Ok(true)
        }
        async fn ttl(&self, _key: &str) -> crate::error::OxCacheResult<Option<std::time::Duration>> {
            Ok(None)
        }
        async fn len(&self) -> crate::error::OxCacheResult<u64> {
            Ok(0)
        }
        async fn capacity(&self) -> crate::error::OxCacheResult<u64> {
            Ok(0)
        }
        async fn stats(&self) -> crate::error::OxCacheResult<HashMap<String, String>> {
            Ok(HashMap::new())
        }
    }

    #[async_trait::async_trait]
    impl CacheWriter for DecoratorTestBackend {
        async fn set(
            &self,
            _key: Arc<str>,
            _value: Arc<Vec<u8>>,
            _ttl: Option<std::time::Duration>,
        ) -> crate::error::OxCacheResult<()> {
            Ok(())
        }
        async fn delete(&self, _key: &str) -> crate::error::OxCacheResult<()> {
            Ok(())
        }
        async fn clear(&self) -> crate::error::OxCacheResult<()> {
            Ok(())
        }
        async fn expire(&self, _key: &str, _ttl: std::time::Duration) -> crate::error::OxCacheResult<bool> {
            Ok(false)
        }
    }

    #[async_trait::async_trait]
    impl CacheConnector for DecoratorTestBackend {
        async fn health_check(&self) -> crate::error::OxCacheResult<()> {
            Ok(())
        }
        async fn shutdown(&self) {}
        fn backend_kind(&self) -> BackendKind {
            BackendKind::Mock
        }
    }

    /// Counting decorator backend that wraps another backend.
    struct CountingDecorator {
        inner: Arc<dyn CacheBackend + Send + Sync>,
        count: Arc<AtomicUsize>,
    }

    #[async_trait::async_trait]
    impl CacheReader for CountingDecorator {
        async fn get(&self, key: &str) -> crate::error::OxCacheResult<Option<Vec<u8>>> {
            self.count.fetch_add(1, Ordering::SeqCst);
            self.inner.get(key).await
        }
        async fn exists(&self, key: &str) -> crate::error::OxCacheResult<bool> {
            self.count.fetch_add(1, Ordering::SeqCst);
            self.inner.exists(key).await
        }
        async fn ttl(&self, key: &str) -> crate::error::OxCacheResult<Option<std::time::Duration>> {
            self.inner.ttl(key).await
        }
        async fn len(&self) -> crate::error::OxCacheResult<u64> {
            self.inner.len().await
        }
        async fn capacity(&self) -> crate::error::OxCacheResult<u64> {
            self.inner.capacity().await
        }
        async fn stats(&self) -> crate::error::OxCacheResult<HashMap<String, String>> {
            self.inner.stats().await
        }
    }

    #[async_trait::async_trait]
    impl CacheWriter for CountingDecorator {
        async fn set(
            &self,
            key: Arc<str>,
            value: Arc<Vec<u8>>,
            ttl: Option<std::time::Duration>,
        ) -> crate::error::OxCacheResult<()> {
            self.count.fetch_add(1, Ordering::SeqCst);
            self.inner.set(key, value, ttl).await
        }
        async fn delete(&self, key: &str) -> crate::error::OxCacheResult<()> {
            self.count.fetch_add(1, Ordering::SeqCst);
            self.inner.delete(key).await
        }
        async fn clear(&self) -> crate::error::OxCacheResult<()> {
            self.count.fetch_add(1, Ordering::SeqCst);
            self.inner.clear().await
        }
        async fn expire(&self, key: &str, ttl: std::time::Duration) -> crate::error::OxCacheResult<bool> {
            self.inner.expire(key, ttl).await
        }
    }

    #[async_trait::async_trait]
    impl CacheConnector for CountingDecorator {
        async fn health_check(&self) -> crate::error::OxCacheResult<()> {
            self.inner.health_check().await
        }
        async fn shutdown(&self) {
            self.inner.shutdown().await;
        }
        fn backend_kind(&self) -> BackendKind {
            self.inner.backend_kind()
        }
    }

    /// Decorator is applied during `kit.build()`; the counting wrapper
    /// increments its counter on each `get` call.
    #[tokio::test]
    async fn decorator_is_applied_on_build() {
        // Override OxcacheModule to produce DecoratorTestBackend.
        // We cannot easily override the module builder in a unit test,
        // so instead we test the decorator type and function directly.
        let count = Arc::new(AtomicUsize::new(0));
        let count_clone = Arc::clone(&count);

        let decorator = move |backend: Arc<dyn CacheBackend + Send + Sync>| -> Arc<dyn CacheBackend + Send + Sync> {
            Arc::new(CountingDecorator {
                inner: backend,
                count: Arc::clone(&count_clone),
            })
        };

        // Build a backend and apply decorator manually.
        let original: Arc<dyn CacheBackend + Send + Sync> = Arc::new(DecoratorTestBackend);
        let decorated = decorator(original);

        // Counter starts at 0.
        assert_eq!(count.load(Ordering::SeqCst), 0);

        // After get, counter increments.
        let _ = decorated.get("key").await;
        assert_eq!(count.load(Ordering::SeqCst), 1);

        let _ = decorated.get("key").await;
        assert_eq!(count.load(Ordering::SeqCst), 2);
    }

    /// `register_cache_decorator` can be called on an `AsyncKit` without panic,
    /// and the built module's capability is usable.
    #[tokio::test]
    async fn register_cache_decorator_does_not_panic() {
        let mut kit = AsyncKit::new();
        kit.set_config(crate::integrations::kit::OxcacheConfig::default());
        kit.register::<OxcacheModule>().expect("register OxcacheModule");

        let count = Arc::new(AtomicUsize::new(0));
        let count_clone = Arc::clone(&count);

        register_cache_decorator(&kit, move |backend| {
            Arc::new(CountingDecorator {
                inner: backend,
                count: Arc::clone(&count_clone),
            })
        });

        // build + require should succeed without panic.
        let built = kit.build().await.expect("kit build");
        let backend = built.require::<OxcacheModule>().expect("require module");

        // The built backend is functional (Moka backend from OxcacheModule).
        // Decorator is applied internally by trait-kit during require;
        // we verify the backend is usable.
        let _ = backend.get("test").await;
    }

    /// Decorator is applied during `kit.build()` — the capability returned
    /// by `require` reflects the decorator transformation.
    #[tokio::test]
    async fn decorator_is_applied_through_kit_build() {
        // Minimal reproduction mirroring trait-kit's async_decorator_tests.
        use std::any::TypeId;
        use std::future::Future;
        use std::pin::Pin;

        #[derive(Debug, Clone)]
        struct TestCap {
            val: String,
        }

        struct TestModule;
        impl trait_kit::core::ModuleMeta for TestModule {
            const NAME: &'static str = "test-dec";
            fn dependencies() -> &'static [(&'static str, TypeId)] {
                &[]
            }
        }
        impl AsyncAutoBuilder for TestModule {
            type Capability = Arc<TestCap>;
            type Error = crate::error::OxCacheError;
            fn build<'a>(
                _kit: &'a AsyncKit,
            ) -> Pin<Box<dyn Future<Output = Result<Arc<TestCap>, crate::error::OxCacheError>> + Send + 'a>>
            {
                Box::pin(async { Ok(Arc::new(TestCap { val: "base".into() })) })
            }
        }

        let mut kit = AsyncKit::new();
        kit.register::<TestModule>().expect("register TestModule");
        kit.decorate::<TestModule>(|cap: Arc<TestCap>| {
            Arc::new(TestCap {
                val: format!("{}+wrapped", cap.val),
            })
        });

        let built = kit.build().await.expect("kit build");
        let cap = built.require::<TestModule>().expect("require TestModule");
        assert_eq!(cap.val, "base+wrapped", "decorator should modify the capability");
    }
}