3 个版本

0.1.2 2023 年 12 月 9 日
0.1.1 2023 年 12 月 7 日
0.1.0 2023 年 12 月 6 日

#1062 in Rust 模式

每月 50 次下载
用于 3 crates

MIT/Apache

20KB
64

derive(Toss)

thiserror 创建的轻量级辅助库,可方便地处理错误。

[dependencies]
tosserror = "0.1"

编译器支持:需要 rustc 1.56+

目录


示例用法

use thiserror::Error;
use tosserror::Toss;

#[derive(Error, Toss, Debug)]
pub enum DataStoreError {
    #[error("invalid value ({value}) encountered")]
    InvalidValue {
      value: i32,
      source: std::num::TryFromIntError,
    },
    #[error("data store disconnected with msg {msg}: {status}")]
    Disconnect{
      status: u8,
      msg: String,
      source: std::io::Error
    }
}

// uses
get_value().toss_invalid_value(123)?;

// lazily provide context
data_store_fn().toss_disconnect_with(|| (123, "some msg".to_owned()))?;

与传统 map_err 的比较

get_value().map_err(|e| DataStoreError::InvalidValue {
  value: 123,
  source: e
})?;

data_store_fn().map_err(|e| DataStoreError::Disconnect {
    status: 123,
    msg: "some msg".to_owned(),
    source: e
})?;

工作原理

derive(Toss) 通过为每个枚举变体创建一个特质来工作。

对于下面的错误

#[derive(Error, Toss, Debug)]
pub enum DataStoreError {
    #[error("invalid value ({value}) encountered")]
    InvalidValue {
      value: i32,
      source: std::num::TryFromIntError,
    },
    #[error("data store disconnected with msg {msg}: {status}")]
    Disconnect {
      status: u8,
      msg: String,
      source: std::io::Error
    }
}

它将生成以下特质和实现

// pseudo generated code
trait TossDataStoreErrorInvalidValue<T> {
    fn toss_invalid_value(self, value: i32) -> Result<T, DataStoreError>;
}
impl<T> TossDataStoreErrorInvalidValue<T> for Result<T, std::num::TryFromIntError> {
    fn toss_invalid_value(self, value: i32) -> Result<T, DataStoreError> { ... }
}

trait TossDataStoreErrorDisconnect<T> {
    fn toss_disconnect(self, status: u8, msg: String) -> Result<T, DataStoreError>;
}
impl<T> TossDataStoreErrorDisconnect<T> for Result<T, std::io::Error> {
    fn toss_disconnect(self, status: u8, msg: String) -> Result<T, DataStoreError> { ... }
}

生成这些特质后,您将获得自动完成 .toss_invalid_value(i32) 的权限,以任何返回 Result<T, TryFromError> 的方法。同样适用于 .toss_disconnect(u8, String),对于 Result<T, io::Error>

如果您想使这些特质对其他模块可见,请参阅 #[visibility]

有关完整生成的代码示例,请参阅从 derive(Toss) 生成的代码

为什么使用 derive(Toss)

简洁性

当我使用类似 thiserror 的库时,我发现错误处理会增加代码的冗余,并阻碍可读性。

你可能遇到如下简单函数调用后跟随 2 到 4 行的错误处理:

simple_function().map_err(|e| LongError::InvalidValue {
    context1: 123u32,
    context2: "some context".to_owned(),
    source: e
})?;

当错误处理比程序本身更长时,你的代码的主要焦点不再是逻辑;而是错误处理。

使用 derive(Toss),你的错误处理代码几乎总是保持在单行。

simple_function().toss_invalid_value(123u32, "some context".to_owned())?;

你也可以传递一个返回参数的闭包来懒加载上下文值。

simple_function().toss_invalid_value_with(|| (123u32, "some context".to_owned()))?;

通过自动完成提高便利性

使用 thiserror,可能需要输入所有字符,比较繁琐。

  1. .map_err
  2. 两个 | 操作符
  3. 枚举名称
  4. 变体名称
  5. 字段名称和值

我不会说这些是编程的主要障碍,但当你不得不一次次地输入这些内容时,确实会感到非常烦恼。

使用 derive(Toss),你会获得自动补全功能!

当你输入 .toss_ 时,你将得到只适用于底层错误类型的建议列表。

