#validate #payload #modify #validify #validation #derive-debug #proc-macro

已删除 derive_validator

validify的Validate特质提供宏

1 个不稳定版本

0.1.0 2023年1月16日

4#validify

MIT 许可证

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>。

验证器

验证器 类型 参数 描述
email 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