#error #attributes #error-context #error-handling #debug-mode #bubble-up

oofs_derive

错误处理库,为您生成并注入上下文

13 个版本

0.2.3 2022年10月13日
0.2.2 2022年10月12日
0.1.15 2022年10月9日
0.1.2 2022年9月28日

#27 in #error-context

34 每月下载量
2 个包中使用 (通过 oofs)

MIT 许可证

105KB
2K SLoC

oofs

Crates.io MIT licensed

错误处理库,为您生成并注入上下文。

本库提供三个主要功能

  • #[oofs] 属性,为带有 ? 操作符的函数调用生成和注入上下文。
  • 为错误分类处理标记错误。
  • 附加自定义上下文。

目录

基本示例 1

以下是通过 #[oofs] 属性注入上下文的简单示例。

use oofs::{oofs, Oof};

#[oofs]
fn outer_fn() -> Result<(), Oof> {
    let x = 123;
    let y = "hello world";

    inner_fn(x, y)?;

    Ok(())
}

#[oofs]
fn inner_fn(x: usize, y: &str) -> Result<(), Oof> {
    let _ = y.parse::<usize>()?;

    Ok(())
}

运行 outer_fn() 输出

inner_fn($0, $1) failed at `oofs/tests/basic.rs:6:5`

Parameters:
    $0: usize = 123
    $1: &str = "hello world"

Caused by:
    0: y.parse() failed at `oofs/tests/basic.rs:17:13`

    1: invalid digit found in string

错误显示失败的链式方法、它们在代码中的位置、参数的类型及其调试值。这就是在将错误格式化为Debug(即{:?})时显示的内容。

由于所有注入的代码要么是常量评估(即类型名称、调用名称等),要么在遇到错误时才懒加载(每个参数的调试字符串),因此对性能的影响几乎可以忽略不计。

注意: 实际上,上面的说法只对一半是真的。让我来解释一下

只有引用或可复制的对象(例如原始数据类型,如boolusize等)的参数可以懒加载它们的调试字符串。对于不可复制的对象(例如将所有者对象传递给函数参数,如String),在调试模式下调用之前会立即加载它们的调试字符串,而在发布模式下则禁用。

对不可复制的值(字符串、自定义对象等)的默认调试行为是

  • 对于调试模式,在每个函数调用之前加载调试格式化的值,每次调用都会产生开销。
  • 对于发布模式,跳过调试不可复制的值。

您可以通过属性参数或通过启用以下功能之一来更改此默认行为:debug_non_copyable_disableddebug_non_copyable_full。更多详细信息请参阅功能

基本示例 2

现在,让我们看看一个稍微长一点的例子。下面是来自oofs/tests/basic.rs的一个示例。

该示例展示了上下文生成、标记和附加自定义上下文。

use oofs::{oofs, Oof, OofExt};

// Marker type used for tagging.
struct RetryTag;

#[oofs]
fn application() -> Result<(), Oof> {
    if let Err(e) = middlelayer("hello world") {
        // Check if any of internal errors is tagged as `RetryTag`; if so, try again.
        if e.tagged_nested::<RetryTag>() {
            println!("Retrying middlelayer!\n");

            // If the call fails again, return it.
            // Since `?` operator is used, context is generated and injected to the call.
            middlelayer("hello world")?;
        } else {
            return Err(e);
        }
    }

    Ok(())
}

#[oofs]
fn middlelayer(text: &str) -> Result<u64, Oof> {
    let my_struct = MyStruct {
        field: text.to_owned(),
    };

    // Passing an expression as arg is also fine.
    // All args are evaluated before being debugged in the error.
    // Context is generated and injected to both `?`s in this statement.
    let ret = my_struct.failing_method(get_value()?)?;

    Ok(ret)
}

fn get_value() -> Result<usize, std::io::Error> {
    Ok(123)
}

#[derive(Debug)]
struct MyStruct {
    field: String,
}

// #[oofs] can also be used to `impl` blocks.
// Context will be injected to all methods that return a `Result`.
#[oofs]
impl MyStruct {
    fn failing_method(&self, x: usize) -> Result<u64, Oof> {
        let ret = self
            .field
            .parse::<u64>()
            ._tag::<RetryTag>()                 // tags the error with the type `RetryTag`.
            ._attach(x)                         // attach anything that implements `Debug` as custom context.
            ._attach(&self.field)               // attach the receiver as attachment to debug.
            ._attach_lazy(|| "extra context")?; // lazily evaluate context; useful for something like `|| serde_json::to_string(&x)`.

        Ok(ret)
    }
}

运行application()将输出

Retrying middlelayer!

middlelayer($0) failed at `oofs/tests/basic.rs:11:13`

Parameters:
    $0: &str = "hello world"

Caused by:
    0: my_struct.failing_method($0) failed at `oofs/tests/basic.rs:26:15`

       Parameters:
           $0: usize = 123

    1: self.field.parse() failed at `oofs/tests/basic.rs:46:14`

       Attachments:
           0: 123
           1: "hello world"
           2: "extra context"

    2: invalid digit found in string

