如何从FieldDescriptor和protoreflect.Range中获取字段值?

huangapple go评论101阅读模式
英文:

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

问题

我有一个proto定义:

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

message BarA {
  string text = 1;
}

message BarB {
  int32 num = 1;
}

然后是代码:

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

}

md.Range中,如何从fd, v对中获取字段值?例如,当迭代Bar字段时,我想获取一个BarA对象并检索text。我需要使用Range,因为我还在使用FieldOptions

更新更多信息

我实际上正在使用的第一个proto是:

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;
  }
}

我的第二个proto是另一个服务+项目的一部分:

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;
  }
}

我想编写一个函数,可以将任何SendNotificationRequest转换为CreateNotificationRequest。让我们将SendNotificationRequest的导入别名称为apiCreateNotificationRequest的别名称为api2。我不能更改api2

虽然不太相关,但我做出了一个非必需的决定,即在我的服务(api)中使用了类型化的Template,而不是api2的通用映射,因为我认为类型化的类更易于维护且更不容易出错。

对于一个示例转换,这个:

&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"}},
                },
            },
        },
    },
}

应该转换为:

&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"}},
    },
}

可能会有新的EmailTemplate的oneof,以及像WelcomeEmailTemplate这样的模板可能会更改其字段。但我想编写一个不需要针对这些更改进行更新的函数。

我相信这是可能的,参考这里的答案

我可以在SendNotificationRequest及其字段上嵌套md.Range,以处理所有字段,并编写一个switch case来转换任何Field的oneof。我还可以获取FieldOption和MessageOption。在此过程中,我构建CreateNotificationRequest

英文:

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 = &amp;Foo{
Bar: &amp;Foo_BarA{
BarA: &amp;BarA{
Text: &quot;text&quot;,
},
},
}
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) = &quot;welcome_email&quot;;
Field title = 1 [(field_name) = &quot;main_title&quot;];
Field user_name = 2 [(field_name) = &quot;name&quot;];
}
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&lt;string, Field&gt; 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

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

should convert to:

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

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.

答案1

得分: 2

你可以使用以下代码获取BarA的值:

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

这段代码可以直接工作,因为你的 proto 消息只有一个已填充的字段(bar_a)。如果你有更多已填充的字段,你应该添加更多的检查。例如,检查当前的 FieldDescriptor 是否是你要查找的字段。一个更健壮的实现可能如下所示:

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
})

使用 ByName 而不是 ByNumber 是有争议的,因为字段名在序列化中并不起作用,但在我看来,它可以使代码更易读。

作为另一种选择,你可以通过对 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
})
英文:

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 &quot;text&quot;
		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() == &quot;bar_a&quot; {
if field := fd.Message().Fields().ByName(&quot;text&quot;); 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.

<hr>

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
})

huangapple
  • 本文由 发表于 2021年9月11日 10:17:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/69139530.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定