#控制 #错误 #字段 #err #per #derive #field-level

err-per-field

对字段级错误处理的更细致控制

1 个不稳定版本

0.1.0 2022 年 1 月 14 日

#1761Rust 模式

MIT/Apache

9KB

err-per-field

此 crate 提供了对字段级错误处理的更细致控制。

当我们编写一个大型复杂项目时,我们可能将大量信息汇集到一个单独的结构体中,例如

struct GatheredInformation {
    info1: Info1,
    info2: Info2,
    info3: Info3,
}

fn generate_info1() -> Result<Info1, Error1> { /* ... */ }
fn generate_info2() -> Option<Info2> { /* ... */ }
fn generate_info3() -> Info3 { /* ... */ }

然而,在生成这样的子信息时,我们可能会遇到 ResultOption。核心逻辑是,如果所有子信息都成功检索到,就处理 GatheredInformation。如果某些字段无法检索,比如说 info2,我们应该通知用户失败,并可能提供其他成功检索到的信息(如 info1info3)给用户。

为了优雅地处理这个问题,动态类型语言如 JavaScript 可以相当直接

const gathered_information = gather_information();
if (gathered_information.info1 && gathered_information.info2) {
    deal_with_gathered_information(gathered_information);
} else {
    deal_with_error();
}

对于 Rust 这样的静态类型语言,事情变得复杂

struct GatheredInformationWrapper {
    info1: Result<Info1, Error1>,
    info2: Option<Info2>,
    info3: Info3,
}

let gathered_information_wrapper = gather_information();
if let GatheredInformationWrapper {
    info1: Ok(info1),
    info2: Some(info2),
    info3,
} = gathered_information_wrappper {
    let gathered_information = GatheredInformation {
        info1,
        info2,
        info3,
    };
    deal_with_gathered_information(gathered_information);
} else {
    deal_with_error();
}

我们必须

  1. 定义一个 GatheredInformation 的包装结构体,其字段根据生成函数是 ResultOption
  2. 在验证检查后构建实际的 GatheredInformation
  3. 将实际汇集的信息传递给下游函数。

这是一个无聊的过程,额外的包装结构体非常分散注意力。

为了解决这个问题,此 crate 提供了一种相当简单和优雅的方法。

示例

use err_per_field::{ErrPerField, Wrapper};

#[derive(Debug)]
struct AnError;

#[derive(ErrPerField)]
struct Foo {
    pub bar1: u8,
    #[err_per_field(maybe_none)]
    pub bar2: u16,
    #[err_per_field(maybe_error = "AnError")]
    pub bar3: u32,
    pub bar4: u64,
}

fn baz1() -> u8 { 0 }
fn baz2() -> Option<u16> { None }
fn baz3() -> Result<u32, AnError> { Err(AnError) }
fn baz4() -> u64 { 0 }

fn generate_foo() -> Wrapper<Foo> {
    let bar1 = baz1();
    let bar2 = baz2();
    let bar3 = baz3();
    let bar4 = baz4();
    Wrapper::<Foo> {
        bar1,
        bar2,
        bar3,
        bar4,
    }
}

// If we write this in another file, we can simply import `Wrapper` type
// from `err_per_field` crate.
let result: Result<Foo, Wrapper<Foo>> = generate_foo().try_into();
assert!(result.is_err());
match result {
    Ok(foo) => {
        // `foo` is of type `Foo`, and you can directly use it without any worries
    },
    Err(foo_wrapper) => {
        // `foo_wrapper` has the same fields as `foo`, but in different types,
        // such as `foo_wrapper.bar2` is of type `Option<u16>` and
        // `foo_wrapper.bar3` is of type `Result<u32, AnError>`
    }
}

使用此 crate,我们不需要自己编写包装结构体,我们可以专注于产品的核心逻辑。我们只需要做的是

  1. 使用宏 ErrPerField 对核心结构体 Foo 进行派生,并用属性标记可能为 ResultOption 的字段;
  2. 在生成结构体 Foo 时,使用 Wrapper<Foo> 作为返回类型。这个包装结构体具有与 Foo 相同的字段名,其类型可能是我们标记声明时的 ResultOption
  3. 当使用生成的结构体时,我们可以通过调用 try_into 来验证其字段,如果它是有效的,则结果是 Ok,内部类型是 Foo,我们可以直接处理它作为核心逻辑;如果它无效,则结果是 Err,内部类型是 Foo 的包装器,我们可以检查每个字段以进行错误处理。

用法

当我们对结构体 Foo 使用 #[derive(ErrPerField)] 时,宏生成一个包装结构体,我们可以使用 Wrapper<Foo> 来访问它。这个包装结构体具有与 Foo 相同的字段名,对于一个字段 bar,如果 foo.bar 的类型是 Bar,那么 foo_wrapper.bar 的类型是

  • Result<Bar, AnError> 如果存在字段级属性 #[err_per_field(maybe_error = "AnError")]
  • Option<Bar> 如果存在字段级属性 #[err_per_field(maybe_none)]
  • 否则为 Bar

Foo 将自动实现 TryFrom<Wrapper<Foo>>。如果有字段是 ErrNone,转换失败,结果是 Err,内部值是未修改的 foo 包装器;否则转换成功,结果是 Ok,内部类型是最终 foo,所有字段都从 ResultOption 中提取。

依赖项

~1.5MB
~36K SLoC