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 模式
598 每月下载量
在 2 个包中使用 (通过 axum-valid)
94KB
2K SLoC
Validify
提供数据验证和修改属性的程序宏。特别适用于 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
作为参数,它们的值必须是字符串字面量(如果指定)。
验证器 | 类型 | 参数 | 参数类型 | 描述 |
---|---|---|---|---|
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
参数,如此所示。
接受的间隔参数有 seconds
、minutes
、hours
、days
、weeks
。
由于它们验证输入的方式,_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_from
和 validify_from
,它们的相应参数是生成的有效载荷。
ValidifyPayload
实现将首先验证有效载荷的必需字段。然后,如果任何必需字段缺失,则不再进行进一步修改/验证,并将错误返回。接下来,将有效载荷转换为原始结构,并对其执行修改和/或验证。
当结构包含嵌套的 validifies(带有 #[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是一个具有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