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

已删除 validify_types

validify 的 Validate 特性提供派生宏

1 个不稳定版本

0.1.0 2023年1月16日

#3#validify

Download history

每月下载量 120
2 个crate 中使用

MIT 许可证

20KB
66

Validify

基于 validator crate 的过程宏,提供字段修饰符的属性。特别适用于 Web 负载的上下文。

修饰符

修饰符 类型 描述
trim* 字符串 删除周围的空白
uppercase* 字符串 调用 .to_uppercase()
lowercase* 字符串 调用 .to_lowercase()
capitalize* 字符串 使字符串的第一个字符大写
custom 任何类型 接受一个函数,其参数是 &mut <Type>
validify* 结构体 只能用于实现 Validify 特性的结构体字段。运行嵌套结构体的所有修饰符和验证

*对于 Vec,通过在每个元素上运行 validate 也适用。

验证器

验证器 类型 参数 描述
email 字符串 -- 根据 此规范 检查电子邮件。
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 实现首先验证生成的有效负载的必需字段。如果有任何必需字段缺失,则不再执行进一步修改/验证,并返回错误。接下来,将有效负载转换为原始结构体,并对它执行修改和验证。

Validifyvalidify 方法始终接收生成的有效负载,并在所有验证都通过的情况下输出原始结构体。

该宏自动在包装器特质 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