1 个不稳定版本
0.1.0 | 2023年1月16日 |
---|
4 在 #validify
110KB
1.5K SLoC
Validify
基于validatorcrate构建的过程宏,提供字段修饰符属性。特别适用于Web有效载荷的上下文。
修饰符
修饰符 | 类型 | 描述 |
---|---|---|
trim* | String | 移除周围空白字符 |
uppercase* | String | 调用.to_uppercase() |
lowercase* | String | 调用.to_lowercase() |
capitalize* | String | 将字符串的第一个字符转换为大写 |
custom | Any | 接受一个参数为 &mut <Type> 的函数 |
validify* | Struct | 只能用于实现了Validify 特质的字段。运行嵌套结构的所有修饰符和验证 |
*也可以通过在元素上运行validate来支持Vec<T>。
验证器
验证器 | 类型 | 参数 | 描述 |
---|---|---|---|
String | -- | 根据此规范检查电子邮件。 | |
url | String | -- | 检查字符串是否为URL。 |
length | Collection | min, max, equal | 检查字段的集合长度是否在指定的参数范围内。 |
range | Number | min, max | 检查字段的值是否在指定的范围内。 |
must_match | Any | Any* | 检查字段是否匹配指定的值 |
contains | Collection | Item* | 检查集合是否包含指定的值 |
does_not_contain | Collection | Item* | 检查集合是否不包含指定的值 |
non_control_character | String | -- | 检查字段是否包含控制字符 |
custom | Function | FnItem* | 执行用户指定的字段验证 |
regex | String | Regex* | 将提供的正则表达式与字段匹配 |
credit_card | String | -- | 检查字段值是否为有效的信用卡号码 |
电话 | String | -- | 检查字段值是否为有效的电话号码 |
必填 | 选项 | -- | 检查字段值是否为Some |
is_in | 字符串/数字 | 集合* | 检查字段值是否在提供的集合中 |
not_in | 字符串/数字 | 集合* | 检查字段值是否不在提供的集合中 |
* 参数以字符串表示法指定,例如 "param"
。
该库提供了Validify
特性和validify
属性宏,并支持validator crate的所有功能。这里的主要增加是可以在验证之前修改有效载荷。
这在例如,当有效载荷的String
字段有最小长度限制而你又不想它只包含空格时非常有用。Validify允许你在验证之前修改字段,以减轻这个问题。
使用validify
宏注释你想要修改和验证的结构体
use validify::{validify, Validify};
#[validify]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
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,
}
#[validify]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
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 = Testor::validify(test.into());
assert!(matches!(res, Ok(_)));
let test = res.unwrap();
// 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]
struct Something {
a: usize,
b: String,
c: Option<bool>
}
幕后将生成一个中间
#[derive(Debug, Clone, Deserialize, validify::Validate)]
struct SomethingPayload {
#[validate(required)]
a: Option<usize>,
#[validate(required)]
b: Option<String>
c: Option<bool>
/* From and Into impls */
}
请注意,每个非选项字段都将作为有效载荷中的“可选”必填字段(仅为避免反序列化错误)。Validify实现首先验证生成有效载荷的必填字段。如果任何必填字段缺失,则不会进行进一步的修改/验证,并返回错误。接下来,将有效载荷转换为原始结构体,并对它执行修改和验证。
Validify的validify
方法始终接受生成的有效载荷,如果所有验证都通过,则输出原始结构体。
该宏自动在包装特性Validify
中实现validator的Validate
特性和validify的Modify
特性。此包装特性只包含一个方法validify
,在上述示例中扩展为
fn validify(payload: Self::Payload) -> Result<(), ValidationErrors> {
<Self::Payload as ::validify::Validate>::validate(&payload)?;
let mut this = Self::from(payload);
let mut errors: Vec<::validify::ValidationErrors> = Vec::new();
if let Err(e) = <Nestor as ::validify::Validify>::validify(this.nested.clone().into()) {
errors.push(e.into());
}
<Self as ::validify::Modify>::modify(&mut this);
if let Err(e) = <Self as ::validify::Validate>::validate(&this) {
errors.push(e.into());
}
if !errors.is_empty() {
let mut errs = ::validify::ValidationErrors::new();
for err in errors {
errs = errs.merge(err);
}
return Err(errs);
}
Ok(this)
}
可以使用以下方式执行模式级别验证
#[validify]
#[validate(schema(function = "validate_testor"))]
struct Testor {
a: String,
b: usize,
}
#[schema_validation]
fn validate_testor(t: &Testor) -> Result<(), ValidationErrors> {
if t.a == "yolo" && t.b < 2 {
validify::schema_err!("Invalid Yolo", "Cannot yolo with b < 2", errors);
}
}
#[schema_validation]
进程宏将函数扩展为
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个变体的枚举,字段和模式。字段错误如名称所示,在字段验证失败时创建,通常在未使用自定义处理程序的情况下自动生成(自定义字段验证始终必须返回一个结果,其Err变体是ValidationError)。模式错误通常由用户在模式验证中创建。schema_err!
和field_err!
宏提供了创建错误的便捷方式。所有错误都组合到ValidationErrors
结构体中,其中包含所有验证错误的向量。
示例
与路由处理器一起使用
fn actix_test() {
#[validify]
#[derive(Debug, Serialize)]
struct JsonTest {
#[modify(lowercase)]
a: String,
#[modify(trim, uppercase)]
#[validate(length(equal = 11))]
b: String,
}
let jt = JsonTest {
a: "MODIFIED".to_string(),
b: " makemeshout ".to_string(),
};
let json = actix_web::web::Json(jt.into());
mock_handler(json)
}
fn mock_handler(data: actix_web::web::Json<JsonTestPayload>
/* OR data: actix_web::web::Json<<JsonTest as Validify>::Payload> */) {
let data = data.0;
let data = JsonTest::validify(data).unwrap();
mock_service(data);
}
fn mock_service(data: JsonTest) {
assert_eq!(data.a, "modified".to_string());
assert_eq!(data.b, "MAKEMESHOUT".to_string())
}
Big Boi
const WORKING_HOURS: &[&str] = &["08", "09", "10", "11", "12", "13", "14", "15", "16"];
const CAREER_LEVEL: &[&str] = &["One", "Two", "Over 9000"];
const STATUSES: &[&str] = &["online", "offline"];
const CONTRACT_TYPES: &[&str] = &["Fulltime", "Temporary"];
const ALLOWED_MIME: &[&str] = &["jpeg", "png"];
const ALLOWED_DURATIONS: &[i32] = &[1, 2, 3];
#[validify]
#[derive(Clone, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[validate(schema(function = "schema_validation"))]
struct BigBoi {
#[modify(trim)]
#[validate(length(max = 300))]
title: String,
#[modify(trim)]
#[validate(is_in = "STATUSES")]
status: String,
#[modify(capitalize, trim)]
city_country: String,
#[validate(length(max = 1000))]
education: String,
#[modify(capitalize)]
type_of_workplace: Vec<String>,
#[validate(is_in = "WORKING_HOURS")]
working_hours: String,
part_time_period: Option<String>,
#[modify(capitalize)]
#[validate(is_in = "CONTRACT_TYPES")]
contract_type: String,
indefinite_probation_period: bool,
#[validate(is_in = "ALLOWED_DURATIONS")]
indefinite_probation_period_duration: Option<i32>,
#[validate(is_in = "CAREER_LEVEL")]
career_level: String,
#[modify(capitalize)]
benefits: String,
#[validate(length(max = 60))]
meta_title: String,
#[validate(length(max = 160))]
meta_description: String,
#[validate(is_in = "ALLOWED_MIME")]
meta_image: String,
#[validate(custom = "greater_than_now")]
published_at: String,
#[validate(custom = "greater_than_now")]
expires_at: String,
#[validify]
languages: Vec<TestLanguages>,
#[validify]
tags: TestTags,
}
#[schema_validation]
fn schema_validation(bb: &BigBoi) -> Result<(), ValidationErrors> {
if bb.contract_type == "Fulltime" && bb.part_time_period.is_some() {
schema_err!("Fulltime contract cannot have part time period", errors);
}
if bb.contract_type == "Fulltime"
&& bb.indefinite_probation_period
&& bb.indefinite_probation_period_duration.is_none()
{
schema_err!(
"No probation duration",
"Indefinite probation duration must be specified",
errors
);
}
}
fn greater_than_now(date: &str) -> Result<(), ValidationError> {
let parsed = chrono::NaiveDateTime::parse_from_str(date, "%Y-%m-%d %H:%M:%S");
match parsed {
Ok(date) => {
if date
< chrono::NaiveDateTime::from_timestamp_opt(chrono::Utc::now().timestamp(), 0)
.unwrap()
{
Err(ValidationError::new_field(
"field",
"Date cannot be less than now",
))
} else {
Ok(())
}
}
Err(e) => {
Err(ValidationError::new_field("field", "Could not parse date"))
}
}
}
#[validify]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct TestTags {
#[modify(trim)]
#[validate(length(min = 1, max = 10), custom = "validate_names")]
names: Vec<String>,
}
fn validate_names(names: &[String]) -> Result<(), ValidationError> {
for n in names.iter() {
if n.len() > 10 || n.is_empty() {
return Err(ValidationError::new_field(
"names",
"Maximum length of 10 exceeded for name",
));
}
}
Ok(())
}
const PROFICIENCY: &[&str] = &["dunno", "killinit"];
#[validify]
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct TestLanguages {
company_opening_id: String,
#[modify(trim)]
language: String,
#[modify(trim)]
#[validate(is_in = "PROFICIENCY")]
proficiency: Option<String>,
required: Option<bool>,
created_by: String,
}
fn biggest_of_bois() {
let tags = TestTags {
// Invalid length due to `validate_names`
names: vec![
"taggggggggggggggggggggggggg".to_string(),
"tag".to_string(),
"tag".to_string(),
],
};
let languages = vec![
TestLanguages {
company_opening_id: "yolo mcswag".to_string(),
language: " tommorrowlang ".to_string(),
// Invalid proficiency
proficiency: Some("invalid ".to_string()),
required: Some(true),
created_by: "me".to_string(),
},
TestLanguages {
company_opening_id: "divops".to_string(),
language: "go".to_string(),
// Invalid proficiency
proficiency: Some(" invalid".to_string()),
required: None,
created_by: "they".to_string(),
},
];
let big = BigBoi {
title: "me so big".to_string(),
// Invalid status
status: "invalid".to_string(),
city_country: "gradrzava".to_string(),
description_roles_responsibilites: "ask no questions tell no lies".to_string(),
education: "any".to_string(),
type_of_workplace: vec!["dumpster".to_string(), "mcdonalds".to_string()],
// Invalid working hours
working_hours: "invalid".to_string(),
// Part time period with fulltime contract type
part_time_period: Some(String::new()),
contract_type: "Fulltime".to_string(),
// Fulltime period with no duration
indefinite_probation_period: true,
indefinite_probation_period_duration: None,
// Invalid career level
career_level: "Over 100000".to_string(),
benefits: "none".to_string(),
meta_title: "this struct is getting pretty big".to_string(),
meta_description: "and it's king of annoying".to_string(),
// Invalid mime type
meta_image: "heic".to_string(),
// Invalid time
published_at: "1999-01-01 00:00:00".to_string(),
// Invalid time
expires_at: "1999-01-01 00:00:00".to_string(),
languages,
tags,
};
let res = BigBoi::validify(big.into());
assert!(matches!(res, Err(ref e) if e.errors().len() == 11));
let schema_errs = res.as_ref().unwrap_err().schema_errors();
let field_errs = res.unwrap_err().field_errors();
assert_eq!(schema_errs.len(), 2);
assert_eq!(field_errs.len(), 9);
}
依赖项
~3–4.5MB
~88K SLoC