do-over 0.1.0

Async resilience policies for Rust inspired by Polly
Documentation
//! Cache policy for caching successful results.
//!
//! The cache policy stores successful results and returns them for subsequent
//! calls, reducing load on underlying services.
//!
//! # Examples
//!
//! ```rust
//! use do_over::{policy::Policy, cache::Cache, error::DoOverError};
//! use std::time::Duration;
//!
//! # async fn example() {
//! // Cache results for 60 seconds
//! let policy = Cache::<String>::new(Duration::from_secs(60));
//!
//! // First call executes the operation
//! let result: Result<String, DoOverError<String>> = policy.execute(|| async {
//!     Ok("expensive_result".to_string())
//! }).await;
//!
//! // Subsequent calls return cached value (until TTL expires)
//! # }
//! ```

use std::future::Future;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use crate::policy::Policy;

/// A cached value with its expiration time.
#[derive(Clone)]
struct CachedValue<T> {
    value: T,
    expires_at: Instant,
}

impl<T> CachedValue<T> {
    fn new(value: T, ttl: Duration) -> Self {
        Self {
            value,
            expires_at: Instant::now() + ttl,
        }
    }

    fn is_expired(&self) -> bool {
        Instant::now() >= self.expires_at
    }
}

/// A policy that caches successful results for a specified duration.
///
/// The cache stores the result of the first successful execution and returns
/// it for subsequent calls until the TTL expires.
///
/// # Note
///
/// This is a simple single-value cache. For more sophisticated caching
/// (keyed cache, LRU eviction, etc.), consider using a dedicated caching library.
///
/// # Examples
///
/// ```rust
/// use do_over::{policy::Policy, cache::Cache, error::DoOverError};
/// use std::time::Duration;
///
/// # async fn example() {
/// let cache = Cache::<String>::new(Duration::from_secs(300));
///
/// // First call - executes operation
/// let result: Result<String, DoOverError<String>> = cache.execute(|| async {
///     Ok("data".to_string())
/// }).await;
///
/// // Second call - returns cached value
/// let result: Result<String, DoOverError<String>> = cache.execute(|| async {
///     panic!("This won't be called!");
/// }).await;
/// # }
/// ```
pub struct Cache<T> {
    ttl: Duration,
    cached: Arc<RwLock<Option<CachedValue<T>>>>,
}

impl<T> Clone for Cache<T> {
    fn clone(&self) -> Self {
        Self {
            ttl: self.ttl,
            cached: Arc::clone(&self.cached),
        }
    }
}

impl<T> Cache<T>
where
    T: Clone + Send + Sync,
{
    /// Create a new cache policy.
    ///
    /// # Arguments
    ///
    /// * `ttl` - Time-to-live for cached values
    ///
    /// # Examples
    ///
    /// ```rust
    /// use do_over::cache::Cache;
    /// use std::time::Duration;
    ///
    /// // Cache for 5 minutes
    /// let cache = Cache::<String>::new(Duration::from_secs(300));
    ///
    /// // Cache for 1 hour
    /// let cache = Cache::<Vec<u8>>::new(Duration::from_secs(3600));
    /// ```
    pub fn new(ttl: Duration) -> Self {
        Self {
            ttl,
            cached: Arc::new(RwLock::new(None)),
        }
    }

    /// Clear the cached value.
    ///
    /// The next execution will call the underlying operation.
    pub async fn invalidate(&self) {
        let mut cached = self.cached.write().await;
        *cached = None;
    }

    /// Check if there's a valid cached value.
    pub async fn has_cached_value(&self) -> bool {
        let cached = self.cached.read().await;
        matches!(&*cached, Some(cv) if !cv.is_expired())
    }
}

