-
Notifications
You must be signed in to change notification settings - Fork 585
Description
Motivation
buf has a ENUM_ZERO_VALUE_SUFFIX lint rule, which requires this zero value to have a specific suffix, like _UNSPECIFIED.
I agree with this practice, as it provides a safe default value if a field is not explicitly set.
The Problem in Rust
Currently, prost generates a Rust enum that includes this _UNSPECIFIED variant. For example, STATUS_UNSPECIFIED becomes Status::Unspecified.
While correct, this is often un-idiomatic and cumbersome in Rust. Having a concrete Unspecified variant forces developers to write boilerplate match arms to handle a case that, in most business logic, should be filtered out during validation.
// Current situation requires handling the zero-value case explicitly
match message.status() { // status() returns Status
Status::Active => { /* do something */ },
Status::Inactive => { /* do something else */ },
Status::Unspecified => unreachable!("should not pass validation"),
}Proposed Solution
I propose a new configuration option in prost-build that would allow prost to treat these suffixed zero-value enum variants as None.
When this option is configured (e.g., config.enum_zero_value_suffix("_UNSPECIFIED")), prost-build would change its generation strategy for matching enums:
- Omit the Variant: The zero-value variant (e.g.,
Unspecified) would be omitted from the generated Rustenumdefinition. - Use
Option<T>: Any method of a message that corresponding to the enum field returnsOption<MyEnum>. - Map Zero to
None: a0value for the enum field would be coded asNone.
This would make the resulting Rust code much more ergonomic and idiomatic.
Example
1. Protobuf Definition (.proto)
syntax = "proto3";
message MyMessage {
Status status = 1;
}
enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_ACTIVE = 1;
STATUS_INACTIVE = 2;
}2. Current prost Output
#[derive(Clone, Copy, Debug, PartialEq, Eq, /* ... */)]
#[repr(i32)]
pub enum Status {
Unspecified = 0, // This variant is present
Active = 1,
Inactive = 2,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct MyMessage {
#[prost(enumeration="Status", tag="1")]
pub status: i32, // Field is i32, requires manual conversion and handling of 0
}
impl MyMessage {
pub fn status() -> Status { ... }
}3. Desired prost Output (with this feature enabled)
// The Unspecified variant is gone
#[derive(Clone, Copy, Debug, PartialEq, Eq, /* ... */)]
#[repr(i32)]
pub enum Status {
Active = 1,
Inactive = 2,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct MyMessage {
#[prost(enumeration="Status", tag="1")]
pub status: i32,
}
impl MyMessage {
pub fn status() -> Option<Status> { ... }
}Now, when I need to process an incoming MyMessage, I now can call status(), responds an invalid argument error if it is None, then passing Status around, not needing to worrying about unspecified case when matching.
Benefits
- Idiomatic Rust: Aligns Protobuf's concept of a default/unset enum value with Rust's
Option. - Reduced Boilerplate: Eliminates the need for
matcharms for_UNSPECIFIEDvariants. - Improved Ergonomics: Working with these enums becomes much more natural for Rust developers.