#validation #payload #validate #modify #macro-derive #date-time #proc-macro

validify

通过使用 derive 宏提供结构体验证和修改功能

8 个稳定版本

1.4.0 2024年3月11日
1.3.0 2024年1月3日
1.2.0 2023年12月27日
1.0.12 2023年10月7日
0.1.0 2023年1月16日

#167 in Rust 模式

Download history 361/week @ 2024-03-24 444/week @ 2024-03-31 526/week @ 2024-04-07 548/week @ 2024-04-14 292/week @ 2024-04-21 185/week @ 2024-04-28 95/week @ 2024-05-05 31/week @ 2024-05-12 80/week @ 2024-05-19 71/week @ 2024-05-26 135/week @ 2024-06-02 204/week @ 2024-06-09 91/week @ 2024-06-16 126/week @ 2024-06-23 229/week @ 2024-06-30 132/week @ 2024-07-07

598 每月下载量
2 个包中使用 (通过 axum-valid)

MIT 许可证

94KB
2K SLoC

Validify

Build test coverage docs version

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

修改器

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

*也可以用于 Vec<String>,通过运行每个元素的修改器。

验证器

所有验证器都接受一个 code 和一个 message 作为参数,它们的值必须是字符串字面量(如果指定)。

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

时间运算符

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

in_period*_from_now 运算符默认为包含。

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

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

接受的间隔参数有 secondsminuteshoursdaysweeks

由于它们验证输入的方式,_from_now 运算符不应使用负持续时间,因为 in_period 的负持续时间可以正常工作。

Op 参数 描述
before target 检查日期[时间]是否在目标日期之前
after target 检查日期[时间]是否在目标日期之后
before_now -- 检查日期[时间]是否在今天的之前
after_now -- 检查日期[时间]是否在今天的之后
before_from_now interval 检查日期[时间]是否在从今天起的指定间隔之前
after_from_now interval 检查日期[时间]是否在从今天起的指定间隔之后
in_period target, interval 检查日期[时间]是否在某个特定时间段内

使用 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 基于三个简单的特性构建

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

当结构包含嵌套的 validifies(带有 #[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是一个具有2个变体的枚举,分别是Field和Schema。Field错误,正如其名所示,在字段验证失败时创建,通常在未使用自定义处理器的情况下自动生成(自定义字段验证函数始终必须返回一个结果,其Err变体为ValidationError)。

如果您想与错误一起提供消息,可以直接在属性中指定(对代码也是如此),例如

#[validate(contains(value= "某种内容",消息= "不包含某种内容",代码= "MUST_CONTAIN"))]

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

#[validate(contains("某种内容",消息= "Bla"))],

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

field_err!宏在自定义函数中创建字段错误时提供了一种简写方法。

位置

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

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

模式

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

参数

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

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

示例

Date[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。所有想法都欢迎!

依赖关系

~25MB
~242K SLoC