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
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
,可能需要输入所有字符,比较繁琐。
.map_err
- 两个
|
操作符 - 枚举名称
- 变体名称
- 字段名称和值
我不会说这些是编程的主要障碍,但当你不得不一次次地输入这些内容时,确实会感到非常烦恼。
使用 derive(Toss)
,你会获得自动补全功能!
当你输入 .toss_
时,你将得到只适用于底层错误类型的建议列表。
例如,如果你的方法返回 io::Error
,建议将仅显示接受 io::Error
的处理方法。
为什么你可能不使用 derive(Toss)
虽然有利,但我承认也存在一些缺点,这可能让你不想使用这个库。
它会使代码难以理解
在我Reddit 帖子中询问对此想法的反馈,最常见的反馈是:
此宏创建了许多你无法看到其定义或实现的特质。如果你是某个开源库的新贡献者,并且在这些方法中遇到了使用,如果你对 tosserror
一无所知,你可能会非常困惑其含义、作用和工作方式。
与此相比,.map_err
和 .with_context
提供了清晰的源代码,你可以轻松地看到其作用和工作方式。
这是一个合理的批评,如果这是一个大问题,你可能不想使用这个库。
对于许多人来说,使用 .map_err
并不是足够大的不便,不足以证明掩盖代码库是合理的。
对于那些仍然想尝试的人,我对这个担忧有以下看法
- 如果你使用 rust-analyzer lsp,它将非常清楚地显示定义、参数和返回类型
- 库足够简单,新用户可以很容易地了解其功能,或者你可以简单地告诉他们这个库存在。
属性
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 *
)。
在这种情况下,我通常这样做:
-
创建一个新的模块
error
并将错误定义放入其中。pub(crate) mod error { #[derive(Error, Toss, Debug)] #[visibility(pub(crate))] pub enum MyError { ... } }
确保指定可见性属性为
pub(crate)
或所需的任何内容。 -
每次使用错误处理时,只需导入错误模块。
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
,这样您就不必同时依赖于 tosserror
和 thiserror
。
现在您只需使用 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