snafu icon indicating copy to clipboard operation
snafu copied to clipboard

Any easy way to add common implicit data to an error enum?

Open thexiay opened this issue 1 year ago • 5 comments

currntly, if i want to get a err enum with some same detail info ,i must define them in every enum variants.

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum MyErrorEnum {
    Err1 {
        #[snafu(implicit)]
        backtrace: Backtrace,
        #[snafu(implicit)]
        loc: Location,
        #[snafu(implicit)]
        span: Span,
    },

    Err2 {
        detail: String,
        #[snafu(implicit)]
        backtrace: Backtrace,
        #[snafu(implicit)]
        loc: Location,
        #[snafu(implicit)]
        span: Span,
    },
    
    // ...
}

pub type Result<T> = std::error::Result<T, MyError>;

or like this

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub struct MyError {
    #[snafu(implicit)]
    backtrace: Backtrace,
    #[snafu(implicit)]
    loc: Location,
    #[snafu(implicit)]
    span: Span,
    source: MyErrorEnum,
}

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum MyErrorEnum {
    Err1,
    Err2(String),
    // ...
}

pub type Result<T> = std::error::Result<T, MyError>;

impl<E> From<E> for MetaError
where
    E: Into<MetaErrorEnum>,
{
    #[track_caller]
    fn from(error: E) -> Self {
        Self {
            source: error.into(),
            loc: GenerateImplicitData::generate(),
            span: GenerateImplicitData::generate(),
            backtrace: GenerateImplicitData::generate(),
        }
    }
}

impl MyError {
    pub fn inner(&self) -> &MyErrorEnum {
        // ...
    }
}

Is there an easier way to do this without having to write frustratingly repetitive property values?

thexiay avatar Nov 27 '24 12:11 thexiay

Those would be the two approaches that I know of as well. Perhaps there could be a way to automatically fill in MetaError upon context insertion, but it's worth pointing out that some of the implicit data might only be desirable in leaf error variants.

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum MyErrorEnum {
    Err1 {
        #[snafu(implicit)]
        backtrace: Backtrace,
        #[snafu(implicit)]
        loc: Location,
        #[snafu(implicit)]
        span: Span,
    },
    
    // has a source with the backtrace
    Err2 {
        #[snafu(backtrace)]
        source: BarError,
        #[snafu(implicit)]
        loc: Location,
        #[snafu(implicit)]
        span: Span,
    },
    
    // ...
}

Enet4 avatar Nov 27 '24 13:11 Enet4

do this without [writing] repetitive property values?

I appreciate this question because it's in the small set of things I can answer definitely: no. SNAFU is currently implemented as a derive macro and derive macros cannot modify the type they are attached to, they may only output additional code.

It's actually on my long-term roadmap to rewrite to an attribute macro [^1] because SNAFU produces additional types (the context selectors) and people generally don't expect a derive macro to do anything beyond implement a trait. Even in that future world, I don't know how I'd feel about SNAFU modifying the fields of the type.

Perhaps some crate out there provides an attribute macro that will automatically add an attribute + field pair to every enum? If so, then you could use it in addition to SNAFU.

In your case, it appears you have multiple implicit fields, so you could create a composite type that implements GenerateImplicitData by delegating off to its children. That way your error definitions would only need one field each, simplifying things a small bit.

Another possibility is to have a wrapping error that contains the implicit data and then always convert the inner error to the wrapping error:

use snafu::prelude::*;

#[derive(Debug, Snafu)]
enum Inner {
    #[snafu(display("Bad thing 1"))]
    Alfa,

    #[snafu(display("Bad thing 2"))]
    Beta,
}

#[derive(Debug, Snafu)]
#[snafu(transparent)]
struct Outer {
    source: Inner,

    #[snafu(implicit)]
    location: snafu::Location,
}

fn inner_main(value: bool) -> Result<(), Outer> {
    if value {
        AlfaSnafu.fail()?;
    } else {
        BetaSnafu.fail()?;
    }

    Ok(())
}

#[snafu::report]
fn main() -> Result<(), Outer> {
    let v = inner_main(std::env::args().count() > 1);
    if let Err(e) = &v {
        eprintln!("It happened at {}", e.location);
    }
    v
}

This will run into some ergonomic problems around type inference sometimes though, so I wouldn't recommend it as a first solution.


#[snafu(implicit)]
backtrace: Backtrace,

Note that you don't need implicit on a backtrace field — it's implicit.


#[snafu(implicit)]
backtrace: Backtrace,
#[snafu(implicit)]
loc: Location,

It seems odd to have both of these as the backtrace should contain the location, no?

[^1]: This would look like #[snafu] enum Error {} instead of #[derive(Snafu)] enum Error {}.

shepmaster avatar Nov 27 '24 19:11 shepmaster

do this without [writing] repetitive property values?

I appreciate this question because it's in the small set of things I can answer definitely: no. SNAFU is currently implemented as a derive macro and derive macros cannot modify the type they are attached to, they may only output additional code.

It's actually on my long-term roadmap to rewrite to an attribute macro 1 because SNAFU produces additional types (the context selectors) and people generally don't expect a derive macro to do anything beyond implement a trait. Even in that future world, I don't know how I'd feel about SNAFU modifying the fields of the type.

