Macros for convenient serialization of Rust data structures into/from Protocol Buffers.
This is a fork of exonum-derive with some changes to allow easier integration with other projects, and some new features.
First, add the dependency in Cargo.toml
:
protobuf-convert = "0.4.0"
Then, define a ProtobufConvert
trait:
trait ProtobufConvert {
/// Type of the protobuf clone of Self
type ProtoStruct;
/// Struct -> ProtoStruct
fn to_pb(&self) -> Self::ProtoStruct;
/// ProtoStruct -> Struct
fn from_pb(pb: Self::ProtoStruct) -> Result<Self, Error>;
}
And to use it, import the trait and the macro:
For example, given the following protobuf:
message Ping {
fixed64 nonce = 1;
}
rust-protobuf will generate the following struct:
#[derive(PartialEq,Clone,Default)]
#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
pub struct Ping {
// message fields
pub nonce: u64,
// special fields
#[cfg_attr(feature = "with-serde", serde(skip))]
pub unknown_fields: ::protobuf::UnknownFields,
#[cfg_attr(feature = "with-serde", serde(skip))]
pub cached_size: ::protobuf::CachedSize,
}
We may want to convert that struct into a more idiomatic one, and derive more traits. This is the necessary code:
// Import trait
use crate::proto::ProtobufConvert;
// Import macro
use protobuf_convert::ProtobufConvert;
// Import module autogenerated by protocol buffers
use crate::proto::schema;
#[derive(ProtobufConvert)]
#[protobuf_convert(source = "schema::Ping")]
struct Ping {
nonce: u64,
}
Note that the ProtobufConvert
trait must be implemented for all the fields,
see an example implementation for u64
:
impl ProtobufConvert for u64 {
type ProtoStruct = u64;
fn to_pb(&self) -> Self::ProtoStruct {
*self
}
fn from_pb(pb: Self::ProtoStruct) -> Result<Self, Error> {
Ok(pb)
}
}
Now, converting between Ping
and schema::Ping
can be done effortlessly.
A more complex example, featuring enums:
message Ping {
fixed64 nonce = 1;
}
message Pong {
fixed64 nonce = 1;
}
message Message {
oneof kind {
Ping Ping = 1;
Pong Pong = 2;
}
}
#[derive(ProtobufConvert)]
#[protobuf_convert(source = "schema::Ping")]
struct Ping {
nonce: u64,
}
#[derive(ProtobufConvert)]
#[protobuf_convert(source = "schema::Pong")]
struct Pong {
nonce: u64,
}
#[derive(ProtobufConvert)]
#[protobuf_convert(source = "schema::Message")]
enum Message {
Ping(Ping),
Pong(Pong),
}
And it just works!
You can also generate From
and TryFrom
traits for enum variants. Note that this will not work if enum has variants
with the same field types. To use this feature add impl_from_trait
attribute.
#[derive(ProtobufConvert)]
#[protobuf_convert(source = "schema::Message"), impl_from_trait]
enum Message {
Ping(Ping),
Pong(Pong),
}
From<Ping>
, From<Pong>
and also TryFrom<..>
traits will be generated.
Another attribute that can be used with enum is rename
. It instructs macro to generate methods with case
specified in attribute param.
#[derive(ProtobufConvert)]
#[protobuf_convert(source = "schema::Message"), rename(case = "snake_case")]
enum Message {
Ping(Ping),
Pong(Pong),
}
Currently, only snake case is supported.
This macro also supports skipping fields in struct
s so they are ignored when serializing, i.e they will not be mapped to any field in the schema:
#[derive(ProtobufConvert)]
#[protobuf_convert(source = "schema::Ping")]
struct Ping {
pub nonce: u64,
#[protobuf_convert(skip)]
my_private_field: u64
}
Note that you can only skip fields whose type implements the Default
trait.
This macro also supports serde-like attribute with
for modules with the custom implementation of from_pb
and to_pb
conversions.
protobuf-convert
will use functions $module::from_pb
and $module::to_pb
instead of ProtobufConvert
trait for the specified field.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum CustomId {
First = 5,
Second = 15,
Third = 35,
}
#[derive(Debug, Clone, ProtobufConvert, Eq, PartialEq)]
#[protobuf_convert(source = "proto::SimpleMessage")]
struct CustomMessage {
#[protobuf_convert(with = "custom_id_pb_convert")]
id: Option<CustomId>,
name: String,
}
mod custom_id_pb_convert {
use super::*;
pub(super) fn from_pb(pb: u32) -> Result<Option<CustomId>, anyhow::Error> {
match pb {
0 => Ok(None),
5 => Ok(Some(CustomId::First)),
15 => Ok(Some(CustomId::Second)),
35 => Ok(Some(CustomId::Third)),
other => Err(anyhow::anyhow!("Unknown enum discriminant: {}", other)),
}
}
pub(super) fn to_pb(v: &Option<CustomId>) -> u32 {
match v {
Some(id) => *id as u32,
None => 0,
}
}
}