shepmaster / snafu

Easily assign underlying errors into domain-specific errors while adding context
https://docs.rs/snafu/
Apache License 2.0
1.39k stars 60 forks source link

Add an ensure macro that works with pattern matching #429

Closed YjyJeff closed 8 months ago

YjyJeff commented 8 months ago

In some of the cases, we need to ensure the variable is a specific variant and return an error if the pattern does not match. We can achieve it with let-else now. For example:

enum Input{
    A(string),
    B(i64),
}

let Input::A(val) = input else{
    return Err(....)
}

In the snafu, we encourage the user to use ensure macros to check the condition. Could we provide a new macro ensure_variant to check the pattern matching?

shepmaster commented 8 months ago

I've had similar thoughts before, but a repeatable structure has never really emerged. For example, for Advent of Code, I've written a number of things where I use the wrong value in the error:

Ok(match c {
    'a' => Thing::A,
    'b' => Thing::B,
    other => return InvalidSnafu { other }.fail(),
})

Some questions to tease out details...

shepmaster commented 8 months ago

In similar cases, I use SNAFU as part of a bigger picture. I'll often define an enum so that each variant is exactly zero or one tuple, then sub structs with the details and conversion methods:

use snafu::prelude::*;

#[derive(Debug)]
enum Input {
    A(A),
    B(B),
}

#[derive(Debug)]
struct A(String);
#[derive(Debug)]
struct B(i64);

impl From<A> for Input {
    fn from(other: A) -> Self {
        Self::A(other)
    }
}

impl TryFrom<Input> for A {
    type Error = InvalidError;

    fn try_from(original: Input) -> Result<Self, Self::Error> {
        match original {
            Input::A(v) => Ok(v),
            original => InvalidSnafu { original }.fail(),
        }
    }
}

#[derive(Debug, Snafu)]
#[snafu(display("The enum was not the requested type"))]
struct InvalidError {
    original: Input,
}

I then often create an enum to generate that for me:

use snafu::prelude::*;

macro_rules! awesome {
    (
        enum $e_name:ident {
            $(
                $v_name:ident($v_field:ty)
            ,)+
        }
    ) => (
        #[derive(Debug)]
        enum $e_name {
            $($v_name($v_name),)*
        }

        $(
            #[derive(Debug)]
            struct $v_name($v_field);

            impl From<$v_name> for $e_name {
                fn from(other: $v_name) -> Self {
                    Self::$v_name(other)
                }
            }

            impl TryFrom<$e_name> for $v_name {
                type Error = InvalidError;

                fn try_from(original: $e_name) -> Result<Self, Self::Error> {
                    match original {
                        Input::$v_name(v) => Ok(v),
                        original => InvalidSnafu { original }.fail(),
                    }
                }
            }
        )*
    );
}

awesome! {
    enum Input {
        A(String),
        B(i64),
    }
}

#[derive(Debug, Snafu)]
#[snafu(display("The enum was not the requested type"))]
struct InvalidError {
    original: Input,
}

I'm certain there are crates out there that do the same as that macro (likely even better!), but I tend to write the one-off each time I need it.