4 个版本 (2 个重大变更)

0.3.0 2023 年 7 月 8 日
0.2.1 2023 年 7 月 8 日
0.1.2 2023 年 6 月 16 日

#1417 in Rust 模式

每月 29 次下载

MIT 许可证

80KB
1K SLoC

validus --- 验证过的字符串切片

// A three-step process.

// 1. Define your validation. No need to re-implement your validation logic!
// (though, we have a macro that makes it easy if you do have control.)
// Validus offers multiple tradeoff points between flexibility and convenience.

// 2. Replace `str` with `vstr<Rule>` where you need validation.
// `&vstr` coerces to `&str`, so you can use it deep inside your codebase,
// or even a foreign codebase. (Limitations exist.)

// 3. Use `vstr<Rule>` as though it were `str`.

# use std::sync::OnceLock;
# 
use validus::prelude::*; // vstr, fast_rule, <&str>.validate(), etc.
use validus::fast_rule;

# 
# use regex::Regex;
#
// Let's define a string validation rule that uses a Regex.
const USERNAME_RE_S: &str = r#"^[a-zA-Z0-9_]{1,16}$"#;
static USERNAME_RE: OnceLock<Regex> = OnceLock::new();

// 1. Defining the rule type and the error type.
// - I used fast_rule! because I have control over the error type.
// - If you are using existing error types and/or function,
// you can use easy_rule! instead. There's also a macro-free way;
// see the `vstr` module docs for more info.
struct BadUsernameError;
struct UsernameRule;
fast_rule!(
    UsernameRule,
    err = BadUsernameError,
    msg = "bad username",
    // The closure below could very well be a function.
    // So if the function were called `username_rule`,
    // I could do fast_rule!(..., ..., ..., username_rule)
    |s: &str| {
        let re = USERNAME_RE.get_or_init(|| Regex::new(USERNAME_RE_S).unwrap());
        re.is_match(s)
    }
);
// (There's also easy_rule that allows you to bring your own error type.)
// (That's helpful when you have existing error types.)
// (... or, walk the macro-free path and implement ValidateString manually.)
// (consult the `vstr` module docs for more info.)

// 2. Restrict your string slice with the rule.
type Username = vstr<UsernameRule>;

// 3. Use your `vstr<Rule>` as though it were `str`.
let input = "hello";
let username: Result<&Username, BadUsernameError> = input.validate();
assert!(username.is_ok());
assert_eq!(username.unwrap(), "hello");

let input = "haha 😊";
let username: Result<&Username, _> = input.validate();
assert!(username.is_err());

// It's that easy! Let's do a recap.

// - Error and the rule:
// struct MyError;
// struct MyRule;
// fn my_rule_priv(s: &str) -> Result<(), MyError> { ... }
// fast_rule!(MyRule, err = MyError, msg = "my error", my_rule_priv);

// - Use `vstr` as though it were `str`:
// type MyStr = vstr<MyRule>;
// let a: &vstr<MyRule> = "hello".validate().unwrap();

// Plus, this library has serde support with validation, and more.

摘要

  1. 自带验证,将 vstr<_> 当作 str 使用(期望不可变 str 引用)。
  2. 将其混合到您的代码中。 如果您已经有了现有的验证模块,不要修改它。Validus 有多种简单的方法将您的验证包装到 vstr<_> 兼容的规则类型中。
  3. 开始时走捷径。 如果您没有现有的验证模块,可以使用 fast_rule! 宏快速定义一个带有谓词(闭包或外部函数,可能或可能不属于您的包)的规则类型。您的错误类型将是正确的 std::error::Error 类型;甚至可以使用一个 ad-hoc &'static str 错误消息。
  4. 无需额外代码即可与 serde 一起工作。 简单地将 #[derive(Serialize, Deserialize)] 添加到您的结构体中,就像平常一样。现在,无需担心无效的字符串被反序列化。
  5. 如果您想,可以选择懒验证。 使用一个名为 Later<_> 的特殊规则,您还可以选择是立即验证还是延迟验证,直到您决定需要它的时候。(例如,对于 serde 反序列化非常有用。)
  6. 现场合并规则。默认启用扩展提供了三个逻辑连接符(和、或、非);这些在您的序言中,因此您可以立即使用它们。

Validus库提供了一个名为vstr的新类型,它是正则字符串切片的封装。此vstr由您的验证规则类型参数化,例如vstr<EmailRule>。任何实现了ValidateString特质的零大小类型都可以用作规则。

