prost icon indicating copy to clipboard operation
prost copied to clipboard

How to access EnumValueOption

Open shradej1 opened this issue 3 years ago • 6 comments

I'm struggling to figure out how to access EnumValueOption extensions on an enum. For example, given

proto/demo.proto:

syntax = "proto3";

import "google/protobuf/descriptor.proto";

package demo;

extend google.protobuf.EnumValueOptions {
  optional uint32 len = 50000;
}

enum Foo {
  None = 0 [(len) = 0];
  One = 1 [(len) = 1];
  Two = 2 [(len) = 2];
}

and

build.rs:

use std::path::PathBuf;

fn main() {
    let out_dir =
        PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR environment variable not set."));

    prost_build::Config::new()
        .file_descriptor_set_path(out_dir.join("file_descriptor_set.bin"))
        .compile_protos(&["proto/demo.proto"], &["proto"])
        .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
}

I suspect I can somehow access the len options using the FileDescriptorSet, but I can't figure out how.

src/lib.rs:

use prost::Message;
use prost_types::FileDescriptorSet;

include!(concat!(env!("OUT_DIR"), concat!("/demo.rs")));

pub fn parse_file_descriptor_set() -> FileDescriptorSet {
    let file_descriptor_set_bytes =
        include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin"));
    prost_types::FileDescriptorSet::decode(&file_descriptor_set_bytes[..]).unwrap()
}

#[cfg(test)]
mod tests {
    use crate::parse_file_descriptor_set;

    #[test]
    fn get_len() {
        let set = parse_file_descriptor_set();
        for f in &set.file {
            dbg!(&f.extension);
        }
        todo!()
    }
}

shradej1 avatar Apr 26 '22 16:04 shradej1

@shradej1 were you able to figure this out?

LucioFranco avatar May 05 '22 15:05 LucioFranco

Nope, still hoping for a response.

shradej1 avatar May 05 '22 16:05 shradej1

I think the right place to look also would be in protoc what it generates since FileDescriptorSet etc is all just generated from protoc I don't know off the top of my head what gets generated into there beyond what we use.

LucioFranco avatar May 05 '22 16:05 LucioFranco

I wonder if this is just missing functionality. If I change get_len() as follows

fn get_len(foo: Foo) -> u32 {
        let set = parse_file_descriptor_set();
        for f in &set.file {
            for ext in &f.extension {
                dbg!(ext);
            }
            for e in &f.enum_type {
                for v in &e.value {
                    dbg!(&v.options);
                }
            }
        }
        todo!()
    }

I see

[src/lib.rs:22] ext = FieldDescriptorProto {
    name: Some(
        "len",
    ),
    number: Some(
        50000,
    ),
    label: Some(
        Optional,
    ),
    r#type: Some(
        Uint32,
    ),
    type_name: None,
    extendee: Some(
        ".google.protobuf.EnumValueOptions",
    ),
    default_value: None,
    oneof_index: None,
    json_name: Some(
        "len",
    ),
    options: None,
    proto3_optional: Some(
        true,
    ),
}

so the extension is being parsed. I don't see the actual len values being stored anywhere though. However, if I add a deprecated option to one of the enum values - None = 0 [(len) = 10, deprecated = true];, it does show up.

[src/lib.rs:29] &v.options = Some(
    EnumValueOptions {
        deprecated: Some(
            true,
        ),
        uninterpreted_option: [],
    },
)

I feel like len should be captured in that uninterpreted_option, which I should be able to parse via (missing?) functionality in the FieldDescriptorProto extension? I might be able to take a peek and figure out where deprecated is being set, and what would actually end up in the uninterpreted_option of EnumValueOption.

shradej1 avatar May 05 '22 17:05 shradej1

I may be missing something. I think that extension value is actually encoded in the serialized message, in which case I'd need the byte stream - not just the enum value - to retrieve it.

shradej1 avatar May 05 '22 17:05 shradej1

I feel like len should be captured in that uninterpreted_option, which I should be able to parse via (missing?) functionality in the FieldDescriptorProto extension? I might be able to take a peek and figure out where deprecated is being set, and what would actually end up in the uninterpreted_option of EnumValueOption.

It looks extension options are actually serialized as unknown fields, which prost doesn't currently capture (#574 might change that)

For example, the text format of your example file descriptor is:

file {
  name: "foo.proto"
  package: "demo"
  dependency: "google/protobuf/descriptor.proto"
  enum_type {
    name: "Foo"
    value {
      name: "None"
      number: 0
      options {
        50000: 0
      }
    }
    value {
      name: "One"
      number: 1
      options {
        50000: 1
      }
    }
    value {
      name: "Two"
      number: 2
      options {
        50000: 2
      }
    }
  }
  extension {
    name: "len"
    extendee: ".google.protobuf.EnumValueOptions"
    number: 50000
    label: LABEL_OPTIONAL
    type: TYPE_UINT32
    json_name: "len"
    proto3_optional: true
  }
  syntax: "proto3"
}

Which does technically contain enough information to reconstruct the option, but you have to inspect the extension fields as well.

My crate prost-reflect is able to retrieve the option values, for example:

let file_descriptor_set_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin"));

let descriptors = prost_reflect::DescriptorPool::decode(file_descriptor_set_bytes.as_ref()).unwrap();
let file_descriptor_set = descriptors
    .get_message_by_name("google.protobuf.FileDescriptorSet")
    .unwrap();

let mut dynamic_message = prost_reflect::DynamicMessage::new(file_descriptor_set);
dynamic_message.merge(bytes.as_slice()).unwrap();

let extension = descriptors
    .get_message_by_name("google.protobuf.EnumValueOptions")
    .unwrap()
    .extensions()
    .find(|ext| ext.name() == "len")
    .unwrap();

assert_eq!(dynamic_message
    .get_field_by_name("file")
    .unwrap()
    .as_list()
    .unwrap()[1]
    .as_message()
    .unwrap()
    .get_field_by_name("enum_type")
    .unwrap()
    .as_list()
    .unwrap()[0]
    .as_message()
    .unwrap()
    .get_field_by_name("value")
    .unwrap()
    .as_list()
    .unwrap()[1]
    .as_message()
    .unwrap()
    .get_field_by_name("options")
    .unwrap()
    .as_message()
    .unwrap()
    .get_extension(&extension)
    .as_ref(), &Value::U32(1));

Ideally there would be solution to bind into a strongly-typed message to avoid the nasty dynamic message inspection, but I'm not aware of any crates that provide that

andrewhickman avatar Jul 09 '22 16:07 andrewhickman