4 个版本 (2 个重大变更)
0.3.0 | 2023 年 7 月 8 日 |
---|---|
0.2.1 | 2023 年 7 月 8 日 |
0.1.2 | 2023 年 6 月 16 日 |
#1417 in Rust 模式
每月 29 次下载
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.
摘要
- 自带验证,将
vstr<_>
当作str
使用(期望不可变str
引用)。 - 将其混合到您的代码中。 如果您已经有了现有的验证模块,不要修改它。Validus 有多种简单的方法将您的验证包装到
vstr<_>
兼容的规则类型中。 - 开始时走捷径。 如果您没有现有的验证模块,可以使用
fast_rule!
宏快速定义一个带有谓词(闭包或外部函数,可能或可能不属于您的包)的规则类型。您的错误类型将是正确的std::error::Error
类型;甚至可以使用一个 ad-hoc&'static str
错误消息。 - 无需额外代码即可与
serde
一起工作。 简单地将#[derive(Serialize, Deserialize)]
添加到您的结构体中,就像平常一样。现在,无需担心无效的字符串被反序列化。 - 如果您想,可以选择懒验证。 使用一个名为
Later<_>
的特殊规则,您还可以选择是立即验证还是延迟验证,直到您决定需要它的时候。(例如,对于serde
反序列化非常有用。) - 现场合并规则。默认启用扩展提供了三个逻辑连接符(和、或、非);这些在您的序言中,因此您可以立即使用它们。
Validus库提供了一个名为vstr
的新类型,它是正则字符串切片的封装。此vstr
由您的验证规则类型参数化,例如vstr<EmailRule>
。任何实现了ValidateString
特质的零大小类型都可以用作规则。
vstr
旨在在某些上下文中替代str
。因此,
vstr
实现了与其它规则vstr
和str
相同的Eq
,- ...以及与任何其他
str
引用相同的Ord
和Hash
。 vstr
了解String
、Cow
以及智能指针类型,例如Box
、Rc
和Arc
。
为了向您展示vstr<_>
如何与str
引用相同地进行比较和哈希,我将给出一个直接使用vstr
作为HashMap
和HashSet
中的键的示例。
// 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
不是 unsafe
: vstr
对字符串切片的有效性不提供任何额外保证,超出 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
特性门控,默认启用。)
目前,在扩展模块中实现了三个 逻辑连接词 和一些规则
Conj
,它要求两个规则都满足。Disj
,它要求至少满足两个规则中的一个。Neg
,它要求一个规则不被满足。StringSizeRule
(及其变体),它检查字符串的大小。StringAsciiRule
,我们迄今为止唯一的检查字符串内容的规则。
非常感谢您在扩展模块中添加更多规则的帮助。
实验性功能
条件验证
实验性的 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