令人赏心悦目的错误不仅于此;我们还得到了带有标签的分类错误处理。

在源方法failing_method中,我们使用RetryTag类型标记parse方法。在最高级函数application中,我们调用e.tagged_nested::<RetryTag>来检查是否有任何内部调用被标记为RetryTag。当找到标签时,我们通过再次调用middlelayer来处理这种情况。

有了标记,我们就不再需要逐级检查每个错误变体。我们只需查找我们想要处理的标签,并相应地处理标记的错误。在上面的例子中,如果找到RetryTag标签,我们会再次尝试调用middlelayer

#[oofs] 属性

默认行为

该属性会做出一些默认行为选择

  1. 对于impl块,返回Result<_, _>的方法将注入上下文。

    • 通过在fn上方指定#[oofs(skip)]来覆盖行为,使该方法被跳过。
  2. 对于 impl 块,将跳过那些不返回 Result<_, _> 的方法。

    • 通过在 fn 上指定 #[oofs] 来覆盖行为,无论何时都应用注入。
  3. 闭包内部(即 || { ... })中的 ? 运算符将不会有上下文注入。

    • 通过在 fn 上指定 #[oofs(closures)] 来覆盖行为,将注入应用到闭包内部。
  4. 异步块内部(即 async { ... })中的 ? 运算符将不会有上下文注入。

    • 通过在 fn 上指定 #[oofs(async_blocks)] 来覆盖行为,将注入应用到异步块内部。
  5. 不带分号的 return ... 语句和最后一个表达式将不会有上下文注入。

可以通过属性参数来更改这些默认行为。

属性参数

可能的属性参数包括:tagattachattach_lazyclosuresasync_blocksskipdebug_skipdebug_withdebug_non_copyable

有关如何使用它们的详细信息,请参阅 文档

标记错误

如上例所示,您可以使用 _tag 为错误标记,并使用 taggedtagged_nested 检测标记。

struct MyTag;

#[oofs]
fn application_level() -> Result<(), Oof> {
    if let Err(e) = source() {
        if e.tagged_nested::<MyTag>() {
            ...handle for this tag
        } else if e.tagged_nested::<OtherTag>() {
            ...handle for this tag
        } else {
            ...
        }
    }
}

...

#[oofs]
fn source() -> Result<(), Oof> {
    some_fn()._tag::<MyTag>()?;

    Ok(())
}

这允许您将错误分类到不同的标记组中,并相应地处理它们。与在嵌套函数调用中匹配每个枚举变体相比,这提供了更好的错误处理体验。

请注意,您还可以使用多个不同的标记标记错误。

我选择类型作为标记,因为类型小、可读且唯一。 Stringusize 可能会意外地导致重复值。

附加自定义上下文

在某个时候,您可能会发现生成的上下文不足。毕竟,它只显示了失败的调用及其传递的参数。它不会捕获所有其他可能上下文信息。

您可以使用 _attach_attach_lazy 方法将您自己的上下文信息附加到错误上。

#[oofs]
fn outer_fn() -> Result<(), Oof> {
    let x = 123usize;
    let y = std::time::Instant::now();

    "hello world"
        .parse::<usize>()
        ._attach(&x)
        ._attach(&y)?;

    Ok(())
}

上面的代码将打印以下错误

$0.parse() failed at `oofs/tests/basic.rs:10:10`

Parameters:
    $0: &str = "hello world"

Attachments:
    0: 123
    1: Instant { t: 11234993365176 }

Caused by:
    invalid digit found in string

_attach 接受任何实现 std::fmt::Debug 的类型。

_attach_lazy另一方面,接受任何返回实现了ToString类型的闭包。

它可以是像&str这样的东西,例如._attach_lazy(|| "some context"),也可以是像String这样的,例如._attach_lazy(|| format!("some context {:?}", x)),或者是一些需要一些工作来显示的函数,例如._attach_lazy(|| serde_json::to_string(&x))

返回自定义错误

在某个时候,你还需要返回自定义错误。

对于这些情况,你有一些选项:oof!(...)wrap_err(_)ensure!(...)ensure_eq!(...)

  • oof!(...):这很像anyhow!eyre!;你输入到宏的方式就像对println!一样。这返回一个Oof结构体,你可以调用返回的Oof的方法,例如

    return oof!("my custom error").tag::<MyTag>().attach(&x).into_res();
    

    into_res()Oof包装成Result::Err(_)

  • wrap_err(_):一个将自定义错误包装成Oof的函数。

    return wrap_err(std::io::Error::new(std::io::ErrorKind::Other, "Some Error")).tag::<MyTag>().into_res();
    

    into_res()Oof包装成Result::Err(_)

  • ensure!(...):这类似于许多其他库,但略有不同。

    可选地,你可以输入自定义上下文消息,就像format!(...)一样。

    此外,你也可以可选地提供括号内的标签和属性。

    ensure!(false, "custom context with value {:?}", x, {
      tag: [MyTag, OtherTag],
      attach: [&y, "attachment", Instant::now()],
      attach_lazy: [|| serde_json::to_string(&y), || format!("lazy attachment {}", &z)]
    });
    
  • ensure_eq!(...):这与许多其他库类似,但有一些细微的差别。

    可选地,你可以输入自定义上下文消息,就像format!(...)一样。

    此外,你也可以可选地提供括号内的标签和属性。

    ensure_eq!(1u8, 2u8, "custom context with value {:?}", x, {
      tag: [MyTag, OtherTag],
      attach: [&y, "attachment", Instant::now()],
      attach_lazy: [|| serde_json::to_string(&y), || format!("lazy attachment {}", &z)]
    });
    

