宣布Valuable:一个提供对象安全的值检查的类库
在过去的几周里,我们一直在开发Valuable,一个提供对象安全的值检查的新crate。它几乎已经准备好发布了,所以我想我应该写一篇文章来介绍它。这个 crate 提供了一个对象安全的trait–Valuable,它允许调用者在不知道其类型的情况下检查值的内容,无论是字段、枚举变体,还是基础类型。最初,我们写Valuable是为了支持Tracing;然而,它在几种情况下是有帮助的。对象安全的值检查有点拗口,所以让我们先看看Tracing,以及为什么那里需要它。
Tracing 是一个用于检测Rust程序的框架,收集结构化的、基于事件的诊断信息。有些人认为它是一个结构化的日志框架,虽然它可以满足这个用例,但它可以做更多。例如,Console 的目标是成为调试异步Rust应用程序的强大工具,并使用 Tracing 作为其支柱。Tokio 和其他库通过 Tracing 发出仪表信息。Console 将这些事件聚合到应用程序的执行模型中,使开发者能够深入了解错误和其他问题。
仪器化的应用程序发出带有丰富结构化数据的事件,而收集器则接收这些事件。当然,在编译时,被探测的应用程序和事件收集器并不了解对方。一个 trait 对象将仪器化的一半与收集器的一半连接起来,使收集器能够动态地注册自己。因此,将丰富的、结构化的数据从仪器化的一半传递给收集器需要通过 trait 对象的边界传递。今天,Tracing 在最低水平上支持这个,但不支持传递嵌套数据。
让我们来看看一个实际的用例。给定一个HTTP服务,在一个HTTP请求开始的时候,我们想发出一个包括相关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 Headers {
user_agent: String,
host: String,
content_type: ContentType,
accept_encoding: Vec<String>,
}
struct ContentType {
mime: String,
charset: String,
}
我们想把这些数据传递给事件收集器,但如何传递呢?事件收集器不知道 Headers
结构,所以我们不能只是定义一个接收 &Headers
的方法。我们可以使用像 serde_json::Value
这样的类型来传递任意的结构化数据,但这需要从我们应用程序的结构体中分配和复制数据来交给收集器。
Valuable crate的目的就是要解决这个问题。在HTTP头的案例中,首先,我们要为我们的 Headers
类型实现 Valuable
。然后,我们可以将一个 &dyn Valuable
引用传递给事件收集器。采集器可以使用 Valuable
的访问者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[..]);
注意访问者API如何让我们选择检查哪些数据。我们只关心 accept_encoding
值,所以这是我们唯一访问的字段。我们不访问 content_type
字段。
Valuable crate将每个值表示为 Value
枚举的一个实例。原始的 rust 类型被枚举出来,其他类型被分为可结构化(Structable)、可枚举(Enumerable)、可列表(Listable)或可映射(Mappable),由同名的 trait 表示。实现一个结构体或枚举体的 traits 通常是使用程序性宏来完成的;然而,它可能看起来像这样:
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 实现除了原始类型外没有复制任何数据。如果访问者不需要检查子字段,就不需要进一步的工作。
我们期望 Valuable
的作用不仅仅是跟踪。例如,在需要对象安全的时候,它对任何序列化都是有帮助的。Valuable
不是 Serde
的替代品,也不会提供反序列化的API。然而,Valuable
可以补充 Serde
,因为 Serde
的序列化API由于trait的相关类型而不是 trait-object
安全的(erased-serde
的存在可以解决这个问题,但需要为每个嵌套的数据结构分配)。一个 valuable-serde
crate 已经在进行中(感谢 taiki-e),为实现 Valuable 和 Serialize 的类型提供了一个桥梁。为了获得对象安全的序列化,派生 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)方面得到帮助,这些板块为生态系统中常见的数据类型提供宝贵的实现。我们还有很多工作要做,以扩展配置选项和其他功能的派生宏,所以请到Tokio discord服务器上的#valuable频道打招呼。