okerrr 0.1.0

programmatic freedom from matching Result<T, E>—okerrr!
Documentation
use okerrr::okerrr;
use std::cell::Cell;
use std::future::Future;
use std::sync::Arc;
use std::task::{Context, Poll, Wake, Waker};

struct NoopWake;

impl Wake for NoopWake {
    fn wake(self: Arc<Self>) {}
}

fn poll_ready<F: Future>(future: F) -> F::Output {
    let waker = Waker::from(Arc::new(NoopWake));
    let mut context = Context::from_waker(&waker);
    let mut future = Box::pin(future);

    match future.as_mut().poll(&mut context) {
        Poll::Ready(output) => output,
        Poll::Pending => panic!("test future unexpectedly returned pending"),
    }
}

#[test]
fn extracts_ok_payload() {
    fn double(input: Result<i32, &'static str>) -> Result<i32, &'static str> {
        let value = okerrr!(input, case error => return Err(error));
        Ok(value * 2)
    }

    assert_eq!(double(Ok(21)), Ok(42));
}

#[test]
fn binds_and_transforms_err_payload() {
    fn double(input: Result<i32, &'static str>) -> Result<i32, String> {
        let value = okerrr!(input, case error => {
            return Err(format!("invalid input: {error}"));
        });
        Ok(value * 2)
    }

    assert_eq!(double(Err("empty")), Err("invalid input: empty".into()));
}

#[test]
fn evaluates_input_once() {
    fn extract(calls: &Cell<usize>, input: Result<i32, &'static str>) -> Result<i32, &'static str> {
        let value = okerrr!(
            {
                calls.set(calls.get() + 1);
                input
            },
            case error => return Err(error)
        );
        Ok(value)
    }

    let calls = Cell::new(0);
    assert_eq!(extract(&calls, Ok(7)), Ok(7));
    assert_eq!(calls.get(), 1);

    assert_eq!(extract(&calls, Err("bad")), Err("bad"));
    assert_eq!(calls.get(), 2);
}

#[test]
fn controls_enclosing_loop() {
    enum ItemError {
        Ignore,
        Skip,
        Stop,
    }

    let items = [
        Ok(1),
        Err(ItemError::Ignore),
        Err(ItemError::Skip),
        Ok(3),
        Err(ItemError::Stop),
        Ok(5),
    ];
    let mut sum = 0;

    for item in items {
        let value = okerrr!(
            item,
            case ItemError::Ignore | ItemError::Skip => continue,
            case ItemError::Stop => break,
        );
        sum += value;
    }

    assert_eq!(sum, 4);
}

#[test]
fn accepts_explicit_panic_without_formatting_error() {
    struct Opaque;

    let panic = std::panic::catch_unwind(|| {
        let input: Result<i32, Opaque> = Err(Opaque);
        okerrr!(input, case _ => panic!("required value was missing"))
    });

    assert!(panic.is_err());
}

#[test]
fn accepts_awaited_input_in_async_context() {
    async fn load(input: Result<i32, &'static str>) -> Result<i32, &'static str> {
        let value = okerrr!(async { input }.await, case error => return Err(error));
        Ok(value)
    }

    assert_eq!(poll_ready(load(Ok(7))), Ok(7));
    assert_eq!(poll_ready(load(Err("unavailable"))), Err("unavailable"));
}

#[test]
fn borrows_shared_payload_without_consuming_result() {
    fn extract(input: &Result<String, String>) -> Result<&str, &str> {
        let value = okerrr!(input, case error => return Err(error.as_str()));
        Ok(value.as_str())
    }

    let input = Ok(String::from("value"));

    assert_eq!(extract(&input), Ok("value"));
    assert_eq!(input.as_deref(), Ok("value"));
}

#[test]
fn borrows_mutable_payload_from_either_variant() {
    fn increment(input: &mut Result<i32, i32>) {
        let value = okerrr!(input, case error => {
            *error += 1;
            return;
        });
        *value += 1;
    }

    let mut success = Ok(1);
    increment(&mut success);
    assert_eq!(success, Ok(2));

    let mut failure = Err(1);
    increment(&mut failure);
    assert_eq!(failure, Err(2));
}

#[test]
fn dispatches_destructured_errors_with_guards() {
    #[derive(Debug, PartialEq)]
    enum FetchError {
        Busy { attempt: u8 },
        Fatal,
    }

    fn load(input: Result<i32, FetchError>, retries: u8) -> Result<i32, FetchError> {
        let value = okerrr!(
            input,
            case FetchError::Busy { attempt } if attempt < retries => return Ok(attempt.into()),
            case error @ FetchError::Busy { .. } => return Err(error),
            case error => return Err(error),
        );
        Ok(value)
    }

    assert_eq!(load(Ok(7), 3), Ok(7));
    assert_eq!(load(Err(FetchError::Busy { attempt: 2 }), 3), Ok(2));
    assert_eq!(
        load(Err(FetchError::Busy { attempt: 3 }), 3),
        Err(FetchError::Busy { attempt: 3 })
    );
    assert_eq!(load(Err(FetchError::Fatal), 3), Err(FetchError::Fatal));
}

#[test]
fn dispatches_boxed_trait_object_errors() {
    trait SomeError {
        fn is_retryable(&self) -> bool;
    }

    struct DynamicError {
        retryable: bool,
    }

    impl SomeError for DynamicError {
        fn is_retryable(&self) -> bool {
            self.retryable
        }
    }

    fn sum(
        inputs: impl IntoIterator<Item = Result<i32, Box<dyn SomeError>>>,
    ) -> Result<i32, Box<dyn SomeError>> {
        let mut total = 0;

        for input in inputs {
            let value = okerrr!(
                input,
                case error if error.is_retryable() => continue,
                case error => return Err(error),
            );
            total += value;
        }

        Ok(total)
    }

    let retryable: Result<i32, Box<dyn SomeError>> =
        Err(Box::new(DynamicError { retryable: true }));
    assert_eq!(sum([Ok(1), retryable, Ok(2)]).ok(), Some(3));

    let fatal: Result<i32, Box<dyn SomeError>> = Err(Box::new(DynamicError { retryable: false }));
    let error = match sum([fatal]) {
        Err(error) => error,
        Ok(value) => panic!("unexpected success: {value}"),
    };
    assert!(!error.is_retryable());
}