例如,如果你的方法返回 io::Error,建议将仅显示接受 io::Error 的处理方法。

为什么你可能不使用 derive(Toss)

虽然有利,但我承认也存在一些缺点,这可能让你不想使用这个库。

它会使代码难以理解

在我Reddit 帖子中询问对此想法的反馈,最常见的反馈是:

此宏创建了许多你无法看到其定义或实现的特质。如果你是某个开源库的新贡献者,并且在这些方法中遇到了使用,如果你对 tosserror 一无所知,你可能会非常困惑其含义、作用和工作方式。

与此相比,.map_err.with_context 提供了清晰的源代码,你可以轻松地看到其作用和工作方式。

这是一个合理的批评,如果这是一个大问题,你可能不想使用这个库。

对于许多人来说,使用 .map_err 并不是足够大的不便,不足以证明掩盖代码库是合理的。

对于那些仍然想尝试的人,我对这个担忧有以下看法

  1. 如果你使用 rust-analyzer lsp,它将非常清楚地显示定义、参数和返回类型
  2. 库足够简单,新用户可以很容易地了解其功能,或者你可以简单地告诉他们这个库存在。

属性

thiserror 的属性:#[source]#[from]#[backtrace]

库使用与 thiserror 相同的规则来确定源和回溯字段。

如果你通过字段名称或属性定义了源或回溯字段,它将被排除在生成方法的参数之外。

#[derive(Error, Toss, Debug)]
#[error("my error with value {value}")]
pub struct MyError {
  value: i32,
  source: io::Error,
  backtrace: std::backtrace::Backtrace
}

// pseudo generated code
trait TossMyError<T> {
    fn toss_invalid_value(self, value: i32) -> Result<T, MyError>;
}
impl<T> TossMyError<T> for io::Error {
    fn toss_invalid_value(self, value: i32) -> Result<T, MyError> {
      ...
    }
}

#[可见性]

默认情况下,生成的特质是私有的,只能在其创建的模块中可见。

使用 #[visibility],您可以暴露生成的特质给其他模块或公开。

您可以将属性放在枚举上方以应用于为错误生成的所有特质,或者将其放在特定的变体上方以应用于特定变体的生成特质。

示例

#[derive(Error, Toss, Debug)]
#[error("...")]
#[visibility(pub)]
pub enum Error1 {
  Var1 { ... }, // generates trait `pub trait TossError1Var1`
  Var2 { ... }  // generates trait `pub trait TossError1Var2`
}

#[derive(Error, Toss, Debug)]
#[error("...")]
#[visibility(pub(super))]
pub enum Error2 {
  Var1 { ... }, // generates trait `pub(super) trait TossError2Var1`
  #[visibility(pub(crate))]
  Var2 { ... }  // generates trait `pub(crate) trait TossError2Var2`
}

提示:如何使用错误跨模块/项目范围

许多库倾向于在根模块中定义一个错误,并在库中的所有模块中使用它。

这可能会很麻烦,因为不同模块中特质的自动完成可能无法正常工作。这意味着您可能需要手动导入这些特质,即使您甚至不知道它们的模样,或者从该模块中导入所有(use *)。

在这种情况下,我通常这样做:

  1. 创建一个新的模块 error 并将错误定义放入其中。

    pub(crate) mod error {
      #[derive(Error, Toss, Debug)]
      #[visibility(pub(crate))]
      pub enum MyError {
        ...
      }
    }
    

    确保指定可见性属性为 pub(crate) 或所需的任何内容。

  2. 每次使用错误处理时,只需导入错误模块。

    use crate::error::*;
    

    这样,您就可以干净地导入所有生成的特质以及您的错误。

#[前缀]

可能会有多个错误在模块中,并且变体名称冲突的情况。

在这种情况下,编译器会抱怨不明确的方程序名。

使用 #[prefix],您可以为生成的特质方法添加文本前缀。

只需将 #[prefix] 放在蛇形枚举名称作为前缀值的前面,或者将 #[prefix(custom_prefix)] 放入以编写您自己的前缀值。

#[derive(Error, Toss, Debug)]
#[error("...")]
#[prefix] // apply prefix "connect" (enum name without `_error`) to all variants
pub enum ConnectError {
  Var1 { ... }, // generates trait method `fn toss_connect_var1(self)`
  Var2 { ... }  // generates trait method `fn toss_connect_var2(self)`
}

