do-over 0.1.0

Async resilience policies for Rust inspired by Polly
Documentation
//! Policy composition using the Wrap combinator.
//!
//! The [`Wrap`] struct allows you to compose multiple policies together,
//! creating sophisticated resilience strategies.
//!
//! # Execution Order
//!
//! Execution flows from outer → inner → operation. The outer policy
//! wraps the inner policy, which wraps your operation.
//!
//! # Recommended Policy Ordering
//!
//! From outer to inner:
//! 1. **Bulkhead** - Limit concurrency first
//! 2. **Circuit Breaker** - Fast-fail if too many errors
//! 3. **Rate Limiter** - Throttle requests
//! 4. **Retry** - Handle transient failures
//! 5. **Timeout** - Bound individual attempts
//!
//! # Examples
//!
//! ```rust
//! use do_over::{policy::Policy, wrap::Wrap, retry::RetryPolicy, timeout::TimeoutPolicy, error::DoOverError};
//! use std::time::Duration;
//!
//! # async fn example() {
//! // Simple composition: retry with timeout
//! let policy = Wrap::new(
//!     RetryPolicy::fixed(3, Duration::from_millis(100)),
//!     TimeoutPolicy::new(Duration::from_secs(5)),
//! );
//!
//! let result: Result<&str, DoOverError<&str>> = policy.execute(|| async {
//!     Ok("success")
//! }).await;
//! # }
//! ```

use std::future::Future;
use crate::policy::Policy;

/// Composes two policies together.
///
/// The outer policy wraps the inner policy. Execution flows:
/// `outer → inner → operation`
///
/// # Type Parameters
///
/// * `O` - The outer policy type
/// * `I` - The inner policy type
///
/// # Examples
///
/// Basic composition:
///
/// ```rust
/// use do_over::{wrap::Wrap, retry::RetryPolicy, timeout::TimeoutPolicy};
/// use std::time::Duration;
///
/// let policy = Wrap::new(
///     RetryPolicy::fixed(3, Duration::from_millis(100)),
///     TimeoutPolicy::new(Duration::from_secs(5)),
/// );
/// ```
///
/// Multi-layer composition:
///
/// ```rust
/// use do_over::{wrap::Wrap, bulkhead::Bulkhead, retry::RetryPolicy, timeout::TimeoutPolicy};
/// use std::time::Duration;
///
/// let policy = Wrap::new(
///     Bulkhead::new(10),
///     Wrap::new(
///         RetryPolicy::fixed(3, Duration::from_millis(100)),
///         TimeoutPolicy::new(Duration::from_secs(5)),
///     ),
/// );
/// ```
#[derive(Clone)]
pub struct Wrap<O, I> {
    /// The outer policy (executed first).
    pub outer: O,
    /// The inner policy (executed second, wrapping the operation).
    pub inner: I,
}

impl<O, I> Wrap<O, I> {
    /// Create a new policy composition.
    ///
    /// # Arguments
    ///
    /// * `outer` - The outer policy (executed first)
    /// * `inner` - The inner policy (wraps the operation)
    ///
    /// # Examples
    ///
    /// ```rust
    /// use do_over::{wrap::Wrap, retry::RetryPolicy, timeout::TimeoutPolicy};
    /// use std::time::Duration;
    ///
    /// let policy = Wrap::new(
    ///     RetryPolicy::fixed(3, Duration::from_millis(100)),
    ///     TimeoutPolicy::new(Duration::from_secs(5)),
    /// );
    /// ```
    pub fn new(outer: O, inner: I) -> Self {
        Self { outer, inner }
    }
}

#[async_trait::async_trait]
impl<O, I, E> Policy<E> for Wrap<O, I>
where
    O: Policy<E>,
    I: Policy<E>,
    E: Send + Sync,
{
    async fn execute<F, Fut, T>(&self, f: F) -> Result<T, E>
    where
        F: Fn() -> Fut + Send + Sync,
        Fut: Future<Output = Result<T, E>> + Send,
        T: Send,
    {
        self.outer.execute(|| self.inner.execute(&f)).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::retry::RetryPolicy;
    use crate::timeout::TimeoutPolicy;
    use crate::error::DoOverError;
    use std::time::Duration;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::Arc;

    #[tokio::test]
    async fn test_wrap_retry_and_timeout() {
        let attempts = Arc::new(AtomicUsize::new(0));
        let attempts_clone = Arc::clone(&attempts);

        let retry = RetryPolicy::fixed(2, Duration::from_millis(10));
        let timeout = TimeoutPolicy::new(Duration::from_secs(1));

        let wrapped = Wrap::new(retry, timeout);

        let result: Result<String, DoOverError<std::io::Error>> = wrapped
            .execute(|| {
                let a = Arc::clone(&attempts_clone);
                async move {
                    let count = a.fetch_add(1, Ordering::SeqCst);
                    if count < 1 {
                        Err(DoOverError::Inner(std::io::Error::new(
                            std::io::ErrorKind::Other,
                            "temporary failure",
                        )))
                    } else {
                        Ok("success".to_string())
                    }
                }
            })
            .await;

        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "success");
        assert_eq!(attempts.load(Ordering::SeqCst), 2);
    }

    #[tokio::test]
    async fn test_wrap_composition() {
        let timeout = TimeoutPolicy::new(Duration::from_millis(100));
        let retry = RetryPolicy::fixed(1, Duration::from_millis(10));

        let wrapped = Wrap::new(timeout, retry);

        // This should timeout because the inner operation takes too long
        let result: Result<(), DoOverError<()>> = wrapped
            .execute(|| async {
                tokio::time::sleep(Duration::from_millis(200)).await;
                Ok(())
            })
            .await;

        assert!(matches!(result, Err(DoOverError::Timeout)));
    }
}