#error #error-message #bug #concise #down #downcast #track

witcher

使用简洁的错误处理跟踪和记录错误

11 个版本

0.1.19 2020 年 12 月 31 日
0.1.12 2020 年 12 月 30 日
0.0.2 2020 年 11 月 29 日

#359调试


3 个包中使用

MIT/Apache

48KB
715

witcher

license-badge build codecov crates.io Minimum rustc

使用简洁的错误处理跟踪和记录错误

通过使用 unwrapexpect 变体,或者费力地编写自定义枚举包装器,或者必须与杂乱的 Box 类型一起工作,来避免代码中随机终止执行,并通过 Result<T> 从 witcher 中作为返回类型,轻松包装错误并自动向上传播堆栈中的额外上下文消息。witcher 实现了 std::error::Error 保留向下转换和链式操作。最好的是,witcher 提供了圣杯:自动简化的回溯

Display debug

你将获得

  1. 简化的错误处理

    通过提供错误上的类型匹配
    通过自动处理转换
    通过提供简洁和简明的用户交互
    通过提供条件彩色输出

  2. 能够讲述完整故事的错误处理

    通过实现 std::error::Error
    通过将错误链接在一起
    通过提供上下文消息
    通过提供从源头跟踪

  3. 安全性

    100% 安全的代码,没有任何 unsafe 使用
    零低级 TraitObject 操作
    测试覆盖率超过 90%,经过良好测试


宣言

来自Go语言的背景,我原本完全预期可以像Go语言的pkg/errors那样直接导入Rust的默认错误处理包,然后就可以大步前行。然而,随着我深入挖掘,我发现了一个丰富的、层层叠叠的人类学历史,有成千上万的项目和作者都在宣称他们高尚的理想和原则,他们都在试图解决同一个问题。Rust的错误处理功能还不够完善。这让人想起了Go语言在存在/errors之前的样子。我发现了一些明显比其他项目更常用的项目,并看到了一些曾经流行的包的势头转变。然而,经过数周的研究和测试了无数不同的模式和包之后,我仍然没有找到像那个值得尊敬的pkg/errors一样简单易用的东西。因此,witcher应运而生。

顺便提一下,当我深入探索时,我将所有关于低级别TraitObject操作的研究都转移到了phR0ze/rust-examples,我很高兴地说witcher是100%安全的代码。

使用方法

使用wrap扩展方法在Result类型上包装错误,并附加额外的上下文信息,自动将错误连在一起。由于wrap返回一个Result<T>,所以符号更少,输入也更少。

需要rustc >= 1.30

这个最低rustc要求是由对Rust的std::error::Error处理改进所做的增强所推动的。

  1. 在您的Cargo.toml中导入witcher并保留调试符号
    [dependencies]
    witcher = "0.1"
    
    [profile.release]
    debug = true
    
  2. 使用witcher预览
    use witcher::prelude::*;
    
  3. 使用Result别名作为返回类型
    fn do_something() -> Result<()>;
    
  4. 使用wrap扩展方法在Result上提供上下文
    fn do_something() -> Result<()> {
        do_external_thing().wrap("Failed to slay beast")
    }
    fn do_external_thing() -> std::io::Result<()> {
        Err(std::io::Error::new(std::io::ErrorKind::Other, "Oh no, we missed!"))?
    }
    

颜色

颜色自动由gory根据tty检测控制。您可以通过将环境变量TERM_COLOR设置为false值来手动禁用颜色,请参阅gory文档关于控制使用

$ TERM_COLOR=0 cargo run -q --example simple

降级

我们可以使用降级或使用match_err!宏来对错误类型进行匹配。

downcast_ref - 访问std::error::Error的downcast_ref

use witcher::prelude::*;

// Wrap our internal error with additional context as we move up the stack
fn do_something() -> Result<()> {
    do_external_thing().wrap("Failed to slay beast")
}

// Function that returns an external error type outside our codebase
fn do_external_thing() -> std::io::Result<()> {
    Err(std::io::Error::new(std::io::ErrorKind::Other, "Oh no, we missed!"))?
}

