Search code examples
goprotocol-buffersany

Custom protobuf options of message of type Any in Go


I have a GRPC service defined like:

message SendEventRequest {
  string producer = 1;
  google.protobuf.Any event = 2;
}

message SendEventResponse {
  string event_name = 1;
  string status = 2;
}

service EventService {
  rpc Send(SendEventRequest) returns (SendEventResponse);
}

I also have defined a custom message option:

extend google.protobuf.MessageOptions {
  // event_name is the unique name of the event sent by the clients
  string event_name = 50000;
}

What I want to achieve is have clients create custom proto messages that set the event_name option to a "constant". For instance:

message SomeCustomEvent {
  option (mypackage.event_name) = "some_custom_event";

  string data = 1;
  ...
}

That way the service can keep track of what events are being sent. When I do something like this I'm able to get the value of the option from a specific proto.Message:

_, md := descriptor.MessageDescriptorProto(SomeCustomEvent)
mOpts := md.GetOptions()
eventName := proto.GetExtension(mOpts, mypackage.E_EventName)

However, when the message is of type github.com/golang/protobuf/ptypes/any.Any the options are nil. How can I retrieve the event_name from the message? I've come across the protoregistry.MessageTypeResolver, which looks like it might help, but I would need to figure out a way to dynamically update the proto definitions of the events when clients integrate.


Solution

  • In order to obtain the options of an Any type, you need its specific protoreflect.MessageType so that you can unmarshal it into a specific message. In order to get the message type, you need a MessageTypeResolver.

    Any contains a type_url field, which can be used for that purpose. In order to unmarshal the Any object into a message of an existing message type:

    // GlobalTypes contains information about the proto message types
    var res protoregistry.MessageTypeResolver = protoregistry.GlobalTypes
    typeUrl := anyObject.GetTypeUrl()
    msgType, _ := res.FindMessageByURL(typeUrl)
    
    msg := msgType.New().Interface()
    unmarshalOptions := proto.UnmarshalOptions{Resolver: res}
    unmarshalOptions.Unmarshal(anyObject.GetValue(), msg)
    

    After having the specific message, you can simply get the option you need:

    msgOpts := msg.ProtoReflect().Descriptor().Options()
    eventName := proto.GetExtension(msgOpts, mypackage.E_EventName)
    

    Note that proto.GetExtension will panic if the message doesn't extend the event_name option, and it needs to be recovered. This block can be added at the beginning of the function:

    defer func() {
        if r := recover(); r != nil {
            // err is a named return parameter of the outer function
            err = fmt.Errorf("recovering from panic while extracting event_name from proto message: %s", r)
        }
    }()
    

    EDIT: Note that the application has to import the package containing the proto definitions in order for protoregistry.GlobalTypes to recognize the type. You could do something like this in your code:

    var _ mypackage.SomeEvent