24 次发布

0.5.2 2024 年 1 月 28 日
0.5.1 2023 年 11 月 15 日
0.5.0 2022 年 11 月 24 日
0.4.0 2022 年 7 月 12 日
0.1.3 2019 年 11 月 28 日

#2 in #complex

Download history 13/week @ 2024-04-22 259/week @ 2024-04-29 87/week @ 2024-05-06 35/week @ 2024-05-13 54/week @ 2024-05-20 27/week @ 2024-05-27 34/week @ 2024-06-03 58/week @ 2024-06-10 27/week @ 2024-06-17 32/week @ 2024-06-24 4/week @ 2024-07-01 8/week @ 2024-07-08 107/week @ 2024-07-15 178/week @ 2024-07-22 64/week @ 2024-07-29 84/week @ 2024-08-05

433 每月下载量
用于 2 crates

MPL-2.0 许可证

40KB
654

semval

Crates.io Docs.rs Deps.rs Security audit Continuous integration License: MPL 2.0

一个轻量级且无偏见的库,用于在 Rust 中进行语义验证,具有最小的依赖性。

没有任何宏魔法,至少现在还没有。

TL;DR 如果你需要在运行时验证复杂的数据结构,那么这个 crate 可能可以帮助你用语义验证来丰富你的领域模型。

动机

如何递归地验证复杂的数据结构,收集所有违规行为,并最终报告或评估这些发现?在将外部数据馈送到进一步的处理阶段之前,在运行时验证外部数据对于避免不一致性至关重要,更不用说防止物理损害。

示例

用例

假设你正在创建一个用于管理餐厅 预订 的网络服务。客户可以预订特定的开始时间和客人数。作为 联系方式,他们需要留下他们的 电话号码电子邮件地址,至少需要提供其中之一。

创建新预订的 JSON 请求体可能如下所示

{
  "start": "2019-07-30T18:00:00Z",
  "number_of_guests": 4,
  "customer": {
    "name": "slowtec GmbH",
    "contact_data": {
      "phone": "+49 711 500 716 72",
      "email": "[email protected]"
    }
  }
}

领域模型

让我们专注于联系方式。在 Rust 中对应的类型安全数据模型可能看起来像这样

struct PhoneNumber(String);

struct EmailAddress(String);

struct ContactData {
  pub phone: Option<PhoneNumber>,
  pub email: Option<EmailAddress>,
}

在这个例子中,电话号码和电子邮件地址仍然由字符串表示,但被包裹在只有一个成员的元组结构体中。这种常用的 元组结构体 模式在编译时建立类型安全,并使我们能够为这些类型添加 行为

业务规则

我们的预订业务要求只有在以下所有条件都满足时,才能接受联系方式实体

  • 电子邮件地址有效
  • 电话号码有效
  • 电子邮件地址、电话号码或两者都存在

验证

让我们为 预订 用例开发一个软件设计。它应该能够根据我们的业务要求验证领域实体。

我们将仅关注 联系方式 实体以简化问题。这足以推断基本原理。完整的代码可以在仓库中提供的示例文件 reservation.rs 中找到。

无效性

验证可能有哪些结果?如果验证成功,我们就完成了,处理将继续,就像什么都没发生一样,也就是说,验证通常是一个幂等操作。如果验证失败,我们希望以某种方式理解它为什么失败,以解决冲突或修复不一致。最后,我们可能需要将任何未解决的问题反馈给调用者。

验证失败的原因是用无效性来表达的。无效性基本上是某些验证条件的逆。

联系数据的无效性变体有

  • 电子邮件地址无效
  • 电话号码无效
  • 电子邮件地址和电话号码都缺失

请注意,不同的无效性变体可能同时适用,例如,同一个实体的电子邮件地址和电话号码可能都无效。

结果

我们已经意识到验证成功的结果基本上是的。在Rust中,这个无由单元类型()表示。

任何无效性都会导致验证失败。这意味着我们在检测到第一个无效性时应该尽早失败并终止验证吗?不一定。考虑与直接用户交互的表单验证用例。如果用户提交包含多个无效或缺失字段的表单,我们应该报告所有这些字段以减少不成功重试和往返的次数。

这导致我们对验证结果有一个初步的定义

type NaiveValidationResult = Result<(), Vec<Invalidity>>

我们将在稍后对其进行细化。

上下文

