proto_rs 0.2.0

Rust-first gRPC macros collection for .proto/protobufs managment and more
docs.rs failed to build proto_rs-0.2.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

Rust as First-Class Citizen for gRPC

This crate provides 3 macros that will handle all proto-related work, so you don't need to touch .proto files at all.

Motivation

  1. I hate to do conversion after conversion for conversion
  2. I love to see Rust only as first-class citizen for all my stuff
  3. I hate bloat, so no protoc (shoutout to PewDiePie debloat trend)
  4. I don't want to touch .proto files at all

Usage

The #[proto_rpc] macro will convert your Rust native trait to tonic and optionally emit .proto file:

#[proto_rpc(rpc_package = "sigma_rpc", rpc_server = true, rpc_client = true, proto_path = "protos/gen_complex_proto/sigma_rpc.proto")]
#[proto_imports(rizz_types = ["BarSub", "FooResponse"], goon_types = ["RizzPing", "GoonPong"] )]
pub trait SigmaRpc {
    type RizzUniStream: Stream<Item = Result<FooResponse, Status>> + Send;
    async fn rizz_ping(&self, request: Request<RizzPing>) -> Result<Response<GoonPong>, Status>;

    async fn rizz_uni(&self, request: Request<BarSub>) -> Result<Response<Self::RizzUniStream>, Status>;
}

Yep, all types here are just Rust types. We can then implement the server like this:

#[tonic::async_trait]
impl SigmaRpc for S {
    type RizzUniStream = Pin<Box<dyn Stream<Item = Result<FooResponse, Status>> + Send>>;
    async fn rizz_ping(&self, _req: Request<RizzPing>) -> Result<Response<GoonPong>, Status> {
        Ok(Response::new(GoonPong {}))
    }
    async fn rizz_uni(&self, _request: Request<BarSub>) -> Result<Response<Self::RizzUniStream>, Status> {
        let (tx, rx) = tokio::sync::mpsc::channel(128);

        tokio::spawn(async move {
            for _ in 0..5 {
                if tx.send(Ok(FooResponse {})).await.is_err() {
                    break;
                }
                tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
            }
        });

        let stream = ReceiverStream::new(rx);
        let boxed_stream: Self::RizzUniStream = Box::pin(stream);

        Ok(Response::new(boxed_stream))
    }
}

This is possible because of this trait, that handles all conversions automagically:

pub trait HasProto {
    type Proto: Clone + prost::Message + PartialEq;
    fn to_proto(&self) -> Self::Proto;
    fn from_proto(proto: Self::Proto) -> Result<Self, Box<dyn std::error::Error>>
    where
        Self: Sized;
}

We can derive it (or manually implement) for most types with #[proto_message] macro:

#[proto_message(proto_path ="protos/gen_proto/goon_types.proto")]
#[derive(Clone, Debug, PartialEq)]
pub struct RizzPing;

But that's not all — #[proto_message] and #[proto_rpc] will also create .proto definitions for non-Rust clients.

Build All .proto Files from Dependencies at once

Pure Rust Black Magic

This crate provides a powerful feature to collect and build .proto files from ALL dependencies that use proto_rs in a single place. This is incredibly useful for building a centralized proto schema from a multi-crate workspace.

Usage

In your build.rs or main.rs (or any crate that has other proto_rs dependent crates):

use proto_rs::schemas::ProtoSchema;

fn main() {
    proto_rs::schemas::write_all("build_protos").expect("Failed to write proto files");
    
    for schema in inventory::iter::<ProtoSchema> {
        println!("Collected: {}", schema.name);
    }
}

This will automatically collect and build all .proto files from all crates in your dependency tree that use proto_rs macros!

Examples

You can see more in examples:

  • proto_gen_example - simple service with streaming (generated .proto saved here: protos/gen_proto)
  • prosto_proto - showcase of type possibilities (generated .proto saved here: protos/showcase_proto)
  • tests/proto_build_test - example of how you can build .proto files only on demand

.proto Auto-Emission Control

Controls auto-emission of .proto files by macros:

  • "emit-proto-files" - cargo feature
  • "PROTO_EMIT_FILE" - env var

.proto Auto-Emission Behavior

Feature Env Var Result
none not set ❌ No emission
none true ✅ Emit files
none false ❌ No emission
emit-proto-files not set ✅ Emit files
emit-proto-files true ✅ Emit files
emit-proto-files false ❌ No emission (override)
build-schemas (any) ✅ Emit const

Proto Dump Macro

You can just dump proto files with (without HasProto impl, helpful for handwritten prost types):

#[proto_dump(proto_path = "protos/proto_dump.proto")]
#[derive(prost::Message, Clone, PartialEq)]
pub struct LamportsProto {
    #[prost(uint64, tag = 1)]
    pub amount: u64,
}

This crate also provides an auxiliary macro #[proto_dump(proto_path ="protos/proto_dump.proto")] that outputs a .proto file. This is helpful for hand-written prost types.

#[proto_dump(proto_path ="protos/proto_dump.proto")]
#[derive(prost::Message, Clone, PartialEq)]
pub struct LamportsProto {
    #[prost(uint64, tag = 1)]
    pub amount: u64,
}

Generated proto:

syntax = "proto3";
package proto_dump;

message Lamports {
    uint64 amount = 1;
}

Advanced Features

Macros support all prost types, imports, skipping with default and custom functions, custom conversions, support for native Rust enums (like Status below) and prost enumerations (TestEnum in this example, see more in prosto_proto).

Struct with Advanced Attributes

