#error-context #error-message #std #path #wrap

anyhow-std

Wrap std APIs with anyhow error context

5 releases

0.1.4 Jun 3, 2023
0.1.3 Apr 27, 2023
0.1.2 Apr 15, 2023
0.1.1 Apr 8, 2023
0.1.0 Apr 8, 2023

#17 in #wrap

Download history 30/week @ 2024-07-22 16/week @ 2024-07-29 32/week @ 2024-08-05 20/week @ 2024-08-12 14/week @ 2024-08-19 24/week @ 2024-08-26 15/week @ 2024-09-02 15/week @ 2024-09-09 21/week @ 2024-09-16 61/week @ 2024-09-23 31/week @ 2024-09-30 28/week @ 2024-10-07 43/week @ 2024-10-14 12/week @ 2024-10-21 10/week @ 2024-10-28 24/week @ 2024-11-04

92 downloads per month
Used in 4 crates (2 directly)

MIT license

44KB
1K SLoC

anyhow-std

This crate wraps certain [std] functionality to produce anyhow::Results providing better error messages w/ context.

Example: Expecting a UTF8 Path Extension

Suppose we are processing a user-provided path and we expect it to have an extension that is valid UTF8:

use std::path::Path;
use anyhow_std::{PathAnyhow, OsStrAnyhow};

fn process_user_path(path: &Path) -> anyhow::Result<()> {
    let extos = path.extension_anyhow()?;
    let ext = extos.to_str_anyhow()?;
    process_user_path_with_extension(path, ext)?;
    Ok(())
}

fn process_user_path_with_extension(path: &Path, extension: &str) -> anyhow::Result<()> {
    todo!("implement path processing of {path:?} for the extension {extension:?}")
}

/*
Now if the user provides a path without an extension, they'll get an
error message with more helpful context:
*/

let res = process_user_path(Path::new("/tmp/no-extension"));
assert!(res.is_err());

let error_message = format!("{:#}", res.err().unwrap());
assert_eq!(
    error_message,
    r#"while processing path "/tmp/no-extension": missing expected extension"#
);

/*
Unix systems can have non-UTF8 paths:
*/

#[cfg(target_family = "unix")]
fn demonstrate_non_utf8_extension() {
    use std::ffi::OsStr;
    use std::os::unix::ffi::OsStrExt;

    let res = process_user_path(Path::new(OsStr::from_bytes(b"/tmp/non-unicode-extension.wacky-\xf3-extension")));
    assert!(res.is_err());

    let error_message = format!("{:#}", res.err().unwrap());
    assert_eq!(
        error_message,
        r#"while processing os string "wacky-�-extension": not valid utf8"#,
    );
}

#[cfg(target_family = "unix")]
demonstrate_non_utf8_extension();

Extension Traits

A common pattern is used to extend [std] types:

  • An extension trait is provided named <std type>Anyhow, for example: [OsStrAnyhow]
  • An impl is provided for the target type, for example impl OsStrAnyhow for OsStr {}
  • For a subset of methods of the target type, extension trait methods are provided named <method name>_anyhow. These methods always return anyhow::Result types, for example: OsStrAnyhow::to_str_anyhow.
  • Additionally, some …_anyhow methods may be provided to wrap functionality not found directly on the target type. For example Path::read_to_string does not exist, but would be a straightforward wrapper for std::fs::read_to_string, so this crate provides PathAnyhow::read_to_string_anyhow

By consistently appending _anyhow to wrapped methods, callers can unambiguously choose when to use these methods versus the [std] methods.

…_anyhow Methods

These methods convert Option<T> or Result<T, E> return types of the [std] method into anyhow::Result<T>, where the anyhow::Error has context added specific to the [std] type.

For [std] methods that return Option<T>, often a None return type isn't necessarily an "error" per se, but the …_anyhow methods result in an error in this case. So these methods should only be used when code expects Some results, and if your code should handle None as a "non-error", it can simply use the [std] method.

Wrapper types

In some cases, it is necessary to use a "wrapper type" pattern rather than an extension trait, primarily to track extra data used in error contexts. For example the crate::fs wrapper types ReadDir, DirEntry, and Metadata each own a PathBuf in addition to the underlying std::fs type in order to provide paths in error contexts, in order to provide helpful error context for common directory and fs traversal uses.

Wrapper types override some underlying [std] type methods, rather than using the …_anyhow naming convention. They also provide a std::ops::Deref impl for the underlying [std] type, so all non-overridden methods can be called naturally and overridden methods can be called by explicitly using wrapper.deref().target_method().

Types using this wrapper-type pattern consistently provide [From] / [Into] implementations where possible for the underlying [std] types plus any error context data. For example, for crate::fs::Metadata provides [From] / [Into] impls for (std::fs::Metadata, std::fs::PathBuf), the latter providing error context. There is an exception to this for Child because it mutates the underlying std::process::Child upon construction.

Finally, the Output type is an exception to this wrapper pattern because it provides no methods and exposes all contents as pub fields.

API Coverage

This crate only wraps a small subset of [std] based on what the author needs in other projects. If you'd like to see more [std] APIs wrapped, patches are welcome. ;-)

The 0.1.x version series will add APIs as they are useful and may change error context strings. The semantics of wrapped functions should not vary much, but might.

Dependencies

~0.4–1MB
~20K SLoC