snafu icon indicating copy to clipboard operation
snafu copied to clipboard

Allow for pattern-matching of errors to different error contexts

Open leosuggate opened this issue 5 years ago • 4 comments

I would love to see a way to return different error contexts based on a consumed error. As far as I'm aware, such functionality is not currently possible with Snafu.

This could be implemented by adding a new function to the ResultExt trait, nearly identically to with_context, with the only change being to provide the error as an argument to the closure:

fn map_context<F, C, E2>(self, context: F) -> Result<T, E2>
where
    F: FnOnce(&E) -> C,
    C: IntoError<E2, Source = E>,
    E2: Error + ErrorCompat,
{
    self.map_err(|error| {
        let context = context(&error);
        context.into_error(error)
    })
}

For a usage example, consider this code:

use std::{fs, io, path::PathBuf};
use snafu::{Snafu, ResultExt};

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Path `{}` does not exist.", path.display()))]
    PathDoesNotExist { path: PathBuf },

    #[snafu(display("Insufficient permissions when attempting to access `{}`.", path.display()))]
    PathAccessPermissionDenied { path: PathBuf },

    #[snafu(display("Could not gather metadata for `{}`: {}", path.display(), source))]
    GetPathMetadata { path: PathBuf, source: io::Error },
}

fn main() -> Result<(), Error> {
    let meta = &fs::metadata(path)
        .map_context(|e| match e.kind() {
            io::ErrorKind::NotFound => PathDoesNotExist { path },
            io::ErrorKind::PermissionDenied => PathAccessPermissionDenied { path },
            _ => GetDirectoryMetadata { path },
        })?;

    Ok(())
}

As for a little necessary bikeshedding, I do feel that the name map_context is a little ambiguous, as it implies mapping an error's context to another context, when it's really mapping an error to a new context. On the other hand, map_err_to_context seems a little verbose. For what it's worth, when I first took a look at the API, I'd expected that with_context would provide the error as a parameter.

leosuggate avatar Aug 05 '20 12:08 leosuggate

I'm not totally following your motivating example.

  1. GetPathMetadata / GetDirectoryMetadata look like maybe they are supposed to be the same (a typo?) but they have different fields.

  2. PathDoesNotExist and PathAccessPermissionDenied don't have an io::Error so I don't see how they would be used on the result of fs::metadata.

shepmaster avatar Aug 25 '20 01:08 shepmaster

I suppose one close analog you can do today is

use snafu::{IntoError, Snafu};
use std::{fs, io, path::PathBuf};

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Path `{}` does not exist", path.display()))]
    PathDoesNotExist { path: PathBuf, source: io::Error },

    #[snafu(display("Insufficient permissions when attempting to access `{}`", path.display()))]
    PathAccessPermissionDenied { path: PathBuf, source: io::Error },

    #[snafu(display("Could not gather metadata for `{}`", path.display()))]
    GetDirectoryMetadata { path: PathBuf, source: io::Error },
}

fn main() -> Result<(), Error> {
    let path = "/dev/null";
    let _meta = fs::metadata(path).map_err(|e| match e.kind() {
        io::ErrorKind::NotFound => PathDoesNotExist { path }.into_error(e),
        io::ErrorKind::PermissionDenied => PathAccessPermissionDenied { path }.into_error(e),
        _ => GetDirectoryMetadata { path }.into_error(e),
    })?;

    Ok(())
}

Note that you can also create your own extension trait on top of Result to immediately gain the ergonomic benefit in your code today.

shepmaster avatar Aug 25 '20 01:08 shepmaster

you can also create your own extension trait on top of Result to immediately gain the ergonomic benefit in your code today.

Perhaps not! Attempting your example:

fn main() -> Result<(), Error> {
    let path = "/dev/null";
    let _meta = fs::metadata(path).map_context(|e| match e.kind() {
        io::ErrorKind::NotFound => PathDoesNotExist { path },
        io::ErrorKind::PermissionDenied => PathAccessPermissionDenied { path },
        _ => GetDirectoryMetadata { path },
    })?;

    Ok(())
}

trait TrialRun<T, E> {
    fn map_context<F, C, E2>(self, context: F) -> Result<T, E2>
    where
        F: FnOnce(&E) -> C,
        C: IntoError<E2, Source = E>,
        E2: std::error::Error + snafu::ErrorCompat;
}

impl<T, E> TrialRun<T, E> for Result<T, E> {
    fn map_context<F, C, E2>(self, context: F) -> Result<T, E2>
    where
        F: FnOnce(&E) -> C,
        C: IntoError<E2, Source = E>,
        E2: std::error::Error + snafu::ErrorCompat,
    {
        self.map_err(|error| {
            let context = context(&error);
            context.into_error(error)
        })
    }
}

Quickly yields a problem:

error[E0308]: `match` arms have incompatible types
  --> src/main.rs:20:44
   |
18 |       let _meta = fs::metadata(path).map_context(|e| match e.kind() {
   |  ____________________________________________________-
19 | |         io::ErrorKind::NotFound => PathDoesNotExist { path },
   | |                                    ------------------------- this is found to be of type `PathDoesNotExist<&str>`
20 | |         io::ErrorKind::PermissionDenied => PathAccessPermissionDenied { path },
   | |                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `PathDoesNotExist`, found struct `PathAccessPermissionDenied`
21 | |         _ => GetDirectoryMetadata { path },
22 | |     })?;
   | |_____- `match` arms have incompatible types
   |
   = note: expected type `PathDoesNotExist<&str>`
            found struct `PathAccessPermissionDenied<&str>`

Had you tried this locally in some different fashion somehow?

shepmaster avatar Aug 25 '20 01:08 shepmaster

Closing due to a lack of clarity on what is being requested.

shepmaster avatar Nov 22 '20 02:11 shepmaster