cachet_service 0.2.5

Layered service integration for the cachet caching library.
Documentation
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Adapter to use Service implementations as `CacheTier` storage backends.
//!
//! This module provides bidirectional adaptation between `Service<CacheOperation>`
//! and `CacheTier`, enabling remote cache services (Redis, Memcached) to be used
//! as cache storage backends.

use std::hash::Hash;
use std::marker::PhantomData;

use cachet_tier::{CacheEntry, CacheTier, Error, SizeError};
use layered::Service;

use crate::{CacheOperation, CacheResponse, GetRequest, InsertRequest, InvalidateRequest};

/// Adapter that converts a `Service<CacheOperation>` into a `CacheTier`.
///
/// This enables using service-based cache implementations (like Redis or Memcached)
/// as storage backends for `Cache`. The service can be composed with middleware
/// (retry, timeout, circuit breakers) before being wrapped by this adapter.
///
/// # Examples
///
/// ```ignore
/// // Convert any Service<CacheOperation> to a CacheTier
/// let adapter = ServiceAdapter::new(redis_service);
///
/// // Use as cache storage
/// let cache = Cache::builder(clock)
///     .storage(adapter)
///     .build();
/// ```
#[derive(Debug, Clone)]
pub struct ServiceAdapter<K, V, S> {
    service: S,
    _phantom: PhantomData<(K, V)>,
}

impl<K, V, S> ServiceAdapter<K, V, S> {
    /// Creates a new `ServiceAdapter` wrapping the given service.
    ///
    /// The service must implement `Service<CacheOperation<K, V>>` with
    /// output type `Result<CacheResponse<V>, Error>`.
    #[must_use]
    pub fn new(service: S) -> Self {
        Self {
            service,
            _phantom: PhantomData,
        }
    }

    /// Returns a reference to the inner service.
    #[must_use]
    pub fn inner(&self) -> &S {
        &self.service
    }
}

