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
650 每月下载
在 3 个crate中使用(通过 validify)
155KB
3.5K SLoC
Validify
提供数据验证和修改属性的进程宏。特别适用于Web有效载荷的上下文中。
修饰符
修饰符 | 类型 | 描述 |
---|---|---|
trim* | 字符串 | 删除周围空白字符 |
uppercase* | 字符串 | 调用 .to_uppercase() |
lowercase* | 字符串 | 调用 .to_lowercase() |
capitalize* | 字符串 | 将字符串的第一个字符转换为大写 |
custom | 任何 | 接受一个函数作为参数,该函数的参数是 &mut <Type> |
validify | 实现 Validify / 实现Iterator |
只能用于实现 Validify 特性的字段(或结构体的集合)。运行嵌套结构的所有修饰符和验证。 |
*还可以通过运行每个元素的修饰符来用于 Vec
验证器
所有验证器也接受一个 code
和 message
作为参数,如果指定,其值必须是字符串字面量。
验证器 | 类型 | 参数 | 参数类型 | 描述 |
---|---|---|---|---|
字符串 | -- | -- | 根据 此规范 检查电子邮件。 | |
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
参数,如 此 所述。
接受的区间参数是 seconds
、minutes
、hours
、days
、weeks
。
由于它们如何验证输入,_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_from
和 validify_from
,它们各自的参数是生成的有效载荷。
ValidifyPayload
实现首先验证有效载荷的必需字段。然后,如果任何必需字段缺失,则不再进行进一步修改/验证,并返回错误。接下来,将有效载荷转换为原始结构体,并对其运行修改和/或验证。
当一个结构体包含嵌套的有效载荷(带有 #[validify]
注释的子结构体)时,有效载荷中的所有子结构体也将首先转换为有效载荷并进行验证。这意味着任何嵌套的结构体也必须推导 Payload
。
有效载荷和 serde
结构级属性,如 rename_all
,会被传播到有效载荷。当存在修改字段名的属性时,返回的错误中的任何字段名都将表示为原始的(即客户端有效载荷)。
validify 对一些特殊的 serde 属性处理不同;rename
、with
和 deserialize_with
。强烈建议这些属性与其他 serde 属性分开注解,因为这些属性是针对有效载荷进行解析的。
rename
属性由 validify 用于在验证期间设置任何错误中的字段名。with
和 deserialize_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