#prost #grpc #protobuf #tonic #proc-macro

prost-unwrap

prost 生成结构体的验证和类型转换的进程宏

3 个稳定版本

1.1.0 2024年5月1日
1.0.1 2024年4月28日
0.1.5 2024年4月12日

#311数据结构

Apache-2.0

12KB

注意: 此软件包处于开发初期阶段,目前不打算用于生产应用。软件包 API 可能会发生重大更改。

此软件包旨在弥合 gRPC 设计原则与 Rust 数据结构典型方法之间的差距。它自动生成“镜像”数据结构,从 Option<T> 中解包所有必要字段,并为从原始自动生成的结构体提供 TryFrom 实现。

为什么这很重要

随着 protobuf 升级到版本 3,必需字段的观念被淘汰。在 gRPC 的上下文中,这意味着您的消息中的每个嵌套字段默认都是可选的。其理由是将字段验证责任从协议转移到应用级别,以避免必需字段在数据合同演变中引入的复杂性。

Prost 和 Tonic 遵循这些更新的 protobuf 和 gRPC 规范,生成 Rust 数据结构,其中非原始嵌套字段封装在 Option<T> 中。虽然这符合 Rust 的安全和空值特性,但可能很繁琐,特别是在某些字段对于数据结构逻辑一致性地本质上是必需的时。在 Rust 中,首选的范式是在编译时防止无效状态,而 Prost 的自动生成结构体并没有完全实现这一目标,这通常会导致不必要的解包和引用。

提出的解决方案是创建“净化”镜像结构,其中所有基本字段都是直接可访问的,而不是封装在 Option 中。通过实现 TryFrom<OriginalMessage> for MirrorMessage,这些结构遵循 Rust 的设计原则,确保数据完整性并提高代码清晰度。

这种方法的主要挑战是创建和维护这些镜像结构所需的冗长代码。此软件包引入了一个进程宏来消除这些冗长代码,自动生成必要的代码,简化维护,并让您专注于应用程序的逻辑。

快速入门指南

假设我们有一个这样的bar.proto文件,位于你crate中的./proto/foo目录下。

syntax = "proto3";

package foo.bar;

message MsgA {
    int32 f1 = 1;
}

message MsgB {
    MsgA f1 = 1;
    MsgA f2 = 2;
    repeated MsgA f3 = 3;
}

首先,使用prosttonic生成Rust源代码。请参考crates文档以了解构建过程的完整说明。

本快速入门指南将使用prost

首先,为了使用prost-unwrap,您需要指定prost的输出目录。这是因为prost-unwrap需要读取这些文件来生成对应的结构体,而在过程宏展开时环境变量OUT_DIR是不可用的。为了避免提交生成的代码,请在输出目录中添加一个.gitignore文件,并包含*.rs条目。

out_dir调用添加到你的build.rs中的prost构建配置中。tonic-build提供了类似的选项。

use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let inner_proto = Path::new("proto/foo/bar.proto");
    let include_dir = Path::new("proto");

    prost_build::Config::new()
        .out_dir(".proto_out")
        .compile_protos(&[inner_proto], &[include_dir])?;

    Ok(())
}

运行cargo build后,你应该能在输出目录中看到生成的代码(foo.bar.rs文件)。

当使用由prost生成的结构体工作时,需要将生成的代码组织成嵌套模块,以反映你的protobuf包结构。对于上面的例子,你应该将生成的Rust代码封装如下(将所有生成的代码,包括包模块,都封装在generated模块中,以隔离它们)。

pub mod generated {
    pub mod foo {
        pub mod bar {
            include!(".proto/foo.bar.rs"));
        }
    }
}

对于prost-unwrap生成的代码,我们将添加一个具有相似结构的单独模块树。

pub mod unwrapped {
    pub mod foo {
        pub mod bar {
            // insert the prost_unwrap:include! macro call here
        }
    }
}

prost-unwrap提供了一个include!宏来将生成的代码包含到你的源代码中。该宏接受一个方法调用链作为参数。

prost_unwrap:include!(
    with_original_mod(crate::generated)
    .with_this_mod(crate::unwrapped)
    .from_source(foo::bar, ".proto/foo.bar.rs")
    .with_struct(MsgB, [f1])
);