fn main() {
    let err = do_something().unwrap_err();

    // Get the last error in the error chain which will be the root cause
    let root_cause = err.last();

    // Match single concrete error type
    if let Some(e) = root_cause.downcast_ref::<std::io::Error>() {
        println!("Root cause is a std::io::Error: {}", e)
    } else {
        println!("{}", err)
    }
}

结果

$ cargo run -q --example downcast_ref
Root cause is std::io::Error: Oh no, we missed!

match_err! - 匹配具体错误类型

use witcher::prelude::*;

fn do_something() -> Result<()> {
    do_external_thing().wrap("Failed to slay beast")
}

fn do_external_thing() -> std::io::Result<()> {
    Err(std::io::Error::new(std::io::ErrorKind::Other, "Oh no, we missed!"))?
}

fn main() {
    let err = do_something().unwrap_err();

    // Match multiple downcasted cases to handle errors differently
    match_err!(err.last(), {
        x: Error => println!("Root cause is witcher::Error: {}", x),
        x: std::io::Error => println!("Root cause is std::io::Error: {}", x),
        _ => println!("{}", err)
    });
}

结果

$ cargo run -q --example downcast_match
Root cause is std::io::Error: Oh no, we missed!

pass - 透明地传递错误

在某些情况下,可能希望能够使用Witcher常见的错误模式,但将特定的顶级错误作为传递。

警告:这不会与使用类型直接匹配而不是间接匹配Error类型的match_err!宏一起工作。

use witcher::prelude::*;

fn do_something() -> Result<()> {
    do_external_thing().pass()
}

fn do_external_thing() -> std::io::Result<()> {
    Err(std::io::Error::new(std::io::ErrorKind::Other, "Oh no, we missed!"))?
}

fn main() {
    let err = do_something().unwrap_err();

    // Since we used `pass` we can match on the error directly
    match err.downcast_ref::<std::io::Error>() {
        Some(err) => println!("Root cause is std::io::Error: {}", err),
        None => println!("Root cause is witcher::Error: {}", err),
    }
}

结果

$ cargo run -q --example pass
Root cause is std::io::Error: Oh no, we missed!

链式

我们可以继续利用std::error::Error的source方法来链式错误。第一个包装的错误将保留其具体类型,但链中的后续错误已丢失该信息。

source - std::error::Error的source方法被公开

use witcher::prelude::*;
#[derive(Debug)]
struct SuperError {
    side: SuperErrorSideKick,
}
impl std::fmt::Display for SuperError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "SuperError is here!")
    }
}
impl std::error::Error for SuperError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.side)
    }
}

#[derive(Debug)]
struct SuperErrorSideKick;
impl std::fmt::Display for SuperErrorSideKick {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "SuperErrorSideKick is here!")
    }
}
impl std::error::Error for SuperErrorSideKick {}

fn do_something() -> Result<()> {
    do_external_thing().wrap("Failed doing super hero work")
}

fn do_external_thing() -> std::result::Result<(), SuperError> {
    Err(SuperError {side: SuperErrorSideKick})
}

fn main() {
    if let Err(err) = do_something() {

        // Traverse the error chain
        let mut source = Some(err.std());
        while let Some(err) = source {
            match_err!(err, {
                // Using alternate form of display for `Error` to get just the message
                x: Error => println!("Found witcher::Error: {:#}", x),
                x: SuperError => println!("Found SuperError: {}", x),
                x: SuperErrorSideKick => println!("Found SuperErrorSideKick: {}", x),
                _ => println!("unknown")
            });
            source = err.source();
        }
    }
}

结果

$ cargo run -q --example chain
Found witcher::Error: Failed doing super hero work
Found SuperError: SuperError is here!
Found SuperErrorSideKick: SuperErrorSideKick is here!

重试

我们可以使用几个不同的Result扩展函数来重试失败的代码。

err_is - 如果存在错误并且是给定类型,将返回true

use witcher::prelude::*;

