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

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

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

问题

我有一个proto定义:

  1. message Foo {
  2. oneof bar {
  3. BarA bar_a = 1;
  4. BarB bar_b = 2;
  5. }
  6. }
  7. message BarA {
  8. string text = 1;
  9. }
  10. message BarB {
  11. int32 num = 1;
  12. }

然后是代码:

  1. foo := &Foo{
  2. Bar: &Foo_BarA{
  3. BarA: &BarA{
  4. Text: "text",
  5. },
  6. },
  7. }
  8. md := foo.ProtoReflect()
  9. md.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
  10. }

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

更新更多信息

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

  1. extend google.protobuf.FieldOptions {
  2. string field_name = 50000;
  3. }
  4. extend google.protobuf.MessageOptions {
  5. string template_name = 50000;
  6. Type type = 50001;
  7. }
  8. message SendNotificationRequest {
  9. Type type = 1;
  10. Template template = 2;
  11. }
  12. enum Type {
  13. TYPE_UNSPECIFIED = 0;
  14. TYPE_EMAIL = 1;
  15. TYPE_PUSH = 2;
  16. }
  17. message Template {
  18. oneof template {
  19. EmailTemplate email_template = 1;
  20. PushTemplate push_template = 2;
  21. }
  22. }
  23. message EmailTemplate {
  24. option (type) = TYPE_EMAIL;
  25. oneof template {
  26. WelcomeEmailTemplate welcome_email_template = 1;
  27. }
  28. }
  29. message WelcomeEmailTemplate {
  30. option (template_name) = "welcome_email";
  31. Field title = 1 [(field_name) = "main_title"];
  32. Field user_name = 2 [(field_name) = "name"];
  33. }
  34. message Field {
  35. oneof value {
  36. string str = 1;
  37. bool bool = 2;
  38. int64 num = 3;
  39. }
  40. }

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

  1. message CreateNotificationRequest {
  2. Type type = 1;
  3. string template_name = 2;
  4. map<string, Field> template_fields = 2;
  5. }
  6. enum Type {
  7. TYPE_UNSPECIFIED = 0;
  8. TYPE_EMAIL = 1;
  9. TYPE_PUSH = 2;
  10. }
  11. message Field {
  12. oneof value {
  13. string str = 1;
  14. bool bool = 2;
  15. int64 num = 3;
  16. }
  17. }

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

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

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

  1. &api.SendNotificationRequest{
  2. Type: api.Type_TYPE_EMAIL,
  3. Template: &api.Template {
  4. Template: &api.Template_EmailTemplate{
  5. EmailTemplate: &api.EmailTemplate{
  6. Template: &api.EmailTemplate_WelcomeEmailTemplate{
  7. Title: &api.Field{Value: &api.Placeholder_Str{Text: "Welcome Bob"}},
  8. UserName: &api.Field{Value: &api.Placeholder_Str{Text: "Bob Jones"}},
  9. },
  10. },
  11. },
  12. },
  13. }

应该转换为:

  1. &api2.CreateNotificationRequest{
  2. Type: api2.Type_TYPE_EMAIL,
  3. TemplateName: "welcome_email",
  4. Template: map[string]*api2.Field{
  5. "main_title": &api2.Field{Value: &api.Placeholder_Str{Text: "Welcome Bob"}},
  6. "name": &api2.Field{Value: &api.Placeholder_Str{Text: "Bob Jones"}},
  7. },
  8. }

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

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

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

英文:

I have a proto:

  1. message Foo {
  2. oneof bar {
  3. BarA bar_a = 1;
  4. BarB bar_b = 2;
  5. }
  6. }
  7. message BarA {
  8. string text = 1;
  9. }
  10. message BarB {
  11. int num = 1;
  12. }

