宣布Valuable:一个提供对象安全的值检查的类库

Valuable允许调用者在不知道其类型的情况下检查值的内容,无论是字段、枚举变体,还是基础类型

在过去的几周里,我们一直在开发Valuable,一个提供对象安全的值检查的新crate。它几乎已经准备好发布了,所以我想我应该写一篇文章来介绍它。这个 crate 提供了一个对象安全的trait–Valuable,它允许调用者在不知道其类型的情况下检查值的内容,无论是字段、枚举变体,还是基础类型。最初,我们写Valuable是为了支持Tracing;然而,它在几种情况下是有帮助的。对象安全的值检查有点拗口,所以让我们先看看Tracing,以及为什么那里需要它。

Tracing 是一个用于检测Rust程序的框架,收集结构化的、基于事件的诊断信息。有些人认为它是一个结构化的日志框架,虽然它可以满足这个用例,但它可以做更多。例如,Console 的目标是成为调试异步Rust应用程序的强大工具,并使用 Tracing 作为其支柱。Tokio 和其他库通过 Tracing 发出仪表信息。Console 将这些事件聚合到应用程序的执行模型中,使开发者能够深入了解错误和其他问题。

YJIrHK2

仪器化的应用程序发出带有丰富结构化数据的事件,而收集器则接收这些事件。当然,在编译时,被探测的应用程序和事件收集器并不了解对方。一个 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频道打招呼。

内容出处: https://tokio.rs/blog/2021-05-valuable