Perhaps some crate out there provides an attribute macro that will automatically add an attribute + field pair to every enum? If so, then you could use it in addition to SNAFU.

In your case, it appears you have multiple implicit fields, so you could create a composite type that implements GenerateImplicitData by delegating off to its children. That way your error definitions would only need one field each, simplifying things a small bit.

Another possibility is to have a wrapping error that contains the implicit data and then always convert the inner error to the wrapping error:

use snafu::prelude::*;

#[derive(Debug, Snafu)]
enum Inner {
    #[snafu(display("Bad thing 1"))]
    Alfa,

    #[snafu(display("Bad thing 2"))]
    Beta,
}

#[derive(Debug, Snafu)]
#[snafu(transparent)]
struct Outer {
    source: Inner,

    #[snafu(implicit)]
    location: snafu::Location,
}

fn inner_main(value: bool) -> Result<(), Outer> {
    if value {
        AlfaSnafu.fail()?;
    } else {
        BetaSnafu.fail()?;
    }

    Ok(())
}

#[snafu::report]
fn main() -> Result<(), Outer> {
    let v = inner_main(std::env::args().count() > 1);
    if let Err(e) = &v {
        eprintln!("It happened at {}", e.location);
    }
    v
}

This will run into some ergonomic problems around type inference sometimes though, so I wouldn't recommend it as a first solution.

#[snafu(implicit)]
backtrace: Backtrace,

Note that you don't need implicit on a backtrace field — it's implicit.

#[snafu(implicit)]
backtrace: Backtrace,
#[snafu(implicit)]
loc: Location,

It seems odd to have both of these as the backtrace should contain the location, no?

Footnotes

  1. This would look like #[snafu] enum Error {} instead of #[derive(Snafu)] enum Error {}.

yep, maybe attribute macro is more flexible. I use the second way to wrap the common implict data, but I write a lot of other convert function manaually, it's a littele bit weird, how I wish they were automatically generated😁

thexiay avatar Nov 28 '24 08:11 thexiay

currntly, if i want to get a err enum with some same detail info ,i must define them in every enum variants.

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum MyErrorEnum {
    Err1 {
        #[snafu(implicit)]
        backtrace: Backtrace,
        #[snafu(implicit)]
        loc: Location,
        #[snafu(implicit)]
        span: Span,
    },

    Err2 {
        detail: String,
        #[snafu(implicit)]
        backtrace: Backtrace,
        #[snafu(implicit)]
        loc: Location,
        #[snafu(implicit)]
        span: Span,
    },
    
    // ...
}

pub type Result<T> = std::error::Result<T, MyError>;

or like this

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub struct MyError {
    #[snafu(implicit)]
    backtrace: Backtrace,
    #[snafu(implicit)]
    loc: Location,
    #[snafu(implicit)]
    span: Span,
    source: MyErrorEnum,
}

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum MyErrorEnum {
    Err1,
    Err2(String),
    // ...
}

pub type Result<T> = std::error::Result<T, MyError>;

impl<E> From<E> for MetaError
where
    E: Into<MetaErrorEnum>,
{
    #[track_caller]
    fn from(error: E) -> Self {
        Self {
            source: error.into(),
            loc: GenerateImplicitData::generate(),
            span: GenerateImplicitData::generate(),
            backtrace: GenerateImplicitData::generate(),
        }
    }
}

impl MyError {
    pub fn inner(&self) -> &MyErrorEnum {
        // ...
    }
}

Is there an easier way to do this without having to write frustratingly repetitive property values?

https://github.com/yuanyan3060/enum_expand

Maybe you can try this crate, and if it works, I'll upload it to crate.io

yuanyan3060 avatar Dec 16 '24 06:12 yuanyan3060

We at number0 have written a macro that has nothing to do with snafu whatsoever that just adds common fields to each case of an enum.

Usage, for example:


    /// Error that you can get from [`AtConnected::next`]
    #[common_fields({
        backtrace: Option<Backtrace>,
        #[snafu(implicit)]
        span_trace: SpanTrace,
    })]
    #[allow(missing_docs)]
    #[derive(Debug, Snafu)]
    #[non_exhaustive]
    pub enum ConnectedNextError {
        /// Error when serializing the request
        #[snafu(display("postcard ser: {source}"))]
        PostcardSer { source: postcard::Error },
        /// The serialized request is too long to be sent
        #[snafu(display("request too big"))]
        RequestTooBig {},
        /// Error when writing the request to the [`SendStream`].
        #[snafu(display("write: {source}"))]
        Write { source: quinn::WriteError },
        /// Quic connection is closed.
        #[snafu(display("closed"))]
        Closed { source: quinn::ClosedStream },
        /// A generic io error
        #[snafu(transparent)]
        Io { source: io::Error },
    }

it is published in https://docs.rs/nested_enum_utils/latest/nested_enum_utils/attr.common_fields.html for now. If there was interest we could also make a PR to snafu.

rklaehn avatar Jun 25 '25 15:06 rklaehn