vstr旨在在某些上下文中替代str。因此,

  • vstr实现了与其它规则vstrstr相同的Eq
  • ...以及与任何其他str引用相同的OrdHash
  • vstr了解StringCow以及智能指针类型,例如BoxRcArc

为了向您展示vstr<_>如何与str引用相同地进行比较和哈希,我将给出一个直接使用vstr作为HashMapHashSet中的键的示例。

// Illustration: using vstr<_> as a key in a HashMap.
# 
# use std::collections::HashMap;
# 
# use validus::prelude::*;
# use validus::fast_rule;

struct BadUsernameError;
struct UsernameRule;
fast_rule!(
    UsernameRule,
    err = BadUsernameError,
    msg = "bad username",
    |s: &str| s.len() <= 16
);

type Username = vstr<UsernameRule>;

let mut map = HashMap::<&Username, i32>::new();
map.insert("hello".validate().unwrap(), 1);
map.insert("world".validate().unwrap(), 2);

// assume_valid bypasses validation, incurring no computational cost,
// so it's useful in this case.
assert_eq!(map.get("hello".assume_valid()), Some(&1));
assert_eq!(map.get("world".assume_valid()), Some(&2));

// So every time you need a `&str` but with validation,
// you know that Validus and `vstr<_>` have got you covered,
// anywhere in your codebase, or a foreign codebase.
// (Limitations exist.)

serde与验证

使用可选的serde功能,这个crate还支持带有验证的序列化和反序列化。这意味着您可以将vstr<_>用作由serde支持的struct的字段,如果输入验证失败,它将被拒绝,并返回一个根据验证规则关联的Error类型的错误。

  • serde功能默认启用。在您的Cargo.toml中使用default-features = false禁用它。
// Illustration: a struct with a validated email field.

#[cfg(feature = "serde")] {

use validus::prelude::*;
use validus::fast_rule;
use serde::Deserialize;

// This rule is very generous. It accepts any string that
// contains an at-symbol.
// (When the error type is not specified, it is inferred to
// be &'static str.)
struct EmailRule;
fast_rule!(EmailRule, msg = "no at-symbol", |s: &str| s.contains('@'));

#[derive(Deserialize)]
pub struct User {
    pub email: Box<vstr<EmailRule>>,
}

let input = r#"{"email": "notgood"}"#;
let result = serde_json::from_str::<User>(input);
assert!(result.is_err());

let input = r#"{"email": "[email protected]"}"#;
let result = serde_json::from_str::<User>(input);
assert!(result.is_ok());
assert!(result.unwrap().email.as_str() == "[email protected]");

}

使用Later进行延迟验证

有时,您只想在实际使用字符串切片时进行验证。

为此需求,有一个名为Later的规则,它绕过了所有验证,但指定了应该使用哪个规则进行验证。当实际需要验证时,您可以调用make_strict来验证字符串切片,并使用指定的规则将其转换为vstr

这里,我复制了Later类型文档中的示例代码。

use validus::prelude::*;
use validus::fast_rule;

struct EmailError;
struct Email;
fast_rule!(Email,
    err = EmailError,
    msg = "no @ symbol",
    |s: &str| s.contains('@')
);

// Here, we start with an email with deferred (postponed) validation.
// Validation of `Later<_>` is infallible.
let v1: &vstr<Later<Email>> = "[email protected]".validate().unwrap();
// Now, we truly validate it.
let v1: Result<&vstr<Email>, _> = v1.make_strict();
assert!(v1.is_ok());

// So, again, this is going to succeed.
let v2 = "notgood".validate::<Later<Email>>().unwrap();
// But, when we check it, it will fail, since it is not a good email address
// (according to the rule we defined).
let v2 = v2.make_strict();
assert!(v2.is_err());

// With the extension `StrExt`, we can also call `.assume_valid()`
// to skip validation, since we know that `Later<_>` doesn't validate.

let relaxed = "[email protected]".assume_valid::<Later<Email>>();
assert!(relaxed.check().is_ok()); // This is infallible because `Later<_>` is infallible.
assert!(relaxed.make_strict().is_ok()); // Later<Email> -> Email.

let relaxed = "nonono".assume_valid::<Later<Email>>();
assert!(relaxed.check().is_ok()); // Yup, it is still infallible.
let strict = relaxed.make_strict(); // Now, we made it strict.
assert!(strict.is_err()); // It didn't make it (it was a bad email address.)

使用assume_valid覆盖规则并与check检查

