宣布 Valuable,一个用于对象安全值检查的库
2021 年 5 月 24 日
在过去的几周里,我们一直在开发 Valuable,一个新的 crate,它提供了 对象安全 的值检查。它几乎准备好发布了,所以我认为应该写一篇文章来介绍它。这个 crate 提供了一个对象安全的 trait,Valuable
,它允许调用者检查值的内部内容,无论是字段、枚举变体还是原始类型,而无需知道其类型。最初,我们编写 Valuable 是为了支持 Tracing;然而,它在多种场景下都很有用。对象安全的值检查有点拗口,所以让我们首先看看 Tracing 以及为什么在那里需要它。
Tracing 是一个用于检测 Rust 程序以收集结构化、基于事件的诊断信息的框架。有些人认为它是一个结构化日志框架,虽然它可以满足该用例,但它可以做更多的事情。例如,Console 旨在成为一个强大的异步 Rust 应用程序调试工具,并使用 Tracing 作为其骨干。Tokio 和其他库通过 Tracing 发射检测信息。Console 将事件聚合到应用程序执行的模型中,使开发人员能够深入了解错误和其他问题。
检测后的应用程序发出包含丰富结构化数据的事件,收集器接收这些事件。当然,在编译时,检测后的应用程序和事件收集器彼此不了解。trait 对象将检测的一半与收集的一半连接起来,使收集器能够动态注册自身。因此,要将丰富的结构化数据从检测的一半传递到收集器,需要通过 trait 对象边界传递它。Tracing 今天以最小的级别支持这一点,但不支持传递嵌套数据。
让我们看一个实际的用例。给定一个 HTTP 服务,在 HTTP 请求开始时,我们希望发出一个 tracing 事件,其中包含相关的 HTTP 标头。数据可能看起来像这样。
{
user_agent: "Mozilla/4.0 (compatible; MSIE5.01; Windows NT)",
host: "www.example.com",
content_type: {
mime: "text/xml",
charset: "utf-8",
},
accept_encoding: ["gzip", "deflate"],
}
在应用程序中,一个 Rust struct 存储标头。
struct Headers {
user_agent: String,
host: String,
content_type: ContentType,
accept_encoding: Vec<String>,
}
struct ContentType {
mime: String,
charset: String,
}
我们想将此数据传递给事件收集器,但如何传递?事件收集器不知道 Headers
struct,所以我们不能只定义一个接受 &Headers
的方法。我们可以使用像 serde_json::Value
这样的类型来传递任意结构化数据,但这将需要分配和复制数据,从我们的应用程序的 struct 复制到 collector。
Valuable crate 旨在解决这个问题。在 HTTP 标头的情况下,首先,我们将为我们的 Headers
类型实现 Valuable
。然后,我们可以将 &dyn Valuable
引用传递给事件收集器。收集器可以使用 Valuable 的 visitor API 来检查值并提取与其用例相关的数据。
// Visit the root of the Headers struct. This visitor will find the
// `accept_encoding` field on `Headers` and extract the contents. All other
// fields are ignored.
struct VisitHeaders {
/// The extracted `accept-encoding` header values.
accept_encoding: Vec<String>,
}
// Visit the `accept-encoding` `Vec`. This visitor iterates the items in
// the list and pushes it into its `accept_encoding` vector.
struct VisitAcceptEncoding<'a> {
accept_encoding: &'a mut Vec<String>,
}
impl Visit for VisitHeaders {
fn visit_value(&mut self, value: Value<'_>) {
// We expect a `Structable` representing the `Headers` struct.
match value {
// Visiting the struct will call `visit_named_fields`.
Value::Structable(v) => v.visit(self),
// Ignore other patterns
_ => {}
}
}
fn visit_named_fields(&mut self, named_values: &NamedValues<'_>) {
// We only care about `accept_encoding`
match named_values.get_by_name("accept_encoding") {
Some(Value::Listable(accept_encoding)) => {
// Create the `VisitAcceptEncoding` instance to visit
// the items in `Listable`.
let mut visit = VisitAcceptEncoding {
accept_encoding: &mut self.accept_encoding,
};
accept_encoding.visit(&mut visit);
}
_ => {}
}
}
}
// Extract the "accept-encoding" headers
let mut visit = VisitHeaders { accept_encoding: vec![] };
valuable::visit(&my_headers, &mut visit);
assert_eq!(&["gzip", "deflate"], &visit.accept_encoding[..]);
请注意 visitor API 如何让我们选择要检查的数据。我们只关心 accept_encoding
值,所以这是我们访问的唯一字段。我们不访问 content_type
字段。
Valuable crate 将每个值表示为 Value
枚举 的一个实例。原始 rust 类型被枚举,其他类型被分类为 Structable、Enumerable、Listable 或 Mappable,由同名的 trait 表示。struct 或 enum trait 的实现通常使用过程宏完成;然而,它可能看起来像这样。
static FIELDS: &[NamedField<'static>] = &[
NamedField::new("user_agent"),
NamedField::new("host"),
NamedField::new("content_type"),
NamedField::new("accept_encoding"),
];
impl Valuable for Headers {
fn as_value(&self) -> Value<'_> {
Value::Structable(self)
}
fn visit(&self, visit: &mut dyn Visit) {
visit.visit_named_fields(&NamedValues::new(
FIELDS,
&[
Value::String(&self.user_agent),
Value::String(&self.host),
Value::Structable(&self.content_type),
Value::Listable(&self.accept_encoding),
]
));
}
}
impl Structable for Headers {
fn definition(&self) -> StructDef<'_> {
StructDef::new_static("Headers", Fields::Named(FIELDS))
}
}
请注意,除了原始类型之外,visit 实现如何不复制任何数据。如果 visitor 不需要检查子字段,则无需进一步操作。
我们期望 Valuable 不仅对 Tracing 有用。例如,当需要对象安全时,它对任何序列化都很有帮助。Valuable 不是 Serde 的替代品,也不会提供反序列化 API。但是,Valuable 可以作为 Serde 的补充,因为 Serde 的序列化 API 由于 trait 的 关联类型 而不是 trait 对象安全 的(erased-serde 的存在是为了解决这个问题,但需要为每个嵌套数据结构分配内存)。一个 valuable-serde
crate 已经在 开发中(感谢 taiki-e),它提供了实现 Valuable
和 Serialize
的类型之间的桥梁。为了获得对象安全的序列化,可以 derive Valuable
而不是 Serialize
,并序列化 Valuable
trait 对象。
作为另一个潜在的用例,Valuable 可以在渲染模板时有效地提供数据。模板引擎必须在渲染模板时按需访问数据字段。例如,Handlebars crate 当前使用 serde_json::Value
作为渲染时的参数类型,这要求调用者将数据复制到 serde_json::Value
实例中。相反,如果 Handlebars 使用 Valuable,则可以跳过复制步骤。
现在我们需要您试用一下 Valuable,并告知我们它是否满足您的用例。由于 Tracing 1.0 将依赖于 Valuable,我们希望在 2022 年初稳定发布 Valuable 的 1.0 版本。这并没有给我们太多时间,因此我们需要尽快找到 API 漏洞。尝试使用 Valuable 编写库,特别是模板引擎或本文暗示的其他用例。我们也可以在“桥接” crate 方面提供帮助(例如 valuable-http
),它们为常见的生态系统数据类型提供 Valuable 实现。还有很多工作要做,以扩展带有配置选项和其他功能的 derive 宏,所以请在 Tokio discord 服务器 上的 #valuable
频道中打个招呼。