onepiece
onepiece

Reputation: 3539

How to get a field value from FieldDescriptor and Value in protoreflect.Range?

I have a proto:

message Foo {
  oneof bar {
    BarA bar_a = 1;
    BarB bar_b = 2;
  }
}

message BarA {
  string text = 1;
}

message BarB {
  int num = 1;
}

Then the code:

foo = &Foo{
    Bar: &Foo_BarA{
        BarA: &BarA{
            Text: "text",
        },
    },
}
md := foo.ProtoReflect()
md.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {

}

Inside md.Range, how do I get the field value from the fd, v pair? For example when it iterates over the Bar field, I want to get a BarA object and retrieve text. I need to use Range because I'm also using FieldOptions.

Update with more info

My first proto is that I'm actually working with (the above protos are contrived examples) is:

extend google.protobuf.FieldOptions {
  string field_name = 50000;
}

extend google.protobuf.MessageOptions {
  string template_name = 50000;
  Type type = 50001;
}

message SendNotificationRequest {
  Type type = 1;
  Template template = 2;
}

enum Type {
  TYPE_UNSPECIFIED = 0;
  TYPE_EMAIL = 1;
  TYPE_PUSH = 2;
}

message Template {
  oneof template {
    EmailTemplate email_template = 1;
    PushTemplate push_template = 2;
  }
}

message EmailTemplate {
  option (type) = TYPE_EMAIL;

  oneof template {
    WelcomeEmailTemplate welcome_email_template = 1;
  }
}

message WelcomeEmailTemplate {
  option (template_name) = "welcome_email";

  Field title = 1 [(field_name) = "main_title"];
  Field user_name = 2 [(field_name) = "name"];
}

message Field {
  oneof value {
    string str = 1;
    bool bool = 2;
    int64 num = 3;
  }
}

My second proto, part of another service+project, is:

message CreateNotificationRequest {
  Type type = 1;
  string template_name = 2;
  map<string, Field> template_fields = 2; 
}

enum Type {
  TYPE_UNSPECIFIED = 0;
  TYPE_EMAIL = 1;
  TYPE_PUSH = 2;
}

message Field {
  oneof value {
    string str = 1;
    bool bool = 2;
    int64 num = 3;
  }
}

I want to write one function that can convert any SendNotificationRequest to a CreateNotificationRequest. Let's call SendNotificationRequest's import alias api and CreateNotificationRequest's api2. I cannot change api2.

Not too relevant but I made the non-required decision to use typed Template for my service (api) instead of the generic map of api2, as I felt typed classes are more maintainable and less error-prone.

For an example conversion, this

&api.SendNotificationRequest{
    Type: api.Type_TYPE_EMAIL,
    Template:   &api.Template {
        Template: &api.Template_EmailTemplate{
            EmailTemplate: &api.EmailTemplate{
                Template: &api.EmailTemplate_WelcomeEmailTemplate{
                    Title:     &api.Field{Value: &api.Placeholder_Str{Text: "Welcome Bob"}},
                    UserName:     &api.Field{Value: &api.Placeholder_Str{Text: "Bob Jones"}},
                },
            },
        },
    },
}

should convert to:

&api2.CreateNotificationRequest{
    Type: api2.Type_TYPE_EMAIL,
    TemplateName: "welcome_email",
    Template:   map[string]*api2.Field{
        "main_title":     &api2.Field{Value: &api.Placeholder_Str{Text: "Welcome Bob"}},
        "name":     &api2.Field{Value: &api.Placeholder_Str{Text: "Bob Jones"}},
    },
}

There may be new oneofs of EmailTemplate, and templates like WelcomeEmailTemplate may change its fields. But I want to write one function that doesn't need to be updated for those changes.

I believe this is very possible from the answer here.

I can nest md.Range on a SendNotificationRequest and its fields to process all its fields, and write a switch case to convert any Field oneof. I can also fetch FieldOption and MessageOption. While this happens, I build the CreateNotificationRequest.

Upvotes: 2

Views: 6240

Answers (1)

blackgreen
blackgreen

Reputation: 45081

You can get the value of BarA with:

    md.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
        w := v.Message().Get(fd.Message().Fields().ByNumber(1))
        fmt.Println(w) // prints "text"
        return false
    })

This works straight away because your proto message has only one populated field (bar_a). If you have more populated fields, you should add more checks. E.g. check that the current FieldDescriptor is the one you are looking for. A more robust implementation might look like:

    md.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
        if fd.Name() == "bar_a" {
            if field := fd.Message().Fields().ByName("text"); field != nil {
                w := v.Message().Get(field)
                fmt.Println(w) // text
                return false
            }
        }
        return true
    })

Using ByName instead of ByNumber is debatable, since the field name doesn't play a role in the serialization, but IMO it makes for more readable code.


As an alternative, you can directly get the concrete value of the field by type-asserting the interface held in v:

md.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
        if barA, ok := v.Message().Interface().(*BarA); ok {
            fmt.Println(barA.Text)
            return false
        }
        return true
    })

Upvotes: 2

Related Questions