1 个不稳定版本
0.1.0 | 2023年1月16日 |
---|
#3 在 #validify 中
每月下载量 120
在 2 个crate 中使用
20KB
66 行
Validify
基于 validator crate 的过程宏,提供字段修饰符的属性。特别适用于 Web 负载的上下文。
修饰符
修饰符 | 类型 | 描述 |
---|---|---|
trim* | 字符串 | 删除周围的空白 |
uppercase* | 字符串 | 调用 .to_uppercase() |
lowercase* | 字符串 | 调用 .to_lowercase() |
capitalize* | 字符串 | 使字符串的第一个字符大写 |
custom | 任何类型 | 接受一个函数,其参数是 &mut <Type> |
validify* | 结构体 | 只能用于实现 Validify 特性的结构体字段。运行嵌套结构体的所有修饰符和验证 |
*对于 Vec
验证器
验证器 | 类型 | 参数 | 描述 |
---|---|---|---|
字符串 | -- | 根据 此规范 检查电子邮件。 | |
url | 字符串 | -- | 检查字符串是否是 URL。 |
length | 集合 | min, max, equal | 检查字段的集合长度是否在指定的参数内。 |
range | 数字 | min, max | 检查字段的值是否在指定的范围内。 |
must_match | 任何类型 | 任何类型 | 检查字段是否与指定的值匹配 |
contains | 集合 | 项目 | 检查集合是否包含指定的值 |
does_not_contain | 集合 | 项目 | 检查集合是否不包含指定的值 |
non_control_character | 字符串 | -- | 检查字段是否包含控制字符 |
custom | 函数 | FnItem | 执行用户指定的字段自定义验证 |
正则表达式 | 字符串 | Regex* | 将提供的正则表达式与字段进行匹配 |
credit_card | 字符串 | -- | 检查字段值是否为有效的信用卡号码 |
phone | 字符串 | -- | 检查字段值是否为有效的电话号码 |
required | 选项 | -- | 检查字段值是否为“Some” |
is_in | String/Num | Collection* | 检查字段值是否在提供的集合中 |
not_in | String/Num | Collection* | 检查字段值是否不在提供的集合中 |
* 参数以字符串表示法指定,即 "param"
.
该包提供了 Validify
特性和 validify
属性宏,并支持验证器包的所有功能。这里的主要新增功能是在验证之前可以修改有效负载。
这在例如,当有效负载的 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
中实现验证器的 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);
}
依赖项
~1.5MB
~34K SLoC