功能

  • location(默认值:true):启用打印失败代码的位置。

  • debug_non_copyable_disabled(默认值:false):禁用调试不可复制的函数参数。

    默认行为是在调试模式下每次调用之前立即加载不可复制参数的调试字符串,但在发布模式下禁用它们。

  • debug_non_copyable_full(默认值:false):即使在发布模式下,也启用立即加载不可复制参数的调试字符串。

关于库的注释/限制

关于 #[oofs] 属性

  • #[oofs]为所有包含?操作符的语句和表达式生成并注入上下文。

  • return Err(...)或最后一个表达式(无分号)不注入上下文。

  • 如果方法接收器是一个变量(例如,x.some_method()),或变量的字段(例如,x.field.some_method()),则不会显示xx.field的值。这是因为无法在宏中确定此接收器是引用、可变引用还是拥有变量。

    • 对于这些情况,您可以将变量附加如下:x.some_method()._attach(&x)以在错误中显示x的值。

关于 Oof 错误结构体

  • Oof没有实现From<E> where E: std::error::Error,因此必须通过属性宏构建。所以,如果您不包含#[oofs],它将抛出编译器错误;这是故意的,因为它会吸引用户的注意并强制他们包含属性。

  • anyhow::Erroreyre::Report不同,Oof实现了std::error::Error。这很好,因为它使其与这些boxed错误类型兼容。例如,这可以工作

    #[oofs]
    fn outer_fn() -> Result<(), anyhow::Error> {
        inner_fn()?;
        Ok(())
    }
    

    它之所以可以工作,是因为?操作符会隐式地将Oof转换为anyhow::Error

关于下划线方法如 ._tag()._attach(_)

在上面的基本示例中,您可能已经注意到所有用于oof的方法都以下划线开头;您可以称它们为“元方法”,因为它们不影响逻辑,但只影响返回的结果。

这是因为在宏中必须有一种方法来区分功能方法和元方法。这是因为宏还会试图将这些元方法作为显示方法链的一部分,例如 _attach(x) 将会在 ParametersAttachments 部分显示两次。

一开始这可能会让人感觉不舒服,对我来说也是如此。但经过尝试,我习惯了它;现在我认为我喜欢它,因为我可以轻松地区分功能方法和元方法。

我为带来的不便表示歉意,如果还有更好的方法,请告诉我。

调试不可复制参数

创建库时,一个痛点是要懒加载可复制的参数的值,并在编译时立即加载不可复制的参数的值。我找到了一种使用酷炫的Rust技巧来完成这个任务的方法。

现在,默认行为应该是始终立即加载不可复制的参数的值吗?这可能会带来不必要的性能成本,因为它会在非错误情况下加载它们。

作为折衷方案,我使它在调试模式下会立即加载不可复制的参数的值;而在发布模式下,则不会加载不可复制的参数的值。

您可以使用特性 debug_non_copyable_disableddebug_non_copyable_full 来更改此行为。

debug_non_copyable_disabled 将禁用在调试模式下加载不可复制的参数的值。 debug_non_copyable_full 将启用即使在发布模式下也加载不可复制的参数的值。

#[async_trait] 兼容性

#[async_trait] 解析并转换特质中的 async fnfn -> Box<Future<Output = Result<...>>>. 由于 #[oofs] 默认情况下只应用于返回 Result<_, _> 的方法,因此一旦应用了 #[async_trait],它将不会应用注入。

处理这个问题的有两种方法

  • #[oofs] 放置在 #[async_trait] 之上,这样 oofs 首先应用,然后才是 #[async_trait]

    #[oofs]
    #[async_trait]
    impl Trait for Struct {
      ...
    }
    
  • 在impl块中,将#[oofs(closures, async_blocks)]放置在fn ...上方,并且oofs属性将告诉宏应用注入,无论是否启用,而closuresasync_blocks将告诉宏应用注入,以启用默认禁用的闭包和异步块。

    #[async_trait]
    #[oofs]
    impl Trait for Struct {
      #[oofs(closures, async_blocks)]
      async fn do_something() -> Result<(), _> {
          ...
      }
    }
    

未来计划

这个库仍然是WIP。

我计划测试错误处理性能,优化错误内存占用,并实现类似#[oofs(tag(MyTag))]#[oofs(skip)]等的属性参数。

此外,它不会将上下文注入到闭包和异步块中。我计划添加类似#[oofs(closures)]#[oofs(async_blocks)]的属性参数,以启用将上下文注入到闭包和异步块中。

依赖项

~1.5MB
~36K SLoC