#[proto_message(proto_path ="protos/showcase_proto/show.proto")]
pub struct Attr {
    #[proto(skip)]
    id_skip: Vec<i64>,
    id_vec: Vec<String>,
    id_opt: Option<String>,
    #[proto(rust_enum)]
    status: Status,
    #[proto(rust_enum)]
    status_opt: Option<Status>,
    #[proto(rust_enum)]
    status_vec: Vec<Status>,
    #[proto(skip = "compute_hash_for_struct")]
    hash: String,
    #[proto(import_path = "google.protobuf")]
    #[proto(message)]
    timestamp: Timestamp,
    #[proto(message)]
    #[proto(import_path = "google.protobuf")]
    timestamp_vec: Vec<Timestamp>,
    #[proto(message)]
    #[proto(import_path = "google.protobuf")]
    timestamp_opt: Option<Timestamp>,
    #[proto(enum)]
    #[proto(import_path = "google.protobuf")]
    test_enum: TestEnum,
    #[proto(enum)]
    #[proto(import_path = "google.protobuf")]
    test_enum_opt: Option<TestEnum>,
    #[proto(enum)]
    #[proto(import_path = "google.protobuf")]
    test_enum_vec: Vec<TestEnum>,
    #[proto(into = "i64", into_fn = "datetime_to_i64", from_fn = "i64_to_datetime")]
    pub updated_at: DateTime<Utc>,
}

Generated proto:

message Attr {
  repeated string id_vec = 1;
  optional string id_opt = 2;
  Status status = 3;
  optional Status status_opt = 4;
  repeated Status status_vec = 5;
  google.protobuf.Timestamp timestamp = 6;
  repeated google.protobuf.Timestamp timestamp_vec = 7;
  optional google.protobuf.Timestamp timestamp_opt = 8;
  google.protobuf.TestEnum test_enum = 9;
  optional google.protobuf.TestEnum test_enum_opt = 10;
  repeated google.protobuf.TestEnum test_enum_vec = 11;
  int64 updated_at = 12;
}

Complex Enums

#[proto_message(proto_path ="protos/showcase_proto/show.proto")]
pub enum VeryComplex {
    First,
    Second(Address),
    Third {
        id: u64,
        address: Address,
    },
    Repeated {
        id: Vec<u64>,
        address: Vec<Address>,
    },
    Option {
        id: Option<u64>,
        address: Option<Address>,
    },
    Attr {
        #[proto(skip)]
        id_skip: Vec<i64>,
        id_vec: Vec<String>,
        id_opt: Option<String>,
        #[proto(rust_enum)]
        status: Status,
        #[proto(rust_enum)]
        status_opt: Option<Status>,
        #[proto(rust_enum)]
        status_vec: Vec<Status>,
        #[proto(skip = "compute_hash_for_enum")]
        hash: String,
        #[proto(import_path = "google.protobuf")]
        #[proto(message)]
        timestamp: Timestamp,
        #[proto(message)]
        #[proto(import_path = "google.protobuf")]
        timestamp_vec: Vec<Timestamp>,
        #[proto(message)]
        #[proto(import_path = "google.protobuf")]
        timestamp_opt: Option<Timestamp>,
        #[proto(enum)]
        #[proto(import_path = "google.protobuf")]
        test_enum: TestEnum,
        #[proto(enum)]
        #[proto(import_path = "google.protobuf")]
        test_enum_opt: Option<TestEnum>,
        #[proto(enum)]
        #[proto(import_path = "google.protobuf")]
        test_enum_vec: Vec<TestEnum>,
    },
}

Generated proto:

message VeryComplexProto {
  oneof value {
    VeryComplexProtoFirst first = 1;
    Address second = 2;
    VeryComplexProtoThird third = 3;
    VeryComplexProtoRepeated repeated = 4;
    VeryComplexProtoOption option = 5;
    VeryComplexProtoAttr attr = 6;
  }
}

message VeryComplexProtoFirst {}

message VeryComplexProtoThird {
  uint64 id = 1;
  Address address = 2;
}

message VeryComplexProtoRepeated {
  repeated uint64 id = 1;
  repeated Address address = 2;
}

message VeryComplexProtoOption {
  optional uint64 id = 1;
  optional Option address = 2;
}

message VeryComplexProtoAttr {
  repeated string id_vec = 1;
  optional string id_opt = 2;
  Status status = 3;
  optional Status status_opt = 4;
  repeated Status status_vec = 5;
  google.protobuf.Timestamp timestamp = 6;
  repeated google.protobuf.Timestamp timestamp_vec = 7;
  optional google.protobuf.Timestamp timestamp_opt = 8;
  google.protobuf.TestEnum test_enum = 9;
  optional google.protobuf.TestEnum test_enum_opt = 10;
  repeated google.protobuf.TestEnum test_enum_vec = 11;
}

Simple Rust Enum

#[proto_message(proto_path ="protos/showcase_proto/show.proto")]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub enum Status {
    Pending,
    #[default]
    Active,
    Inactive,
    Completed,
}

Generated proto:

enum Status {
  PENDING = 0;
  ACTIVE = 1;
  INACTIVE = 2;
  COMPLETED = 3;
}

Dependencies

Crate pulled dependencies:

01:04:53 ➜ cargo tree
proto_rs v0.1.0
└── prost v0.14.1
    ├── bytes v1.10.1
    └── prost-derive v0.14.1 (proc-macro)
        ├── anyhow v1.0.100
        ├── itertools v0.14.0
        │   └── either v1.15.0
        ├── proc-macro2 v1.0.101
        │   └── unicode-ident v1.0.19
        ├── quote v1.0.41
        │   └── proc-macro2 v1.0.101 (*)
        └── syn v2.0.106
            ├── proc-macro2 v1.0.101 (*)
            ├── quote v1.0.41 (*)
            └── unicode-ident v1.0.19