2个不稳定版本
0.2.0 | 2021年7月24日 |
---|---|
0.1.0 | 2021年7月21日 |
#9 in #yourself
15KB
112 行
check_mate
在自我毁灭之前检查自己。
A small Rust utility library inspired by the ideas of "Parse, don't validate"1 and its follow-up, "Names are not type safety"2. Its goal is to extend the idea to checking invariants more generally. See the crate documentation for more information.
lib.rs
:
在自我毁灭之前检查自己。
This is a small utility library inspired by the ideas of "Parse, don't validate"1 and its follow-up, "Names are not type safety"2. Its goal is to extend the idea to checking invariants more generally.
动机示例
The motivating use-case for this crate was validating signed messages. Consider a Signed
struct like the following
struct Signed {
payload: Vec<u8>,
public_key: PublicKey,
signature: Signature,
}
The struct contains a payload, a public key, and a signature. Let's give the struct a validate
method that we could use to check for validity
impl Signed {
fn validate(&self) -> Result<(), Error> {
self.public_key.verify(&self.payload, &self.signature)
}
}
Now when we find a Signed
we're able to verify it. Of course, whenever we see a Signed
in our code, it may not immediately be clear whether it has been checked yet. In particular, if Signed
appears in another struct, or as a signature to some method, has it already been checked? Should we check it anyway?
It's possible to manage this with disciplined use of documentation and convention, making it clear where signatures should be validated and relying on that being the case later in the call stack. However discipline is not always a reliable tool, particularly in an evolving codebase with multiple contributors. Perhaps we can do something better?
Parse, don't validate
This is where the ideas from "Parse, don't validate" come in. Specifically, rather than validating a Signed
instance, we could 'parse' it into something else, such as CheckedSigned
/// A [`Signed`] that has been checked and confirmed to be valid.
struct CheckedSigned(Signed);
impl CheckedSigned {
fn try_from(signed: Signed) -> Result<Self, Error> {
signed.public_key.verify(&signed.payload, &signed.signature)?;
Ok(Self(signed))
}
}
通过将其自己的模块中的CheckedSigned
保持为私有字段,我们可以保证唯一构建它的方式是通过执行检查的try_from
方法。这意味着结构和函数可以使用CheckedSigned
并安全地假设签名是有效的。
fn process_message(message: CheckedSigned) {
/* ... */
}
// Or
struct ProcessMessage {
message: CheckedSigned,
}
在这两种情况下,message
已经被检查,并且已知是有效的,这一点是立即清晰的。
到目前为止一切顺利,但是由于CheckedSigned
的字段是私有的,我们失去了对内部值的直接访问。Rust通过为CheckedSigned
实现Deref
来简化这里的某些功能。
impl core::ops::Deref for CheckedSigned {
type Target = Signed;
fn deref(&self) -> &Self::Target {
&self.0
}
}
这允许直接在CheckedSigned
实例上调用具有&self
接收器的Signed
方法。
那么这个库呢...?
为需要检查的每个类型创建一个Checked*
新类型将会产生很多样板代码,而且有很多种方法可以解决这个问题。可以说,check_mate
的存在就是为了提供一种一致的模式,并尽可能地减少样板代码。
如何使用
让我们再次从上面的原始Signed
结构体开始
struct Signed {
payload: Vec<u8>,
public_key: PublicKey,
signature: Signature,
}
我们可以通过实现Check
来使用check_mate
,以实现与CheckedSigned
相同的保证。
impl check_mate::Check for Signed {
type Ok = Self;
type Err = Error;
fn check(self) -> Result<Self::Ok, Self::Err> {
self.public_key.verify(&self.payload, &self.signature)?;
Ok(self)
}
}
现在我们可以使用try_from
获取Checked
<Signed>
#
let _ = check_mate::Checked::try_from(signed);
Checked<T>
实现了Deref<Target = T>
,可以使用into_inner
将其转换回内部值。
如果启用了serde
功能,并且T: Serialize
,则Checked<T>
还将实现Serialize
;如果T: Deserialize
并且存在用于检查的Check<Ok = T>
实现,则还将实现Deserialize
。由于未受限制的类型参数限制,无法为任何U: Check<Ok = T>
实现通用的Deserialize
实现——它必须是T
本身)。
何时(或不)使用此功能
希望check_mate
对于开始使用这种'解析'风格的维护不变性以及对于内部API(其中变更的可能性很大,因此希望减少涉及的代码量)将是有用的。
然而,Checked<T>
无法像自定义检查类型那样方便或功能丰富。例如,如果知道某些字段不影响有效性,则可以将它们设置为公共的,或者不影响的验证性的方法可以接收&mut self
。由于内部值只以不可变的方式暴露,这两种情况都不可能通过Checked<T>
实现。
当您想要对如何执行验证进行大量定制时,这可能会不适用。这可以通过以下方式实现:在实现 Check
的类型中包含配置,或者在可以定制行为的包装器上实现 Check
,但使用起来可能会略显笨拙。
最后,正如“名称不是类型安全”一节中讨论的那样,设计无法表示无效状态的类型始终是首选,尽管这并不总是可能的。
接下来是什么?
我想尝试使用它来了解它是否真正有用,以及痛点在哪里。我可以想象添加一些东西
- 实现额外的常见特质(
AsRef<T>
,Borrow<T>
)。 - 实现额外的常见间接方法(
as_deref
,cloned
)。
依赖项
~170KB