此配置指示prost-unwrap执行以下操作:

  • 从文件".proto/foo.bar.rs"中提取结构体和枚举。
  • crate::generated::foo::bar中复制MsgB结构体,将f1字段从Option<T>转换为T
  • 为所有转移的结构体和枚举生成TryFromInto特质。

有了生成的和展开的代码,你可以执行如下的转换

fn a(msg: crate::unwrapped::foo::bar::MsgB)
-> crate::generated::foo::bar::MsgB
{
    msg.into() // Converts unwrapped struct back to the original form.
}

fn b(msg: crate::generated::foo::bar::MsgB)
-> Result<crate::unwrapped::foo::bar::MsgB, Box<dyn Error>>
{
    msg.try_into()? // Attempts conversion, returning an error if the
                    // 'msg.f1' field is 'None'.
}

查看集成测试目录以找到其他使用案例。注意:ui目录包含负面的(失败的)测试,不要考虑这些! :)

include!宏解析

include!宏接受一个伪代码,形式为方法调用链,作为参数。伪方法调用可以按照任何顺序排列。

调用链必须包含对with_original_modwith_this_modfrom_source的一个调用。同时,至少必须有一个with_structwith_enum

with_original_mod

指定包装模块的绝对路径(以 crate:: 开头),其中包含原始生成的源代码。由于 Rust 过程宏存在一些作用域限制,因此需要此路径。

示例

prost_unwrap:include!(
    with_original_mod(crate::generated)
);
with_this_mod

指定包装模块的绝对路径,其中包含由 prost-unwrap 生成的源代码。

prost_unwrap:include!(
    with_this_mod(crate::unwrapped)
);
from_source

指定源代码位置以及包裹此代码的相对模块路径。相对路径必须在原始代码包装器和展开代码包装器中相同。

在大多数情况下,此路径将与您的 proto 文件中的包名(在我们的情况下为 foo.bar)和文件名(在我们的情况下为 foo.bar.rs)匹配。

prost_unwrap:include!(
    from_source(com::acme, ".proto/com.acme.rs")
);
with_struct

指定需要从 Option<T> 解包到 T 的字段的相对路径列表。

prost_unwrap:include!(
    with_struct(AcmeMessage, [field1, field2, field3])
);
with_enum

指定需要包含在生成的代码中的枚举的相对路径(请参阅“已知问题”部分)。

prost_unwrap:include!(
    with_enum(AcmeEnum)
);

生成的代码

prost-unwrap::include! 将生成以下代码片段(包括复制的结构和枚举)

  • 实现 DebugDisplaystd::error::Error 特性的 Error 结构体。
  • 将原始结构体转换为复制的结构体的辅助函数,如果它们被包裹在 Option<T>(可选字段)或 Vec<T>(重复字段)中。

复制的结构和枚举已删除 prost 相关属性

  • 为结构体提供 Message derive;
  • 为枚举提供 Enumeration derive;
  • 结构和枚举的字段特定属性。

您可以使用 cargo-expand 检查生成的代码。

要实现的功能

  • 部分复制:目前 prost-unwrap 会复制它可以在链接的源代码中找到的所有结构和枚举。如果此子集是自包含的,即子集的成员只引用子集的成员,则可以复制数据结构子集。

  • 项后缀:由于原始复制的结构体具有相同的名称,需要某种方式对结构体进行别名,以便在同一个作用域中同时拥有它们。通过实现后缀,可以自动重命名复制的结构体。

已知问题

  • 无用的 with_enum 选项:由于 prost-unwrap 会复制所有结构和枚举,因此此选项在实现部分复制之前是无用的。
  • 复制的结构和枚举缺少 DebugDefault trait 实现(这些是由 prost MessageEnumeration derive 提供的,这些属性已被删除)。
  • 测试没有涵盖所有可能的用例。

贡献

此 crate 处于开发初期。如果您遇到任何意外的行为、不清晰的文档或其他问题,请随时在 github 上创建问题。

如果您发现了一个错误,请创建一个最小可复现场景(proto 文件加 rust 代码)并将其放入 github 问题中。

如果您足够熟练,足以批评源代码的低效之处,也请这样做。

依赖项

~1–1.4MB
~31K SLoC