snafu icon indicating copy to clipboard operation
snafu copied to clipboard

Add support for impl From<T> for ErrorEnum where T: Into<OneVariantsSource>

Open shepmaster opened this issue 4 months ago • 5 comments

Prior comments:

  • https://github.com/shepmaster/snafu/issues/481#issuecomment-3263186224
  • https://github.com/shepmaster/snafu/issues/481#issuecomment-3267179024
  • https://github.com/shepmaster/snafu/issues/481#issuecomment-3267273677
#[derive(Snafu)]
enum Error {
    #[snafu(transparent)]
    #[snafu(source(forward))]
    DomainA { source: a::Error },
    #[snafu(transparent)]
    DomainB { source: b::Error },
}

Generated:

impl<T> From<T> for Error
where T: Into<a::Error>
{
   fn from(val: T) -> Self {
       Self::DomainA { source: val.into() }
   }
}

impl From<b::Error> for Error {
   fn from(val: b::Error) -> Self {
       Self::DomainB { source: val }
   }
}

Since Rust does not yet support specialization, I still need to call map_err for T: Into<b::error>. However, a forward From implementation for the most frequently used variant can significantly reduces boilerplate.

Originally posted by @Huliiiiii in #481

shepmaster avatar Sep 08 '25 18:09 shepmaster

I was continuing to edit my comment while you were responding (sorry!) so you likely missed this question:

Notably, I'm not understanding why you'd want such functionality for an enum variant — under what cases do you have multiple underlying error types that you want to collapse into a single error variant?

shepmaster avatar Sep 08 '25 18:09 shepmaster

This is the actual code. I define repositories and their errors using traits and associated types. As a result, the error type returned by repositories is impl Into<infra::Error>.

This might be a poor abstraction, or perhaps I should re-design my errors. That's why my original feature request was to provide a way to disable automatic From implementation generation.

#[derive(Debug, snafu::Snafu, ApiError)]
pub enum SignInError {
    #[snafu(display("Already signed in"))]
    #[api_error(
        status_code = StatusCode::CONFLICT,
    )]
    AlreadySignedIn,
    #[snafu(transparent)]
    Authn { source: AuthnError },
    #[snafu(transparent)]
    Infra { source: infra::Error },
    #[snafu(transparent)]
    Validate { source: ValidateCredsError },
}

Huliiiiii avatar Sep 08 '25 18:09 Huliiiiii

Work I've been doing locally would allow for you to write this:

#[derive(Debug, Snafu)]
enum SignInError {
    #[snafu(display("Already signed in"))]
    AlreadySignedIn,

    #[snafu(transparent)]
    Authn {
        #[snafu(source(from(generic)))]
        source: AuthnError
    },

    #[snafu(transparent)]
    Infra { source: InfraError },

    #[snafu(transparent)]
    Validate { source: ValidateCredsError },
}

However, you would not be able to add a second #[snafu(source(from(generic)))] (e.g. in the Infra variant) because that would generate conflicting implementations of From:

impl<__SnafuSource> ::core::convert::From<__SnafuSource> for SignInError
where
    __SnafuSource: ::core::convert::Into<AuthnError>,
{
    #[track_caller]
    fn from(error: __SnafuSource) -> Self {
        let error: AuthnError = (Into::into)(error);
        SignInError::Authn {
            source: error,
        }
    }
}
impl<__SnafuSource> ::core::convert::From<__SnafuSource> for SignInError
where
    __SnafuSource: ::core::convert::Into<InfraError>,
{
    #[track_caller]
    fn from(error: __SnafuSource) -> Self {
        let error: InfraError = (Into::into)(error);
        SignInError::Infra {
            source: error,
        }
    }
}

This is a fundamental restriction imposed by Rust as nothing prevents a single type from being converted into both AuthnError and InfraError.

shepmaster avatar Oct 21 '25 18:10 shepmaster

I define repositories and their errors using traits and associated types. As a result, the error type returned by repositories is impl Into<infra::Error>.

This is one of the few cases where I actively recommend using a boxed error, such as by using ResultExt::boxed, although that still requires an extra method call (.boxed()), similar to the original .map_err(...) that you wanted to avoid.

shepmaster avatar Oct 21 '25 18:10 shepmaster

If people are interested in trying this out, please check out #537 (including the docs) and use it in your project to ensure it works for you.

[patch.crates-io]
snafu = { git = 'https://github.com/shepmaster/snafu.git', branch = 'from-generic' }

shepmaster avatar Dec 08 '25 17:12 shepmaster