验证是一个递归操作,需要遍历深度嵌套的数据结构。这种遍历的当前状态定义了具有某种抽象级别的验证上下文。

ContactData级别,如果存在,我们需要递归验证电话号码和电子邮件地址。这些下属验证在较低的抽象级别上执行,不知道高级别的上下文。

此外,我们检查这两个成员是否都缺失,然后拒绝作为不完整ContactData。这是当前级别唯一实际实现且不使用递归的验证。

让我们使用求和类型将所有可能的变体编码在Rust中

enum PhoneNumberInvalidity {
  ...lower abstraction level...
}

enum EmailAddressInvalidity {
  ...lower abstraction level...
}

enum ContactDataInvalidity {
  Phone(PhoneNumberInvalidity),
  Email(EmailAddressInvalidity),
  Incomplete,
}

请注意,每个验证结果仅指代一个Invalidity类型。通过将较低级别上下文的Invalidity类型包裹在下属变体中,实现了从较低级别上下文到验证结果的递归嵌套。这些变体的名称通常类似于当前上下文中的角色名称。

结果 ...继续

在初步考虑的基础上,我们现在能够最终确定通用验证结果的定义

struct ValidationContext<V: Invalidity> {
  ...implementation details...
}

type ValidationResult<V: Invalidity> = Result<(), ValidationContext<V>>

ValidationContext负责以关联的Invalidity类型的多个变体形式收集验证结果。每个项目代表违反某些验证条件的行为,即已检测到的单个无效性。无效性如何收集的具体实现是隐藏的。

行为

我们通过实现通用Validate特质来增强我们的领域实体

pub trait Validate {
    type Invalidity: Invalidity;

    fn validate(&self) -> ValidationResult<Self::Invalidity>;
}

关联类型Invalidity通常定义为对应领域实体的伴随类型,就像我们上面看到的。不要被同名特质的限制所迷惑,它只是一个别名,代表Any + Debug

假设我们的复合实体 ContactData 的所有组件已经实现了此特性,实现过程就变得简单直接。

impl Validate for ContactData {
    type Invalidity = ContactDataInvalidity;

    fn validate(&self) -> ValidationResult<Self::Invalidity> {
        ValidationContext::new()
            .validate_with(&self.email, ContactDataInvalidity::EmailAddress)
            .validate_with(&self.phone, ContactDataInvalidity::PhoneNumber)
            .invalidate_if(
                // Either email or phone must be present
                self.email.is_none() && self.phone.is_none(),
                ContactDataInvalidity::Incomplete,
            )
            .into()
    }
}

验证函数首先创建一个新的空上下文。然后,它通过递归收集子验证的结果以及执行自己的验证规则。最后,它将上下文转换成结果,以便将其返回给调用者。

流畅接口 已被证明在大多数用例中非常有用且易于阅读,即使更复杂的验证可能需要在某些点上打断控制流。

推论

我们将业务需求的验证规则转换成了几行综合的代码。此代码与相应的领域实体相关联,并且只需要考虑一个抽象级别。递归组合使我们能够验证复杂的数据结构,并追踪验证失败的根源。

验证代码与基础设施组件独立,是包含在系统 功能核心 中的理想候选。通过简单的单元测试,我们可以验证验证是否按预期工作,并可靠地保护我们免受接受无效数据的影响。

不要做的事情

我们未涉及

  • 如何通过将它们定义为 标记变体 来增强 Invalidity 类型,以包含额外的、与上下文相关的数据,以及
  • 如何路由和解释验证结果。

这两个问题的答案相互依赖,需要特定于用例的解决方案,并且不受此库的任何限制。

许可证

根据Mozilla公共许可证2.0(MPL-2.0)授权(请参阅 MPL-2.0.txthttps://www.mozilla.org/MPL/2.0/)。

此版权许可的权限取决于提供授权文件的源代码及其修改的文件(在特定情况下,GNU许可证之一)。必须保留版权和许可通知。贡献者提供专利权的明确授予。但是,使用授权作品的大型作品可以根据不同的条款分发,并且不需要添加在大型作品中添加的文件。

贡献

您有意提交给工作以供包含的任何贡献都应根据Mozilla公共许可证2.0(MPL-2.0)授权。

必须在每个文件的顶部添加以下标题,并带有相应的 SPDX简短标识符

// SPDX-License-Identifier: MPL-2.0

依赖项

~74KB