fn retry_on_concreate_error_type_using_err_is() -> Result<()> {
    let mut retries = 0;
    let mut result = do_external_thing();
    while retries < 3 && result.err_is::<std::io::Error>() {
        retries += 1;
        println!("retrying using err_is #{}", retries);
        result = do_external_thing();
    }
    result.wrap("Failed while attacking beast")
}
fn do_external_thing() -> std::io::Result<()> {
    Err(std::io::Error::new(std::io::ErrorKind::Other, "Oh no, we missed!"))
}

fn main() {
    println!("{}", retry_on_concreate_error_type_using_err_is().unwrap_err());
}

结果

cargo run -q --example retry_err_is
retrying using err_is #1
retrying using err_is #2
retrying using err_is #3
 error: witcher::Error: Failed while attacking beast
 cause: std::io::error::Error: Oh no, we missed!
symbol: retry_err_is::retry_on_concreate_error_type_using_err_is
    at: examples/retry_err_is.rs:11:5
symbol: retry_err_is::main
    at: examples/retry_err_is.rs:18:22

retry_on - 是一种更简洁的以类似方式执行的方式,如我们的err_is示例

use witcher::prelude::*;

fn retry_on_concreate_error_type() -> Result<()> {
    do_external_thing().retry_on(3, TypeId::of::<std::io::Error>(), |i| {
        println!("std::io::Error: retrying! #{}", i);
        do_external_thing()
    }).wrap("Failed while attacking beast")
}
fn do_external_thing() -> std::io::Result<()> {
    Err(std::io::Error::new(std::io::ErrorKind::Other, "Oh no, we missed!"))
}

fn main() {
    println!("{}", retry_on_concreate_error_type().unwrap_err());
}

结果

cargo run -q --example retry_on
std::io::Error: retrying! #1
std::io::Error: retrying! #2
std::io::Error: retrying! #3
 error: witcher::Error: Failed while attacking beast
 cause: std::io::error::Error: Oh no, we missed!
symbol: retry_on::retry_on_concreate_error_type
    at: examples/retry_on.rs:4:5
symbol: retry_on::main
    at: examples/retry_on.rs:16:22

retry - retry_on类似,但不考虑错误的类型

use witcher::prelude::*;

fn retry() -> Result<()> {
    do_external_thing().retry(3, |i| {
        println!("std::io::Error: retrying! #{}", i);
        do_external_thing()
    }).wrap("Failed while attacking beast")
}
fn do_external_thing() -> std::io::Result<()> {
    Err(std::io::Error::new(std::io::ErrorKind::Other, "Oh no, we missed!"))
}

fn main() {
    println!("{}", retry().unwrap_err());
}

结果

cargo run -q --example retry
std::io::Error: retrying! #1
std::io::Error: retrying! #2
std::io::Error: retrying! #3
 error: witcher::Error: Failed while attacking beast
 cause: std::io::error::Error: Oh no, we missed!
symbol: retry::retry
    at: examples/retry.rs:4:5
symbol: retry::main
    at: examples/retry.rs:16:22

显示

Witcher的Error类型为每个Display格式选项实现不同的功能。它们遵循Witcher的详细程度,从最少信息到最多信息,即{} {:#} {:?} {:#?}

Normal: {} - 将只输出第一条错误消息

Display normal

Alternate: {:#} - 将输出链中的所有错误消息

Display alternate

Debug: {:?} - 将输出所有错误消息,并带有简化的回溯

Display debug

Alternate Debug: {:#?} - 将输出所有错误消息,并带有简化的回溯

Display alternate debug

贡献

欢迎提交拉取请求。然而,请理解,它们将纯粹根据这些更改是否符合我的项目目标/理念进行评估。

Git钩子

启用git钩子以实现自动版本增量

cd ~/Projects/witcher
git config core.hooksPath .githooks

许可证

该项目许可协议为以下之一

贡献

除非您明确说明,否则您有意提交给本项目并按Apache-2.0许可证定义的贡献,将按上述方式双重许可,不附加任何额外条款或条件。


待办事项

  • 添加 Rust 文档注释
  • 转换为 JSON 的机制可能使用以下代码:{:#?}

更新日志

  • 12/30/2020
    • 修正了所需的最低 rustc 徽章

依赖项

~2.4–3.5MB
~72K SLoC