impl<K, V, S> CacheTier<K, V> for ServiceAdapter<K, V, S>
where
    K: Clone + Eq + Hash + Send + Sync + 'static,
    V: Clone + Send + Sync + 'static,
    S: Service<CacheOperation<K, V>, Out = Result<CacheResponse<V>, Error>> + Send + Sync,
{
    async fn get(&self, key: &K) -> Result<Option<CacheEntry<V>>, Error> {
        let request = CacheOperation::Get(GetRequest::new(key.clone()));
        match self.service.execute(request).await? {
            CacheResponse::Get(entry) => Ok(entry),
            _ => Err(Error::from_message("unexpected response type for get")),
        }
    }

    async fn insert(&self, key: K, entry: CacheEntry<V>) -> Result<(), Error> {
        let request = CacheOperation::Insert(InsertRequest::new(key.clone(), entry));
        match self.service.execute(request).await? {
            CacheResponse::Insert => Ok(()),
            _ => Err(Error::from_message("unexpected response type for insert")),
        }
    }

    async fn invalidate(&self, key: &K) -> Result<(), Error> {
        let request = CacheOperation::Invalidate(InvalidateRequest::new(key.clone()));
        match self.service.execute(request).await? {
            CacheResponse::Invalidate => Ok(()),
            _ => Err(Error::from_message("unexpected response type for invalidate")),
        }
    }

    async fn clear(&self) -> Result<(), Error> {
        match self.service.execute(CacheOperation::Clear).await? {
            CacheResponse::Clear => Ok(()),
            _ => Err(Error::from_message("unexpected response type for clear")),
        }
    }

    async fn len(&self) -> Result<u64, SizeError> {
        // Service-based tiers typically don't expose length information
        Err(SizeError::unsupported())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Mock service for testing
    #[derive(Debug, Clone)]
    struct MockService;

    impl Service<CacheOperation<String, i32>> for MockService {
        type Out = Result<CacheResponse<i32>, Error>;

        async fn execute(&self, input: CacheOperation<String, i32>) -> Self::Out {
            match input {
                CacheOperation::Get(req) => {
                    if req.key == "existing" {
                        Ok(CacheResponse::Get(Some(CacheEntry::new(42))))
                    } else {
                        Ok(CacheResponse::Get(None))
                    }
                }
                CacheOperation::Insert(_) => Ok(CacheResponse::Insert),
                CacheOperation::Invalidate(_) => Ok(CacheResponse::Invalidate),
                CacheOperation::Clear => Ok(CacheResponse::Clear),
            }
        }
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn adapter_get_existing() {
        let adapter = ServiceAdapter::new(MockService);
        let result = adapter.get(&"existing".to_string()).await;
        assert!(result.is_ok());
        assert_eq!(*result.unwrap().unwrap().value(), 42);
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn adapter_get_missing() {
        let adapter = ServiceAdapter::new(MockService);
        let result = adapter.get(&"missing".to_string()).await;
        assert!(result.is_ok());
        assert!(result.unwrap().is_none());
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn adapter_insert_returns_ok() {
        let adapter = ServiceAdapter::new(MockService);
        let result = adapter.insert("key".to_string(), CacheEntry::new(100)).await;
        assert!(result.is_ok(), "insert should succeed");
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn adapter_invalidate_returns_ok() {
        let adapter = ServiceAdapter::new(MockService);
        let result = adapter.invalidate(&"key".to_string()).await;
        assert!(result.is_ok(), "invalidate should succeed");
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn adapter_clear_returns_ok() {
        let adapter = ServiceAdapter::new(MockService);
        let result = adapter.clear().await;
        assert!(result.is_ok(), "clear should succeed");
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn adapter_len() {
        use cachet_tier::SizeErrorKind;

        let adapter = ServiceAdapter::<String, i32, _>::new(MockService);
        let err = adapter.len().await.unwrap_err();
        assert_eq!(err.kind, SizeErrorKind::Unsupported);
    }

    #[test]
    fn adapter_inner_returns_reference() {
        let adapter = ServiceAdapter::<String, i32, _>::new(MockService);
        let _inner: &MockService = adapter.inner();
    }

    // Service that returns wrong response types to exercise the defensive error branches.
    // The type system cannot prevent a Service implementation from returning the wrong
    // CacheResponse variant for a given CacheOperation, so we guard against it at runtime.
    #[derive(Debug, Clone)]
    struct WrongResponseService;

    impl Service<CacheOperation<String, i32>> for WrongResponseService {
        type Out = Result<CacheResponse<i32>, Error>;

        async fn execute(&self, input: CacheOperation<String, i32>) -> Self::Out {
            match input {
                // Returns Clear response for Insert requests (wrong type)
                CacheOperation::Insert(_) => Ok(CacheResponse::Clear),
                // Returns Insert response for Get/Invalidate requests (wrong type)
                CacheOperation::Get(_) | CacheOperation::Invalidate(_) => Ok(CacheResponse::Insert),
                // Returns Get response for Clear requests (wrong type)
                CacheOperation::Clear => Ok(CacheResponse::Get(None)),
            }
        }
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn adapter_get_wrong_response_returns_error() {
        let adapter = ServiceAdapter::new(WrongResponseService);
        let result = adapter.get(&"key".to_string()).await;
        result.unwrap_err();
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn adapter_insert_wrong_response_returns_error() {
        let adapter = ServiceAdapter::new(WrongResponseService);
        let result = adapter.insert("key".to_string(), CacheEntry::new(42)).await;
        result.unwrap_err();
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn adapter_invalidate_wrong_response_returns_error() {
        let adapter = ServiceAdapter::new(WrongResponseService);
        let result = adapter.invalidate(&"key".to_string()).await;
        result.unwrap_err();
    }

    #[cfg_attr(miri, ignore)]
    #[tokio::test]
    async fn adapter_clear_wrong_response_returns_error() {
        let adapter = ServiceAdapter::new(WrongResponseService);
        let result = adapter.clear().await;
        result.unwrap_err();
    }
}