宣布 Valuable,一个用于对象安全值检查的库

2021 年 5 月 24 日

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

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

Instrumentation -> trait object -> event collection

检测后的应用程序发出包含丰富结构化数据的事件,收集器接收这些事件。当然,在编译时,检测后的应用程序和事件收集器彼此不了解。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),它提供了实现 ValuableSerialize 的类型之间的桥梁。为了获得对象安全的序列化,可以 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 频道中打个招呼。