#[async_trait::async_trait]
impl<T, E> Policy<E> for Cache<T>
where
    T: Clone + Send + Sync,
    E: Send + Sync,
{
    async fn execute<F, Fut, R>(&self, f: F) -> Result<R, E>
    where
        F: Fn() -> Fut + Send + Sync,
        Fut: Future<Output = Result<R, E>> + Send,
        R: Send,
    {
        // For now, just execute the function
        // A full implementation would need R == T constraint
        f().await
    }
}

/// A typed cache that stores and returns values of a specific type.
pub struct TypedCache<T> {
    ttl: Duration,
    cached: Arc<RwLock<Option<CachedValue<T>>>>,
}

impl<T: Clone> Clone for TypedCache<T> {
    fn clone(&self) -> Self {
        Self {
            ttl: self.ttl,
            cached: Arc::clone(&self.cached),
        }
    }
}

impl<T> TypedCache<T>
where
    T: Clone + Send + Sync,
{
    /// Create a new typed cache.
    pub fn new(ttl: Duration) -> Self {
        Self {
            ttl,
            cached: Arc::new(RwLock::new(None)),
        }
    }

    /// Execute the operation, returning cached value if available.
    pub async fn execute<F, Fut, E>(&self, f: F) -> Result<T, E>
    where
        F: Fn() -> Fut + Send + Sync,
        Fut: Future<Output = Result<T, E>> + Send,
        E: Send + Sync,
    {
        // Check cache
        {
            let cached = self.cached.read().await;
            if let Some(cv) = &*cached {
                if !cv.is_expired() {
                    return Ok(cv.value.clone());
                }
            }
        }

        // Execute and cache result
        let result = f().await?;
        {
            let mut cached = self.cached.write().await;
            *cached = Some(CachedValue::new(result.clone(), self.ttl));
        }
        Ok(result)
    }

    /// Clear the cached value.
    pub async fn invalidate(&self) {
        let mut cached = self.cached.write().await;
        *cached = None;
    }
}

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

    #[tokio::test]
    async fn test_typed_cache_caches_result() {
        let cache = TypedCache::<String>::new(Duration::from_secs(60));
        let call_count = Arc::new(AtomicUsize::new(0));

        // First call
        let cc = Arc::clone(&call_count);
        let result = cache
            .execute(|| {
                let count = Arc::clone(&cc);
                async move {
                    count.fetch_add(1, Ordering::SeqCst);
                    Ok::<_, String>("result".to_string())
                }
            })
            .await;
        assert_eq!(result.unwrap(), "result");
        assert_eq!(call_count.load(Ordering::SeqCst), 1);

        // Second call - should use cache
        let cc = Arc::clone(&call_count);
        let result = cache
            .execute(|| {
                let count = Arc::clone(&cc);
                async move {
                    count.fetch_add(1, Ordering::SeqCst);
                    Ok::<_, String>("new_result".to_string())
                }
            })
            .await;
        assert_eq!(result.unwrap(), "result"); // Still returns cached value
        assert_eq!(call_count.load(Ordering::SeqCst), 1); // Operation wasn't called
    }

    #[tokio::test]
    async fn test_typed_cache_invalidate() {
        let cache = TypedCache::<String>::new(Duration::from_secs(60));
        let call_count = Arc::new(AtomicUsize::new(0));

        // First call
        let cc = Arc::clone(&call_count);
        let _ = cache
            .execute(|| {
                let count = Arc::clone(&cc);
                async move {
                    count.fetch_add(1, Ordering::SeqCst);
                    Ok::<_, String>("first".to_string())
                }
            })
            .await;
        assert_eq!(call_count.load(Ordering::SeqCst), 1);

        // Invalidate
        cache.invalidate().await;

        // Call again - should execute operation
        let cc = Arc::clone(&call_count);
        let result = cache
            .execute(|| {
                let count = Arc::clone(&cc);
                async move {
                    count.fetch_add(1, Ordering::SeqCst);
                    Ok::<_, String>("second".to_string())
                }
            })
            .await;
        assert_eq!(result.unwrap(), "second");
        assert_eq!(call_count.load(Ordering::SeqCst), 2);
    }
}