-
Notifications
You must be signed in to change notification settings - Fork 507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A more ergonomic enumeration representation #276
Comments
@sfackler are you aware that type-safe getters are generated for enum fields? For instance: https://docs.rs/prost-types/0.6.1/prost_types/struct.Type.html#method.syntax |
I do like this idea, though. It definitely has some merit. My experience has been that working with enums through the typesafe getter/setter is 'good enough', but they have a discoverability problem. I don't think they are actually documented anywhere other than the resulting rustdoc. |
Ooh, I did not know about those! |
I'd be happy to build out the implementation, but I'd be interested on your thoughts on how to structure it. 0.6 is pretty new, so this could just be a breaking change and prost bumps to 0.7, or it could end up as a new flag in prost_build::Config to allow users to opt into the new approach. |
@sfackler @danburkert How's this going? |
I'm going to work on this over the weekend. |
For an enum: enum SomeEnum {
SOME_ENUM_FOO = 0;
SOME_ENUM_BAR = 2;
} prost-build will produce: #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SomeEnum(i32);
#[::prost::enumeration]
impl SomeEnum {
pub const FOO: SomeEnum = SomeEnum(0);
pub const BAR: SomeEnum = SomeEnum(2);
} and prost-derive will expand that to: #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SomeEnum(i32);
impl SomeEnum {
pub const FOO: SomeEnum = SomeEnum(0);
pub const BAR: SomeEnum = SomeEnum(2);
pub fn is_valid(self) -> bool {
match self {
SomeEnum::FOO | SomeEnum::BAR => true,
_ => false,
}
}
impl Default for SomeEnum {
fn default() -> SomeEnum {
SomeEnum::FOO
}
}
impl From<SomeEnum> for i32 {
fn from(value: SomeEnum) -> i32 {
value.0
}
}
impl From<i32> for SomeEnum {
fn from(value: i32) -> SomeEnum {
SomeEnum(value)
}
} |
Hi @sfackler, I've run into the same issue but found this before writing my own code. Is this currently WIP? I'd be happy to help out if so. |
I spent a while working on it, but then got sidetracked with other things I've pushed what I have onto a branch but I'm not sure when I'll be able to circle back to it: sfackler@0330d92 |
Can anyone share some application code that this alternate representation cleans up? I'd love to be convinced since this seems to come up over and over, but I'm still not seeing how this is materially better than the generated getters + generated (Rust) enums. |
Are the generated getters still working? I have this Prost-generated code: #[derive(Clone, PartialEq, ::prost::Message)]
pub struct SuiteRegistrationResponse {
#[prost(enumeration = "SuiteAction", tag = "1")]
pub suite_action: i32,
}
/// Tells the suite what action it should perform, based on the args that the API container received
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum SuiteAction {
/// Indicates that the testsuite should operate in metadata-serializing mode, sending suite metadata to the
/// API container
SerializeSuiteMetadata = 0,
/// Indicates that the testsuite should operate in test-executing mode, running a test
ExecuteTest = 1,
} which is throwing the following where when I try to use
Version: |
Yes, they are still present in |
Gotcha, |
Yeah these methods are generated via derives, so RA doesn't seem to 'see' through it a lot of the time. Best way to work with |
Maybe I'm missing something here but why not just generate a Rust enum with the |
The discoverability problem is pretty big imo. I want to expose a Prost generated struct as a function param in a library function. The rust docs just says @danburkert could we have a decision to move forward? There are multiple proposals better than the status quo, e.g Unknown(i32) or the proposal at the top of this issue. |
In proto3, the enums are open and the language guide suggests that unknown values are preserved in the message. I could not find anything more normative, but Prost currently conforms to this by erasing the enum type in message fields. While I mostly find myself needing more idiomatic domain types to work with protobuf-originated data anyway, this gives more room for programmer error than necessary.
A somewhat more cumbersome solution that preserves the enums could be a generic wrapper like this: pub enum EnumFieldValue<T> {
Known(T),
Unknown(i32),
}
impl<T> EnumFieldValue<T> {
pub fn unwrap(self) -> T {
unimplemented!("for brevity")
}
pub fn known_or_else<E, F>(self, err: F) -> Result<T, E>
where
F: FnOnce(i32) -> E,
{
unimplemented!("for brevity")
}
} This has the same effect on the field size as Regardless of the above, you'd want your proto3-derived enums to be annotated with |
pub struct EnumFieldValue<T> {
value: i32,
_phantom: core::marker::PhantomData<T>,
}
impl<T> EnumFieldValue<T>
where
i32: TryInto<T>,
{
pub fn unwrap(self) -> T {
let v = self.value;
v.try_into()
.map_err(|_| format!("unknown field value {}", v))
.unwrap()
}
pub fn known_or_else<E, F>(self, err: F) -> Result<T, E>
where
F: FnOnce(i32) -> E,
{
let v = self.value;
v.try_into().map_err(|_| err(v))
}
} |
every couple of years i pick prost back up and run into this again 😅
real enum types are definitely better for documentation / external use / compatibility with things like serde! given fields are wrapped in |
I agree, we should work towards using the actual enum for enum fields.
I would argue that separated lossy mode is also not discoverable. I think I think a good middle ground for all described use cases is: pub enum EnumOption<T> {
Known(T),
Unknown(i32),
Missing,
}
impl<T> EnumFieldValue<T> {
pub fn unwrap(self) -> T {
todo!()
}
pub fn known(self) -> Option<T> {
todo!()
}
} And then let #[repr(i32)]
pub enum Label {
Optional = 1,
Required = 2,
Repeated = 3,
}
#[derive(::prost::Message)]
pub struct FieldDescriptorProto {
#[prost(int32, optional, tag = "3")]
pub label: ::prost::EnumOption<Label>,
} @ryankurte Are you willing to do the work? I suggest opening a proof of concept pull request with the |
I have just submitted #1061 which I have been holding off for some time. I will un-draft it after adding documentation. The memory layout of the One thing I dislike about the Furthermore, I'm not convinced that making all enum fields unconditionally optional is the best ergonomic approach in all cases, for the same reason why scalars are not guarded with an |
definitely in favour of more useful / idiomatic generated code! it'd be nice to be able to use prost-generated objects like they're normal(ish) rust objects. something like @mzabaluev the
i think your earlier proposal would make sense with separate handling of known/unknown and optional fields? so pub enum EnumFieldValue<T: Default> {
Known(T),
Unknown(i32),
}
struct Something {
// Normal fields are always optional and default to 0'th variant
pub normal_field: EnumFieldValue<SomeEnum>,
// Field labeled 'optional' should be able to represent zero _or_ unset condition
pub optional_field: Option<EnumFieldValue<AnotherEnum>>,
} |
I think you can match on
Regardless of the wrapper's design, such implementations/derives would need to be transparent for the known values. I don't think you can do |
If nobody's working on it, I can try to remake my |
Since proto is pretty open on enum values, meaning they must be preserved even when invalid, I think Forwarded to #1079 |
To deal with the "openness" of Protobuf enumeration types, Prost currently represents them in the message struct as
i32
and generates a Rust enum that can convert to and fromi32
. This is a bit awkward to work with, though.A better approach could be to use a newtype with associated constants. To use the
Syntax
enum as an example, it would look like this:This approach has a couple of advantages:
i32
value.The text was updated successfully, but these errors were encountered: