Skip to main content

Crate protto

Crate protto 

Source
Expand description

§protto

Derive seamless conversions between prost-generated Protobuf types and custom Rust types.

§Overview

protto is a procedural macro for automatically deriving efficient, bidirectional conversions between Protobuf types generated by prost and your native Rust structs. This macro will significantly reduce boilerplate when you’re working with Protobufs.

§Features

  • Automatic Bidirectional Conversion: Derives From<Proto> and Into<Proto> implementations.
  • Primitive Type Support: Direct mapping for Rust primitive types (u32, i64, String, etc.).
  • Option and Collections: Supports optional fields (Option<T>) and collections (Vec<T>).
  • Newtype Wrappers: Transparent conversions for single-field tuple structs.
  • Field Renaming: Customize mapping between Rust and Protobuf field names using #[protto(proto_name = "...")].
  • Custom Conversion Functions: Handle complex scenarios with user-defined functions via #[protto(from_proto_fn = "...")] and #[protto(to_proto_fn = "...")].
  • Ignored Fields: Exclude fields from conversion using #[protto(ignore)].
  • Advanced Error Handling: Support for custom error types and functions.
  • Smart Optionality Detection: Automatic inference with manual override capabilities.
  • Configurable Protobuf Module: Defaults to searching for types in a proto module, customizable per struct or globally.

§Basic Usage

Given Protobuf definitions compiled with prost:

syntax = "proto3";
package service;

message Track {
    uint64 track_id = 1;
}

message State {
    repeated Track tracks = 1;
}

Derive conversions in Rust:

use protto::Protto;

mod proto {
    tonic::include_proto!("service");
}

#[derive(Protto)]
#[protto(module = "proto")]
pub struct Track {
    #[protto(transparent, proto_name = "track_id")]
    pub id: TrackId,
}

#[derive(Protto)]
pub struct TrackId(u64);

#[derive(Protto)]
pub struct State {
    pub tracks: Vec<Track>,
}

§Attribute Reference

§Struct-Level Attributes

§#[protto(module = "path")]

Specifies the module path where protobuf types are located.

#[derive(Protto)]
#[protto(module = "my_proto")]  // looks for types in my_proto::*
struct MyStruct { ... }
§#[protto(proto_name = "ProtoName")]

Maps the struct to a different protobuf type name.

#[derive(Protto)]
#[protto(proto_name = "StateMessage")]  // maps to proto::StateMessage
struct State { ... }
§#[protto(error_type = ErrorType)]

Sets the error type for conversions that can fail.

#[derive(Protto)]
#[protto(error_type = MyError)]
struct MyStruct { ... }
§#[protto(error_fn = "function_name")]

Specifies a function to handle conversion errors at the struct level.

§Field-Level Attributes

§#[protto(transparent)]

For newtype wrappers - directly converts the inner value.

#[derive(Protto)]
struct UserId(#[protto(transparent)] u64);
§#[protto(proto_name = "proto_field_name")]

Maps the field to a different name in the protobuf.

#[protto(proto_name = "user_id")]
pub id: u64,  // maps to proto.user_id
§#[protto(ignore)]

Excludes the field from proto conversion (uses Default::default).

#[protto(ignore)]
pub runtime_data: HashMap<String, String>,
§Custom Conversion Functions
§#[protto(from_proto_fn = "function")]

Uses a custom function for proto → rust conversion.

#[protto(from_proto_fn = "parse_timestamp")]
pub created_at: DateTime<Utc>,
§#[protto(to_proto_fn = "function")]

Uses a custom function for rust → proto conversion.

#[protto(to_proto_fn = "format_timestamp")]
pub created_at: DateTime<Utc>,

Both can be combined for bidirectional custom conversion:

#[protto(from_proto_fn = "from_proto_map", to_proto_fn = "to_proto_map")]
pub metadata: HashMap<String, Value>,
§Optionality Control
§#[protto(proto_optional)]

Explicitly treats the proto field as optional.

#[protto(proto_optional)]
pub field: String,  // proto field is Option<String>, gets unwrapped
§#[protto(proto_required)]

Explicitly treats the proto field as required.

#[protto(proto_required)]
pub field: Option<String>,  // proto field is String, gets wrapped
§Error Handling
§#[protto(expect)]

Uses .expect() with panic on missing optional fields.

#[protto(expect)]
pub required_field: String,  // panics if proto field is None
§#[protto(error_type = ErrorType)]

Field-level error type override.

§#[protto(error_fn = "function")]

Custom error handling function for this field.

#[protto(expect, error_fn = "handle_missing_field")]
pub critical_field: String,
§Default Values
§#[protto(default)]

Uses Default::default() for missing/empty fields.

#[protto(default)]
pub optional_field: String,  // empty string if proto field is None
§#[protto(default_fn = "function")]

Uses a custom function for default values.

#[protto(default_fn = "default_username")]
pub username: String,

fn default_username() -> String {
    "anonymous".to_string()
}

§Advanced Examples

§Complex conversions with custom functions

use std::collections::HashMap;

#[derive(Protto)]
#[protto(proto_name = "State")]
pub struct StateMap {
    #[protto(from_proto_fn = "into_map", to_proto_fn = "from_map")]
    pub tracks: HashMap<TrackId, Track>,
}

pub fn into_map(tracks: Vec<proto::Track>) -> HashMap<TrackId, Track> {
    tracks.into_iter().map(|t| (TrackId(t.track_id), t.into())).collect()
}

pub fn from_map(tracks: HashMap<TrackId, Track>) -> Vec<proto::Track> {
    tracks.into_values().map(Into::into).collect()
}

§Ignoring runtime fields

use std::sync::atomic::AtomicU64;

#[derive(Protto)]
#[protto(proto_name = "State")]
pub struct ComplexState {
    pub tracks: Vec<Track>,
    #[protto(ignore)]
    pub counter: AtomicU64,
    #[protto(ignore, default = "default_cache")]
    pub cache: HashMap<String, String>,
}

fn default_cache() -> HashMap<String, String> {
    HashMap::with_capacity(100)
}

§Handling enums

enum Status {
    STATUS_OK = 0;
    STATUS_MOVED_PERMANENTLY = 1;
    STATUS_FOUND = 2;
    STATUS_NOT_FOUND = 3;
}

message StatusResponse {
    Status status = 1;
    string message = 2;
}
// Enum variant names are automatically mapped (prefix removal supported)
#[derive(Protto)]
pub enum Status {
    Ok,              // maps to STATUS_OK
    MovedPermanently, // maps to STATUS_MOVED_PERMANENTLY
    Found,           // maps to STATUS_FOUND
    NotFound,        // maps to STATUS_NOT_FOUND
}

#[derive(Protto)]
pub struct StatusResponse {
    pub status: Status,
    pub message: String,
}

§Error handling strategies

#[derive(Protto)]
#[protto(error_type = ConversionError)]
pub struct User {
    // Panic on missing field
    #[protto(expect)]
    pub id: UserId,

    // Use default value
    #[protto(default)]
    pub name: String,

    // Custom error handling
    #[protto(error_fn = handle_email_error)]
    pub email: String,

    // Custom default function
    #[protto(default_fn = default_role)]
    pub role: UserRole,
}

fn handle_email_error() -> ConversionError {
    ConversionError::MissingEmail
}

fn default_role() -> UserRole {
    UserRole::Guest
}

§Optionality Handling

The macro automatically detects and handles optionality between proto and Rust types:

Proto TypeRust TypeBehavior
string field = 1;StringDirect assignment
optional string field = 1;Option<String>Direct assignment
optional string field = 1;StringUse #[protto(expect)] or #[protto(default)]
string field = 1;Option<String>Wrapped in Some()
repeated string items = 1;Vec<String>Direct conversion
repeated string items = 1;Option<Vec<String>>None for empty, Some(vec) otherwise

§Type Support

§Primitives

All Rust primitives are supported: bool, u32, u64, i32, i64, f32, f64, String

§Collections

  • Vec<T>repeated T
  • Option<Vec<T>>repeated T (with empty handling)
  • Custom collections via from_proto_fn/to_proto_fn

§Optional Types

  • Option<T>optional T
  • Automatic unwrapping/wrapping with error handling

§Custom Types

  • Any type implementing From/Into traits
  • Newtype wrappers with #[protto(transparent)]
  • Custom conversion functions

§Enums

  • Rust enums ↔ proto enums (with automatic prefix handling)
  • Custom discriminant handling

§Error Handling

The macro supports multiple error handling strategies:

  1. Panic: Use #[protto(expect)] - panics with descriptive messages
  2. Default Values: Use #[protto(default)] - provides sensible defaults
  3. Custom Errors: Use #[protto(expect, error_type = T, error_fn = "f")] - custom error handling
  4. Result Types: Generated TryFrom implementations for fallible conversions

§Limitations

  • Assumes Protobuf-generated types live in a single module (configurable).
  • Optional Protobuf message fields use .expect and panic if missing (unless configured otherwise).
  • Complex nested generics may require custom conversion functions.
  • Recursive types require careful handling of conversion cycles.

§Best Practices

  1. Let the macro infer: Start without attributes and add them only when needed.
  2. Use transparent for newtypes: #[protto(transparent)] for simple wrapper types.
  3. Handle errors appropriately: Choose between panic, default, or custom error strategies.
  4. Test edge cases: Verify behavior with None/empty values and boundary conditions.
  5. Document custom functions: Make conversion logic clear for maintenance.

Derive Macros§

Protto