您还拥有使用 assume_valid 覆盖底层机制的能力。这在您有一个已知有效但难以在某一时刻判断的 vstr<_> 时非常有用;或者,在某些情况下,您不需要验证 vstr<_>(例如,当您将其用作 HashMap 中的查找键时)。这个库提供了一个 check() 方法,可以用来确定 vstr<_> 的有效性。

// Illustration: overriding the validation mechanism.

use validus::prelude::*;
use validus::easy_rule;

// easy_rule is a different macro that helps you define rules.
// The difference with fast_rule! is that leaves the error type
// untouched, and you need to return a Result<(), YourError>
// instead of a bool in the closure.
struct No;
easy_rule!(No, err = &'static str, |s: &str| Err("i won't accept anything"));

let s = "hello";
let v: &vstr<No> = vstr::assume_valid(s);

// Yup, it works. We overrode the validation mechanism.
assert_eq!(v, "hello");

// But it's not valid. Let's test that.
assert!(v.check().is_err());

assume_valid 不是 unsafevstr 对字符串切片的有效性不提供任何额外保证,超出 str 提供的内容。[它也没有更少]。因此,assume_valid 不应受到引起未定义行为的责备。

定义规则之间的蕴涵关系

此外,由于某些规则对可以自动转换(它们之间存在 IMPLIES 关系),您可以使用 change_rules 相关方法将 vstr<Rule1> 的引用转换为 vstr<Rule2> 的引用。这需要 Rule 实现 Into<Rule2>。(否则,可以在任何两个规则之间使用常规的 try_change_rules。)

// Illustration: declaring implication.
// Implication means: "Whenever [rule] A says good, so does B."

use validus::prelude::*;
use validus::fast_rule;

// Less generous
struct A;
fast_rule!(A, msg = "no wow", |s: &str| s.contains("wow"));

// More generous: includes all strings that A accepts and
// perhaps more.
struct B;
fast_rule!(B, msg = "neither wow nor bad found", |s: &str| {
    s.contains("wow") || s.contains("bad")
});

// Assert that A implies B.
// In English: "whatever string A accepts, B accepts, too."
impl From<A> for B {
    // This particular formulation is idiomatic
    // to the `validus` crate because all rules are supposed
    // to be freely constructed Zero-Sized Types (ZSTs).
    fn from(_: A) -> Self {
        // And, this value never gets used, anyway.
        // All methods of `ValidateString` (trait that
        // defines rules) have static methods, not instance
        // methods.
        B
    }
}

// The declaration of implication unlocks the `change_rules`
// method that converts a reference to `vstr<A>` to a reference
// to `vstr<B>` infallibly.

let good = "wow bad";
let a: &vstr<A> = vstr::assume_valid(good); // we know it works, so.
let _: &vstr<B> = a.change_rules(); // infallible. see, no Result or unwrap().

特殊的规则 ValidateAll

还有一点。有两个特殊的规则,分别验证所有字符串和不验证任何字符串。它们被称为 ValidateAll()。尽管您不能使用 change_rules 将您的规则转换为 ValidateAll,但您仍然可以使用专门的 erase_rules 方法。从 ValidateAll,您可以使用 try_change_rules 转换为任何其他规则。

内置电池...(但我需要您的帮助!)

请查看模块 vstrext 中的一些准备好的验证规则。该模块应该已经包含在预置模块中(它由 ext 特性门控,默认启用。)

目前,在扩展模块中实现了三个 逻辑连接词 和一些规则

非常感谢您在扩展模块中添加更多规则的帮助。

实验性功能

条件验证

实验性的 cow 功能引入了一个新类型,VCow,它代表一个可能是有效或无效的类型 Cow<'_, vstr<_>>。有效性在运行时进行跟踪。

注意vstr 已经支持像这样的 Cow:Cow<'_, vstr<_>>,即使没有 cow 功能。 cow 功能添加了一个实验性的包装类型,该类型跟踪可能在运行时更改的 vstr 的有效性。

功能

  • (默认) serde:启用 serde 支持。将 serde 作为依赖项引入。
  • (默认) ext:启用内置扩展。将 thiserror 作为依赖项引入。
  • (默认) std:启用 std 支持。实现 Error trait 所必需。将 std 作为依赖项引入。
  • (默认) alloc:与 alloc crate 集成,并启用智能指针。
  • cow:启用实验性的 VCow 类型。将 alloc 作为依赖项引入。

依赖项

~110–350KB