stillwater 1.0.1

Pragmatic effect composition and validation for Rust - pure core, imperative shell
Documentation
//! EnsureWith combinator for validation with lazy error construction.

use crate::effect::Effect;

/// Validates with an error factory function.
///
/// Like `Ensure`, but the error is only constructed if the predicate fails.
/// The error function receives a reference to the value, allowing for
/// context-specific error messages.
///
/// # Example
///
/// ```rust,ignore
/// use stillwater::effect::prelude::*;
///
/// let effect = pure::<_, String, ()>(-5)
///     .ensure_with(
///         |x| *x > 0,
///         |x| format!("{} is not positive", x)
///     );
///
/// assert_eq!(effect.execute(&()).await, Err("-5 is not positive".to_string()));
/// ```
pub struct EnsureWith<E, P, F> {
    pub(crate) inner: E,
    pub(crate) predicate: P,
    pub(crate) error_fn: F,
}

impl<E, P, F> std::fmt::Debug for EnsureWith<E, P, F> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("EnsureWith")
            .field("inner", &"<effect>")
            .field("predicate", &"<function>")
            .field("error_fn", &"<function>")
            .finish()
    }
}

impl<E, P, F> EnsureWith<E, P, F> {
    /// Create a new EnsureWith combinator.
    pub fn new(inner: E, predicate: P, error_fn: F) -> Self {
        EnsureWith {
            inner,
            predicate,
            error_fn,
        }
    }
}

impl<E, P, F> Effect for EnsureWith<E, P, F>
where
    E: Effect,
    P: FnOnce(&E::Output) -> bool + Send,
    F: FnOnce(&E::Output) -> E::Error + Send,
{
    type Output = E::Output;
    type Error = E::Error;
    type Env = E::Env;

    async fn run(self, env: &Self::Env) -> Result<Self::Output, Self::Error> {
        let value = self.inner.run(env).await?;
        if (self.predicate)(&value) {
            Ok(value)
        } else {
            Err((self.error_fn)(&value))
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::effect::constructors::pure;
    use crate::effect::EffectExt;

    #[tokio::test]
    async fn test_ensure_with_lazy_error() {
        let effect =
            pure::<_, String, ()>(-5).ensure_with(|x| *x > 0, |x| format!("{} is not positive", x));
        assert_eq!(
            effect.execute(&()).await,
            Err("-5 is not positive".to_string())
        );
    }

    #[tokio::test]
    async fn test_ensure_with_passes() {
        let effect =
            pure::<_, String, ()>(5).ensure_with(|x| *x > 0, |x| format!("{} is not positive", x));
        assert_eq!(effect.execute(&()).await, Ok(5));
    }

    #[tokio::test]
    async fn test_ensure_with_error_not_called_on_success() {
        let mut called = false;
        let effect = pure::<_, String, ()>(5).ensure_with(
            |x| *x > 0,
            |_| {
                called = true;
                "error".to_string()
            },
        );
        assert_eq!(effect.execute(&()).await, Ok(5));
        // Note: In the actual implementation, we can't check 'called'
        // because error_fn is moved into the closure. This is just
        // documenting the behavior - error_fn is only called on failure.
    }
}