9个稳定版本

1.4.0 2024年3月11日
1.3.0 2024年1月3日
1.2.0 2023年12月27日
1.0.12 2023年10月7日
1.0.11 2023年4月26日

#2 in #validify

Download history 506/week @ 2024-04-11 472/week @ 2024-04-18 246/week @ 2024-04-25 138/week @ 2024-05-02 59/week @ 2024-05-09 72/week @ 2024-05-16 68/week @ 2024-05-23 124/week @ 2024-05-30 181/week @ 2024-06-06 159/week @ 2024-06-13 114/week @ 2024-06-20 126/week @ 2024-06-27 208/week @ 2024-07-04 106/week @ 2024-07-11 141/week @ 2024-07-18 161/week @ 2024-07-25

650 每月下载
3 个crate中使用(通过 validify

MIT 许可证

155KB
3.5K SLoC

Validify

Build test coverage docs version

提供数据验证和修改属性的进程宏。特别适用于Web有效载荷的上下文中。

修饰符

修饰符 类型 描述
trim* 字符串 删除周围空白字符
uppercase* 字符串 调用 .to_uppercase()
lowercase* 字符串 调用 .to_lowercase()
capitalize* 字符串 将字符串的第一个字符转换为大写
custom 任何 接受一个函数作为参数,该函数的参数是 &mut <Type>
validify 实现 Validify / 实现Iterator 只能用于实现 Validify 特性的字段(或结构体的集合)。运行嵌套结构的所有修饰符和验证。

*还可以通过运行每个元素的修饰符来用于 Vec

验证器

所有验证器也接受一个 codemessage 作为参数,如果指定,其值必须是字符串字面量。

验证器 类型 参数 参数类型 描述
email 字符串 -- -- 根据 此规范 检查电子邮件。
ip 字符串 格式 标识符(v4/v6) 检查字符串是否是IP地址。
url 字符串 -- -- 检查字符串是否是URL。
length 集合 min, max, equal LitInt 检查集合长度是否在指定的参数范围内。通过HasLen trait进行。
range Int/Float min, max LitFloat 检查值是否在指定的范围内。
must_match 任何 value 标识符 检查字段是否与结构体的另一个字段匹配。值必须等于派生结构体上的字段标识符。
包含 集合 value 文件/路径 检查集合是否包含指定的值。如果用于 K,V 集合,则检查是否包含提供的键。
不包含 集合 value 文件/路径 检查集合是否不包含指定的值。如果用于 K,V 集合,则检查是否包含提供的键。
非控制字符 字符串 -- -- 检查字段是否包含控制字符
custom 函数 函数 路径 通过调用提供的函数,在字段上执行自定义验证
正则表达式 字符串 路径 路径 将提供的正则表达式与字段匹配。预期与 lazy_static 一起使用,提供初始化正则表达式的路径。
信用卡 字符串 -- -- 检查字段值是否为有效的信用卡号码
电话 字符串 -- -- 检查字段值是否为有效的电话号码
必填 Option -- -- 检查字段值是否为 Some
包含于 impl PartialEq 集合 路径 检查字段值是否在指定的集合中
不包含于 impl PartialEq 集合 路径 检查字段值是否不在指定的集合中
验证 impl Validate -- -- 调用底层结构的 validate 实现方法
迭代器 impl Iterator 验证器列表 验证器 对可迭代对象的每个元素运行提供的验证器
时间 NaiveDate[Time] 见下文 见下文 根据指定的操作执行检查

时间操作符

所有时间操作符都可以接受 inclusive = bool。所有时间操作符在验证日期时间时必须接受 time = bool,默认情况下,时间验证器将尝试验证日期。

in_period*_from_now 操作符默认是包含的。

参数 target 必须是一个字符串字面量日期或一个返回日期[时间]的无参数函数的路径。

如果目标是字符串字面量,它必须包含一个 format 参数,如 所述。

接受的区间参数是 secondsminuteshoursdaysweeks

由于它们如何验证输入,_from_now 操作符不应使用负持续时间,但对于 in_period,负持续时间可以正常工作。

操作符 参数 描述
之前 目标 检查日期[时间]是否在目标日期之前
之后 目标 检查日期[时间]是否在目标日期之后
之前于今天 -- 检查日期[时间]是否在今天的之前
之后于今天 -- 检查日期[时间]是否在今天的之后
之前于从现在起 间隔 检查日期[时间]是否在从今天[现在]起指定的间隔之前
之后于从现在起 间隔 检查日期[时间]是否在从今天[现在]起指定的间隔之后
在期内 目标,间隔 检查日期[时间]是否在某个特定时间段内

使用 Validify 属性(如果您不需要有效载荷或修改,则推导 validify::Validate)来注释您想要修改和验证的结构体

use validify::Validify;

#[derive(Debug, Clone, serde::Deserialize, Validify)]
struct Testor {
    #[modify(lowercase, trim)]
    #[validate(length(equal = 8))]
    pub a: String,
    #[modify(trim, uppercase)]
    pub b: Option<String>,
    #[modify(custom(do_something))]
    pub c: String,
    #[modify(custom(do_something))]
    pub d: Option<String>,
    #[validify]
    pub nested: Nestor,
}

#[derive(Debug, Clone, serde::Deserialize, Validify)]
struct Nestor {
    #[modify(trim, uppercase)]
    #[validate(length(equal = 12))]
    a: String,
    #[modify(capitalize)]
    #[validate(length(equal = 14))]
    b: String,
}

fn do_something(input: &mut String) {
    *input = String::from("modified");
}

let mut test = Testor {
  a: "   LOWER ME     ".to_string(),
  b: Some("  makemeshout   ".to_string()),
  c: "I'll never be the same".to_string(),
  d: Some("Me neither".to_string()),
  nested: Nestor {
    a: "   notsotinynow   ".to_string(),
      b: "capitalize me.".to_string(),
  },
};

// The magic line
let res = test.validify();

assert!(matches!(res, Ok(_)));

// Parent
assert_eq!(test.a, "lower me");
assert_eq!(test.b, Some("MAKEMESHOUT".to_string()));
assert_eq!(test.c, "modified");
assert_eq!(test.d, Some("modified".to_string()));
// Nested
assert_eq!(test.nested.a, "NOTSOTINYNOW");
assert_eq!(test.nested.b, "Capitalize me.");

注意,尽管字段 d 是一个选项,但用于修改字段的函数仍然接受 &mut String。这是因为只有当字段不是 None 时,才会执行修改器和验证器。

特质

Validify 是围绕 3 个简单特质构建的

  • 验证
  • 修改
  • Validify

理论上永远不需要手动实现这些特质。

如它们的名称所示,前两个特性执行验证和修改,而第三个特性将这两个动作合并为一个 - validify

这些特性包含一个函数,当推导它们时,该函数基于结构体注释构建。

有效载荷

带有 #[derive(Payload)] 注释的结构体会获得一个关联的有效载荷结构体,例如:

#[derive(validify::Validify, validify::Payload)]
struct Something {
  a: usize,
  b: String,
  c: Option<bool>
}

幕后将生成一个中间件

#[derive(Debug, Clone, serde::Deserialize, validify::Validate)]
struct SomethingPayload {
  #[validate(required)]
  a: Option<usize>,
  #[validate(required)]
  b: Option<String>,
  c: Option<bool>,

  /* From and Into impls */
}

这样做的原因是帮助反序列化可能缺失的字段。即使有效载荷结构体无法帮助反序列化错误类型,它仍然很有用,并提供了一些更有意义的错误消息,当字段缺失时。

原始结构体获得了 ValidifyPayload 实现和两个相关函数:validate_fromvalidify_from,它们各自的参数是生成的有效载荷。

ValidifyPayload 实现首先验证有效载荷的必需字段。然后,如果任何必需字段缺失,则不再进行进一步修改/验证,并返回错误。接下来,将有效载荷转换为原始结构体,并对其运行修改和/或验证。

当一个结构体包含嵌套的有效载荷(带有 #[validify] 注释的子结构体)时,有效载荷中的所有子结构体也将首先转换为有效载荷并进行验证。这意味着任何嵌套的结构体也必须推导 Payload

有效载荷和 serde

结构级属性,如 rename_all,会被传播到有效载荷。当存在修改字段名的属性时,返回的错误中的任何字段名都将表示为原始的(即客户端有效载荷)。

validify 对一些特殊的 serde 属性处理不同;renamewithdeserialize_with。强烈建议这些属性与其他 serde 属性分开注解,因为这些属性是针对有效载荷进行解析的。

rename 属性由 validify 用于在验证期间设置任何错误中的字段名。withdeserialize_with 将被转移到有效载荷字段,并创建一个特殊的反序列化函数,该函数将调用原始函数并将结果包裹在选项中。如果自定义反序列化器已经返回一个选项,则不会执行任何操作。

模式验证

可以使用以下方式执行模式级验证

use validify::{Validify, ValidationErrors, schema_validation, schema_err};
#[derive(validify::Validify)]
#[validate(validate_testor)]
struct Testor {
    a: String,
    b: usize,
}

#[schema_validation]
fn validate_testor(t: &Testor) -> Result<(), ValidationErrors> {
  if t.a.as_str() == "yolo" && t.b < 2 {
    schema_err!("Invalid Yolo", "Cannot yolo with b < 2");
  }
}

#[schema_validation] proc 宏将函数展开为:

fn validate_testor(t: &Testor) -> Result<(), ValidationErrors> {
    let mut errors = ValidationErrors::new();
    if t.a == "yolo" && t.b < 2 {
        errors.add(ValidationError::new_schema("Invalid Yolo").with_message("Cannot yolo with b < 2".to_string()));
    }
    if errors.is_empty() { Ok(()) } else { Err(errors) }
}

这使得模式验证更加便捷和简洁。与字段级验证一样,模式级验证在修改后执行。

错误

主要的 ValidationError 是一个具有两种变体的枚举,字段和模式。字段错误是,正如其名称所示,在字段验证失败时创建的,通常在除非使用自定义处理程序(自定义字段验证函数始终必须返回一个结果,其 Err 变体是 ValidationError)的情况下自动生成。

如果您想随错误提供消息,可以直接在属性中指定它(同样适用于代码),例如

#[验证(包含(value= "某种",消息= "不包含某种",代码= "MUST_CONTAIN"))]

请注意,以这种方式指定验证时,所有属性参数都必须指定为 NameValue 对。这意味着如果您编写

#[验证(包含("某种",消息= "Bla"))],

您将收到一个错误,因为解析器期望一个值或多个名称值对。

field_err! 宏在自定义函数中使用时提供了创建字段错误的快捷方式。

位置

位置以类似JSON 指针的方式跟踪每个错误。当使用自定义验证时,返回错误中指定的任何字段名称都将用于该字段的定位。请注意,当处理散列映射/集合集合时,位置可能不可靠,因为这些项的顺序没有保证。

错误位置的显示将取决于原始客户端有效负载,即它们将以接收原始有效负载的原始情况显示(例如,当使用 serde 的 rename_all)。任何被覆盖的字段名称都将按原样显示。

模式

模式错误通常在模式验证中由用户创建。与 #[schema_validation] 一起使用的 schema_err! 宏提供了一种创建模式错误的便捷方式。所有错误都组合到一个 ValidationErrors 结构中,该结构包含所有验证错误的向量。

参数

当合理时,validify 会自动将失败的参数及其验证过的目标值追加到创建的错误中,以向客户端提供更多清晰度并节省一些手动工作。

始终追加的一个参数是表示在验证期间违反字段的目标属性的值的 actual 字段。一些验证器将表示字段预期值的附加数据追加到错误中。

示例

日期[times]s

use chrono::{NaiveDate, NaiveDateTime};

#[derive(Debug, validify::Validate)]
struct DateTimeExamples {
    #[validate(time(op = before, target = "2500-04-20", format = "%Y-%m-%d", inclusive = true))]
    before: NaiveDate,
    #[validate(time(op = before, target = "2500-04-20T12:00:00.000", format = "%Y-%m-%-dT%H:%M:%S%.3f"))]
    before_dt: NaiveDateTime,
    #[validate(time(op = after, target = "2022-04-20", format = "%Y-%m-%d"))]
    after: NaiveDate,
    #[validate(time(op = after, target = "2022-04-20T12:00:00.000", format = "%Y-%m-%-dT%H:%M:%S%.3f"))]
    after_dt: NaiveDateTime,
    #[validate(time(op = in_period, target = "2022-04-20", format = "%Y-%m-%d", weeks = -2))]
    period: NaiveDate,
}

带有路由处理程序

    use validify::{Validify, Payload, ValidifyPayload};

    #[derive(Debug, Validify, Payload)]
    struct JsonTest {
        #[modify(lowercase)]
        a: String,
        #[modify(trim, uppercase)]
        #[validate(length(equal = 11))]
        b: String,
    }

    // This would normally come from a framework
    struct Json<T>(T);

    fn test() {
      let jt = JsonTest {
          a: "MODIFIED".to_string(),
          b: "    makemeshout    ".to_string(),
      };
      let json = Json(JsonTestPayload::from(jt));
      mock_handler(json)
    }

    fn mock_handler(data: Json<JsonTestPayload>) {
      let data = data.0;
      let data = JsonTest::validify_from(data.into()).unwrap();
      mock_service(data);
    }

    fn mock_service(data: JsonTest) {
      assert_eq!(data.a, "modified".to_string());
      assert_eq!(data.b, "MAKEMESHOUT".to_string())
    }

更多示例请参阅测试目录

贡献

如果您有任何关于如何改进 Validify 的想法,例如您发现有用的常见验证或更好的错误消息,请毫不犹豫地打开一个问题或 PR。所有想法都受欢迎!

依赖关系

~3.5–5MB
~90K SLoC