Then the code:

  1. foo = &amp;Foo{
  2. Bar: &amp;Foo_BarA{
  3. BarA: &amp;BarA{
  4. Text: &quot;text&quot;,
  5. },
  6. },
  7. }
  8. md := foo.ProtoReflect()
  9. md.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
  10. }

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:

  1. extend google.protobuf.FieldOptions {
  2. string field_name = 50000;
  3. }
  4. extend google.protobuf.MessageOptions {
  5. string template_name = 50000;
  6. Type type = 50001;
  7. }
  8. message SendNotificationRequest {
  9. Type type = 1;
  10. Template template = 2;
  11. }
  12. enum Type {
  13. TYPE_UNSPECIFIED = 0;
  14. TYPE_EMAIL = 1;
  15. TYPE_PUSH = 2;
  16. }
  17. message Template {
  18. oneof template {
  19. EmailTemplate email_template = 1;
  20. PushTemplate push_template = 2;
  21. }
  22. }
  23. message EmailTemplate {
  24. option (type) = TYPE_EMAIL;
  25. oneof template {
  26. WelcomeEmailTemplate welcome_email_template = 1;
  27. }
  28. }
  29. message WelcomeEmailTemplate {
  30. option (template_name) = &quot;welcome_email&quot;;
  31. Field title = 1 [(field_name) = &quot;main_title&quot;];
  32. Field user_name = 2 [(field_name) = &quot;name&quot;];
  33. }
  34. message Field {
  35. oneof value {
  36. string str = 1;
  37. bool bool = 2;
  38. int64 num = 3;
  39. }
  40. }

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

  1. message CreateNotificationRequest {
  2. Type type = 1;
  3. string template_name = 2;
  4. map&lt;string, Field&gt; template_fields = 2;
  5. }
  6. enum Type {
  7. TYPE_UNSPECIFIED = 0;
  8. TYPE_EMAIL = 1;
  9. TYPE_PUSH = 2;
  10. }
  11. message Field {
  12. oneof value {
  13. string str = 1;
  14. bool bool = 2;
  15. int64 num = 3;
  16. }
  17. }

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

  1. &amp;api.SendNotificationRequest{
  2. Type: api.Type_TYPE_EMAIL,
  3. Template: &amp;api.Template {
  4. Template: &amp;api.Template_EmailTemplate{
  5. EmailTemplate: &amp;api.EmailTemplate{
  6. Template: &amp;api.EmailTemplate_WelcomeEmailTemplate{
  7. Title: &amp;api.Field{Value: &amp;api.Placeholder_Str{Text: &quot;Welcome Bob&quot;}},
  8. UserName: &amp;api.Field{Value: &amp;api.Placeholder_Str{Text: &quot;Bob Jones&quot;}},
  9. },
  10. },
  11. },
  12. },
  13. }

should convert to:

  1. &amp;api2.CreateNotificationRequest{
  2. Type: api2.Type_TYPE_EMAIL,
  3. TemplateName: &quot;welcome_email&quot;,
  4. Template: map[string]*api2.Field{
  5. &quot;main_title&quot;: &amp;api2.Field{Value: &amp;api.Placeholder_Str{Text: &quot;Welcome Bob&quot;}},
  6. &quot;name&quot;: &amp;api2.Field{Value: &amp;api.Placeholder_Str{Text: &quot;Bob Jones&quot;}},
  7. },
  8. }

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的值:

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

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

  1. md.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
  2. if fd.Name() == "bar_a" {
  3. if field := fd.Message().Fields().ByName("text"); field != nil {
  4. w := v.Message().Get(field)
  5. fmt.Println(w) // 输出"text"
  6. return false
  7. }
  8. }
  9. return true
  10. })

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

作为另一种选择,你可以通过对 v 中持有的接口进行类型断言来直接获取字段的具体值:

  1. md.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
  2. if barA, ok := v.Message().Interface().(*BarA); ok {
  3. fmt.Println(barA.Text)
  4. return false
  5. }
  6. return true
  7. })
英文:

You can get the value of BarA with:

  1. md.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
  2. w := v.Message().Get(fd.Message().Fields().ByNumber(1))
  3. fmt.Println(w) // prints &quot;text&quot;
  4. return false
  5. })

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:

  1. md.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
  2. if fd.Name() == &quot;bar_a&quot; {
  3. if field := fd.Message().Fields().ByName(&quot;text&quot;); field != nil {
  4. w := v.Message().Get(field)
  5. fmt.Println(w) // text
  6. return false
  7. }
  8. }
  9. return true
  10. })

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:

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

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:

确定