#[derive(Error, Toss, Debug)]
#[error("...")]
#[prefix(custom)] // apply prefix "custom" to all variants
pub enum AnotherError {
  Var1 { ... }, // generates trait method `fn toss_custom_var1(self)`
  #[prefix(specific)] // apply prefix "specific" just to this variant
  Var2 { ... }  // generates trait method `fn toss_specific_var2(self)`
}

功能

thiserror

[dependencies]
tosserror = { version = "0.1", features = ["thiserror"] }
# thiserror = "1.0" # no longer necessary

启用功能 thiserror 重新导出 thiserror::Error,这样您就不必同时依赖于 tosserrorthiserror

现在您只需使用 tosserror::Error 来派生您的错误。

use tosserror::{Error, Toss};

#[derive(Error, Toss, Debug)]
pub enum DataStoreError {
    #[error("invalid value ({value}) encountered")]
    InvalidValue {
      value: i32,
      source: std::num::TryFromIntError,
    },
}

这应该是一个默认功能吗?请通过在 将 "thiserror" 设置为默认功能 PR 上点赞来告诉我。

限制

这通过在 derive(Toss) 的生成代码中重新导出 thiserror 来工作。

因此,derive(tosserror::Error) 仅在与其他 derive(Toss) 一起使用时才有效。

derive(Toss) 生成的代码

示例错误

use thiserror::Error;
use tosserror::Toss;

#[derive(Error, Toss, Debug)]
pub enum DataStoreError {
    #[error("invalid value ({value}) encountered")]
    InvalidValue {
      value: i32,
      source: std::num::TryFromIntError,
    },
    #[error("data store disconnected with msg {msg}: {status}")]
    #[visibility(pub(crate))]
    Disconnect{
      status: u8,
      msg: String,
      source: std::io::Error
    }
}

生成代码

trait TossDataStoreErrorInvalidValue<__RETURN> {
    fn toss_invalid_value(self, value: i32) -> Result<__RETURN, DataStoreError>;
    fn toss_invalid_value_with<F: FnOnce() -> (i32)>(
        self,
        f: F,
    ) -> Result<__RETURN, DataStoreError>;
}
impl<__RETURN> TossDataStoreErrorInvalidValue<__RETURN>
for Result<__RETURN, std::num::TryFromIntError> {
    fn toss_invalid_value(self, value: i32) -> Result<__RETURN, DataStoreError> {
        self.map_err(|e| {
            DataStoreError::InvalidValue {
                source: e,
                value,
            }
        })
    }
    fn toss_invalid_value_with<F: FnOnce() -> (i32)>(
        self,
        f: F,
    ) -> Result<__RETURN, DataStoreError> {
        self.map_err(|e| {
            let (value) = f();
            DataStoreError::InvalidValue {
                source: e,
                value,
            }
        })
    }
}

pub(crate) trait TossDataStoreErrorDisconnect<__RETURN> {
    fn toss_disconnect(
        self,
        status: u8,
        msg: String,
    ) -> Result<__RETURN, DataStoreError>;
    fn toss_disconnect_with<F: FnOnce() -> (u8, String)>(
        self,
        f: F,
    ) -> Result<__RETURN, DataStoreError>;
}
impl<__RETURN> TossDataStoreErrorDisconnect<__RETURN>
for Result<__RETURN, std::io::Error> {
    fn toss_disconnect(
        self,
        status: u8,
        msg: String,
    ) -> Result<__RETURN, DataStoreError> {
        self.map_err(|e| {
            DataStoreError::Disconnect {
                source: e,
                status,
                msg,
            }
        })
    }
    fn toss_disconnect_with<F: FnOnce() -> (u8, String)>(
        self,
        f: F,
    ) -> Result<__RETURN, DataStoreError> {
        self.map_err(|e| {
            let (status, msg) = f();
            DataStoreError::Disconnect {
                source: e,
                status,
                msg,
            }
        })
    }
}

致谢

这个库从 thiserror 中汲取了许多实现细节和代码结构,因为它是 thiserror 的互补库,并基于相同的属性。

特别感谢 thiserror 的创建者和维护者创建了这样一个简单、干净、易于阅读但很棒的库。

依赖项

~240–690KB
~16K SLoC