3 个稳定版本
1.1.0 | 2024年5月1日 |
---|---|
1.0.1 | 2024年4月28日 |
0.1.5 |
|
#311 在 数据结构
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;
}
首先,使用prost或tonic生成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
。 - 为所有转移的结构体和枚举生成
TryFrom
和Into
特质。
有了生成的和展开的代码,你可以执行如下的转换
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_mod
、with_this_mod
和from_source
的一个调用。同时,至少必须有一个with_struct
或with_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!
将生成以下代码片段(包括复制的结构和枚举)
- 实现
Debug
、Display
和std::error::Error
特性的Error
结构体。 - 将原始结构体转换为复制的结构体的辅助函数,如果它们被包裹在
Option<T>
(可选字段)或Vec<T>
(重复字段)中。
复制的结构和枚举已删除 prost 相关属性
- 为结构体提供
Message
derive; - 为枚举提供
Enumeration
derive; - 结构和枚举的字段特定属性。
您可以使用 cargo-expand 检查生成的代码。
要实现的功能
-
部分复制:目前
prost-unwrap
会复制它可以在链接的源代码中找到的所有结构和枚举。如果此子集是自包含的,即子集的成员只引用子集的成员,则可以复制数据结构子集。 -
项后缀:由于原始复制的结构体具有相同的名称,需要某种方式对结构体进行别名,以便在同一个作用域中同时拥有它们。通过实现后缀,可以自动重命名复制的结构体。
已知问题
- 无用的
with_enum
选项:由于prost-unwrap
会复制所有结构和枚举,因此此选项在实现部分复制之前是无用的。 - 复制的结构和枚举缺少
Debug
和Default
trait 实现(这些是由 prostMessage
和Enumeration
derive 提供的,这些属性已被删除)。 - 测试没有涵盖所有可能的用例。
贡献
此 crate 处于开发初期。如果您遇到任何意外的行为、不清晰的文档或其他问题,请随时在 github 上创建问题。
如果您发现了一个错误,请创建一个最小可复现场景(proto 文件加 rust 代码)并将其放入 github 问题中。
如果您足够熟练,足以批评源代码的低效之处,也请这样做。
依赖项
~1–1.4MB
~31K SLoC