Joe Tsai has uploaded this change for review.
reflect: add protopath and protorange packages
Change-Id: I29cbd5142fe169d78367d54a95d37801888b64f4
---
M internal/msgfmt/format.go
A reflect/protopath/message.go
A reflect/protopath/path.go
A reflect/protopath/step.go
A reflect/protorange/example_test.go
A reflect/protorange/range.go
6 files changed, 1,300 insertions(+), 4 deletions(-)
diff --git a/internal/msgfmt/format.go b/internal/msgfmt/format.go
index 9547a53..a3fff50 100644
--- a/internal/msgfmt/format.go
+++ b/internal/msgfmt/format.go
@@ -31,8 +31,15 @@
return string(appendMessage(nil, m.ProtoReflect()))
}
+// FormatValue returns a formatted string for an arbitrary value.
+func FormatValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) string {
+ return string(appendValue(nil, v, fd))
+}
+
func appendValue(b []byte, v protoreflect.Value, fd protoreflect.FieldDescriptor) []byte {
switch v := v.Interface().(type) {
+ case nil:
+ return append(b, "<invalid>"...)
case bool, int32, int64, uint32, uint64, float32, float64:
return append(b, fmt.Sprint(v)...)
case string:
@@ -40,7 +47,7 @@
case []byte:
return append(b, strconv.Quote(string(v))...)
case protoreflect.EnumNumber:
- return appendEnum(b, v, fd.Enum())
+ return appendEnum(b, v, fd)
case protoreflect.Message:
return appendMessage(b, v)
case protoreflect.List:
@@ -52,9 +59,11 @@
}
}
-func appendEnum(b []byte, v protoreflect.EnumNumber, ed protoreflect.EnumDescriptor) []byte {
- if ev := ed.Values().ByNumber(v); ev != nil {
- return append(b, ev.Name()...)
+func appendEnum(b []byte, v protoreflect.EnumNumber, fd protoreflect.FieldDescriptor) []byte {
+ if fd != nil {
+ if ev := fd.Enum().Values().ByNumber(v); ev != nil {
+ return append(b, ev.Name()...)
+ }
}
return strconv.AppendInt(b, int64(v), 10)
}
diff --git a/reflect/protopath/message.go b/reflect/protopath/message.go
new file mode 100644
index 0000000..e52fc2f
--- /dev/null
+++ b/reflect/protopath/message.go
@@ -0,0 +1,112 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package protopath
+
+import "google.golang.org/protobuf/reflect/protoreflect"
+
+// Message extends protoreflect.Message with accessors operating upon a Path.
+// The runtime for Path-based operations is O(n) where n is length of the path.
+type Message struct{ protoreflect.Message }
+
+// TODO
+func (m Message) HasPath(p Path) bool {
+ return false
+}
+
+// TODO
+func (m Message) ClearPath(p Path) {
+ return
+}
+
+// TODO
+func (m Message) GetPath(p Path) protoreflect.Value {
+ return protoreflect.Value{}
+}
+
+// TODO
+//
+// If p contains an ExpandedAny step, then mutations to the returned values may
+// not be observable within the source values.
+func (m Message) MutablePath(p Path) protoreflect.Value {
+ return protoreflect.Value{}
+}
+
+// TODO
+func (m Message) SetPath(p Path, v protoreflect.Value) {
+ return
+}
+
+/*
+// TODO
+func (m Message) HasPath(p Path) (bool, error) {
+ return false, nil
+}
+
+// TODO
+func (m Message) ClearPath(p Path) error {
+ return nil
+}
+
+// TODO
+func (m Message) GetPath(p Path) (protoreflect.Value, error) {
+ return protoreflect.Value{}, nil
+}
+
+// TODO
+//
+// If p contains an ExpandedAny step, then mutations to the returned values may
+// not be observable within the source values.
+func (m Message) MutablePath(p Path) (protoreflect.Value, error) {
+ return protoreflect.Value{}, nil
+}
+
+// TODO
+func (m Message) SetPath(p Path, v protoreflect.Value) error {
+ return nil
+}
+
+// MustHasPath is like HasPath but panics if an error is encountered.
+func (m Message) MustHasPath(p Path) bool {
+ ok, err := m.HasPath(p)
+ if err != nil {
+ panic(err)
+ }
+ return ok
+}
+
+// MustClearPath is like ClearPath but panics if an error is encountered.
+func (m Message) MustClearPath(p Path) {
+ err := m.ClearPath(p)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// MustGetPath is like GetPath but panics if an error is encountered.
+func (m Message) MustGetPath(p Path) protoreflect.Value {
+ v, err := m.GetPath(p)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// MustMutablePath is like MutablePath but panics if an error is encountered.
+func (m Message) MustMutablePath(p Path) protoreflect.Value {
+ v, err := m.MutablePath(p)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// MustSetPath is like SetPath but panics if an error is encountered.
+func (m Message) MustSetPath(p Path, v protoreflect.Value) {
+ err := m.SetPath(p, v)
+ if err != nil {
+ panic(err)
+ }
+}
+*/
diff --git a/reflect/protopath/path.go b/reflect/protopath/path.go
new file mode 100644
index 0000000..facee0f
--- /dev/null
+++ b/reflect/protopath/path.go
@@ -0,0 +1,313 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package protopath TODO
+package protopath
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "google.golang.org/protobuf/internal/errors"
+ "google.golang.org/protobuf/internal/msgfmt"
+ "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/reflect/protoregistry"
+)
+
+// TODO: Needs Go type information for extensions
+type Resolver interface {
+ FindDescriptorByName(protoreflect.FullName) (protoreflect.Descriptor, error)
+}
+
+type Path []Step
+
+//
+// If r is nil, then protoregistry.GlobalFiles is used.
+func Parse(md protoreflect.MessageDescriptor, s string, r Resolver) (Path, error) {
+ if r == nil {
+ r = protoregistry.GlobalFiles
+ }
+
+ s0 := s // original path
+ parseError := func(f string, v ...interface{}) error {
+ return errors.New("could not parse %q: %v", s0, fmt.Sprintf(f, v...))
+ }
+
+ parseDelims := func(start, end byte) (string, error) {
+ offset := len(s0) - len(s)
+ if len(s) == 0 || s[0] != start {
+ return "", parseError("expected %c at offset %d", start, offset)
+ }
+ s = s[1:] // trim start delim
+ i := strings.IndexByte(s, end)
+ if i < 0 {
+ return "", parseError("expected %c after %c at offset %d", end, start, offset)
+ }
+ value := s[:i]
+ s = s[i:]
+ s = s[1:] // trim end delim
+ return value, nil
+ }
+
+ // Parse optional root message type.
+ var p Path
+ if len(s) > 0 && s[0] == '(' {
+ got, err := parseDelims('(', ')')
+ if err != nil {
+ return p, err
+ }
+ if want := string(md.FullName()); got != want {
+ return p, parseError("mismatching root message: got %v, want %v", got, want)
+ }
+ }
+ p = append(p, Root(md))
+
+ var fd protoreflect.FieldDescriptor
+ for {
+ offset := len(s0) - len(s)
+ if len(s) == 0 || s[0] != '.' {
+ return p, parseError("expected %c at offset %d", '.', offset)
+ }
+ s = s[1:] // trim '.'
+
+ if len(s) == 0 {
+ return p, parseError("expected token after %c at offset %d", '.', offset)
+ }
+ switch s[0] {
+ case '(':
+ name, err := parseDelims('(', ')')
+ if err != nil {
+ return p, err
+ }
+ d, err := r.FindDescriptorByName(protoreflect.FullName(name))
+ if err != nil {
+ return p, parseError("could not resolve %v", name)
+ }
+ switch {
+ case md == nil:
+ return p, parseError("%s is not a message", p)
+ case md.FullName() == "google.protobuf.Any":
+ md, _ = d.(protoreflect.MessageDescriptor)
+ if md == nil {
+ return p, parseError("%v is not a message", d.FullName())
+ }
+ p = append(p, ExpandAny(md))
+ fd = nil
+ case md.ExtensionRanges().Len() > 0:
+ fd, _ = d.(protoreflect.FieldDescriptor)
+ if fd == nil || !fd.IsExtension() {
+ return p, parseError("%v is not an extension field", d.FullName())
+ }
+ if fd.ContainingMessage() != md || !md.ExtensionRanges().Has(fd.Number()) {
+ return p, parseError("extension field %v does not extend message %v", fd.FullName(), md.FullName())
+ }
+ p = append(p, FieldAccess(fd))
+ md = fd.Message() // may be nil
+ if fd.Cardinality() == protoreflect.Repeated {
+ md = nil // let this be populated after ListIndex or MapIndex step
+ }
+ // TODO: message_set_extension and groups
+ default:
+ return p, parseError("%v is not an extensible or Any message", md.FullName())
+ }
+ case '[':
+ key, err := parseDelims('[', ']')
+ if err != nil {
+ return p, err
+ }
+ switch {
+ case fd == nil:
+ return p, parseError("%s is not a repeated field", p)
+ case fd.IsList():
+ var i int
+ _ = key // TODO: Parse i; use prototext format
+ p = append(p, ListIndex(i))
+ md = fd.Message() // may be nil
+ fd = nil
+ case fd.IsMap():
+ var k protoreflect.MapKey
+ _ = key // TODO: Parse k; use prototext format
+ p = append(p, MapIndex(k))
+ md = fd.MapValue().Message() // may be nil
+ fd = nil
+ default:
+ return p, parseError("%v is not a list or map field", fd.FullName())
+ }
+ default:
+ i := strings.IndexByte(s, '.')
+ if i < 0 {
+ i = len(s)
+ }
+ name := s[:i]
+ switch {
+ case md == nil:
+ return p, parseError("%s is not a message", p)
+ case name == "?":
+ p = append(p, UnknownAccess())
+ md, fd = nil, nil
+ default:
+ // TODO: Group
+ fd = md.Fields().ByName(protoreflect.Name(name))
+ if fd == nil {
+ return p, parseError("message %v has no field named %v", md.FullName(), name)
+ }
+ p = append(p, FieldAccess(fd))
+ md = fd.Message() // may be nil
+ if fd.Cardinality() == protoreflect.Repeated {
+ md = nil // let this be populated after ListIndex or MapIndex step
+ }
+ }
+ s = s[i:]
+ }
+ if s == "" {
+ return p, nil
+ }
+ }
+}
+
+// MustParse is like Parse but panics if the path expression cannot be parsed.
+func MustParse(md protoreflect.MessageDescriptor, s string, r Resolver) Path {
+ p, err := Parse(md, s, r)
+ if err != nil {
+ panic(err)
+ }
+ return p
+}
+
+// Index returns the ith step in the path and supports negative indexing.
+// A negative index starts counting from the tail of the Path such that -1
+// refers to the last step, -2 refers to the second-to-last step, and so on.
+func (p Path) Index(i int) Step {
+ if i < 0 {
+ i = len(p) + i
+ }
+ if i < 0 || i >= len(p) {
+ return Step{}
+ }
+ return p[i]
+}
+
+// String returns a structured representation of the path.
+// A valid path is always parsible by Parse.
+func (p Path) String() string {
+ var b []byte
+ for _, s := range p {
+ switch s.Kind() {
+ case RootKind:
+ b = append(b, '(')
+ b = append(b, s.desc.FullName()...)
+ b = append(b, ')')
+ case FieldAccessKind:
+ // TODO: message_set_extension
+ b = append(b, '.')
+ switch fd := s.desc.(protoreflect.FieldDescriptor); {
+ case fd.IsExtension():
+ b = append(b, '(')
+ b = append(b, fd.FullName()...)
+ b = append(b, ')')
+ case fd.Kind() == protoreflect.GroupKind:
+ b = append(b, fd.Message().Name()...)
+ default:
+ b = append(b, fd.Name()...)
+ }
+ case UnknownAccessKind:
+ b = append(b, '.')
+ b = append(b, '?')
+ case ListIndexKind:
+ b = append(b, '[')
+ b = strconv.AppendInt(b, s.key.Int(), 10)
+ b = append(b, ']')
+ case MapIndexKind:
+ b = append(b, '[')
+ switch k := s.key.Interface().(type) {
+ case bool:
+ b = strconv.AppendBool(b, bool(k)) // e.g., "true" or "false"
+ case int32:
+ b = strconv.AppendInt(b, int64(k), 10) // e.g., "-32"
+ case int64:
+ b = strconv.AppendInt(b, int64(k), 10) // e.g., "-64"
+ case uint32:
+ b = strconv.AppendUint(b, uint64(k), 10) // e.g., "32"
+ case uint64:
+ b = strconv.AppendUint(b, uint64(k), 10) // e.g., "64"
+ case string:
+ // TODO: use prototext string representation
+ b = strconv.AppendQuote(b, string(k))
+ }
+ b = append(b, ']')
+ case ExpandAnyKind:
+ b = append(b, '.')
+ b = append(b, '(')
+ b = append(b, s.desc.FullName()...)
+ b = append(b, ')')
+ default:
+ b = append(b, "<invalid>"...)
+ }
+ }
+ return string(b)
+}
+
+// PathValues is a Path paired with a sequence of values at each step.
+// The lengths of Path and Values must be identical.
+// The first step must be a Root step and
+// the first value must be a concrete message value.
+type PathValues struct {
+ Path Path
+ Values []protoreflect.Value
+}
+
+// Len reports the length of the path and values.
+// If the path and values have differing length, it returns the minimum length.
+func (p PathValues) Len() int {
+ n := len(p.Path)
+ if n > len(p.Values) {
+ n = len(p.Values)
+ }
+ return n
+}
+
+// Index returns the ith step and value and supports negative indexing.
+// A negative index starts counting from the tail of the PathValues such that -1
+// refers to the last pair, -2 refers to the second-to-last pair, and so on.
+func (p PathValues) Index(i int) (out struct {
+ Step Step
+ Value protoreflect.Value
+}) {
+ n := p.Len()
+ if i < 0 {
+ i = n + i
+ }
+ if i < 0 || i >= n {
+ return out
+ }
+ out.Step = p.Path[i]
+ out.Value = p.Values[i]
+ return out
+}
+
+// String returns a humanly readable representation of the path and last value.
+// Do not depend on the output being stable.
+//
+// For example:
+// (path.to.MyMessage).list_field[5].map_field["hello"] = {hello: "world"}
+func (p PathValues) String() string {
+ n := p.Len()
+ if n == 0 {
+ return ""
+ }
+
+ // Determine the field descriptor associated with the last step.
+ var fd protoreflect.FieldDescriptor
+ last := p.Index(-1)
+ switch last.Step.Kind() {
+ case FieldAccessKind:
+ fd = last.Step.FieldDescriptor()
+ case MapIndexKind, ListIndexKind:
+ fd = p.Index(-2).Step.FieldDescriptor()
+ }
+
+ // Format the full path with the last value.
+ return fmt.Sprintf("%v = %v", p.Path[:n], msgfmt.FormatValue(last.Value, fd))
+}
diff --git a/reflect/protopath/step.go b/reflect/protopath/step.go
new file mode 100644
index 0000000..8e6862b
--- /dev/null
+++ b/reflect/protopath/step.go
@@ -0,0 +1,198 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package protopath
+
+import (
+ "fmt"
+
+ "google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// Step is a union where only one step operation may be specified at a time.
+// The different kinds of steps are specified by the constants defined for
+// the StepKind type.
+type Step struct {
+ // For performance reasons, we do not store the StepKind, which is a:
+ // Root if desc is a protoreflect.MessageDescriptor and key is true,
+ // FieldAccess if desc is a protoreflect.FieldDescriptor,
+ // UnknownAccess if desc is a *unknownAccess,
+ // ListIndex if desc is a *listIndex,
+ // MapIndex if desc is a *mapIndex, or
+ // ExpandAny if desc is a protoreflect.MessageDescriptor and key is false.
+ desc protoreflect.Descriptor
+ key protoreflect.Value
+}
+
+type unknownAccess struct{ protoreflect.Descriptor }
+type listIndex struct{ protoreflect.Descriptor }
+type mapIndex struct{ protoreflect.Descriptor }
+
+// Root indicates the root message that a path is relative to.
+// It is always (and only ever) the first step in a path.
+func Root(md protoreflect.MessageDescriptor) Step {
+ if md == nil {
+ panic("invalid message descriptor")
+ }
+ return Step{desc: md, key: protoreflect.ValueOfBool(true)}
+}
+
+// FieldAccess describes access of a field within a message.
+// Extension field accesses are also represented using a FieldAccess.
+//
+// Within the context of a PathValues,
+// the type of the current step value is determined by the field descriptor and
+// the type of the previous step value is always a message.
+func FieldAccess(fd protoreflect.FieldDescriptor) Step {
+ if fd == nil {
+ panic("invalid field descriptor")
+ } else if _, ok := fd.(protoreflect.ExtensionTypeDescriptor); !ok && fd.IsExtension() {
+ panic(fmt.Sprintf("extension field %q must implement protoreflect.ExtensionTypeDescriptor", fd.FullName()))
+ }
+ return Step{desc: fd}
+}
+
+// UnknownAccess describes access to the unknown fields within a message.
+//
+// Within the context of a PathValues,
+// the type of the current step value is always a bytes type and
+// the type of the previous step value is always a message.
+func UnknownAccess() Step {
+ return Step{desc: (*unknownAccess)(nil)}
+}
+
+// ListIndex describes index of an element within a list.
+//
+// Within the context of a PathValues,
+// the type of the current step value is determined by the field descriptor,
+// the type of the previous step value is always a list, and
+// the type of the previous previous step value is always a message
+// (since lists can only occur in the context of a repeated field).
+func ListIndex(i int) Step {
+ if i < 0 {
+ panic(fmt.Sprintf("invalid list index: %v", i))
+ }
+ return Step{desc: (*listIndex)(nil), key: protoreflect.ValueOfInt64(int64(i))}
+}
+
+// MapIndex describes index of an entry within a map.
+// The key type is determined by field descriptor that the map belongs to.
+//
+// Within the context of a PathValues,
+// the type of the current step value is determined by the field descriptor,
+// the type of the previous step value is always a map, and
+// the type of the previous previous step value is always a message
+// (since maps can only occur in the context of a repeated field).
+func MapIndex(k protoreflect.MapKey) Step {
+ if !k.IsValid() {
+ panic("invalid map index")
+ }
+ return Step{desc: (*mapIndex)(nil), key: k.Value()}
+}
+
+// ExpandAny describes expansion of a google.protobuf.Any message into
+// a structured representation of the underlying message.
+//
+// Within the context of a PathValues,
+// the type of the current step value is always message and
+// the type of the previous step value is always a google.protobuf.Any message.
+func ExpandAny(md protoreflect.MessageDescriptor) Step {
+ if md == nil {
+ panic("invalid message descriptor")
+ }
+ return Step{desc: md, key: protoreflect.ValueOfBool(false)}
+}
+
+// MessageDescriptor returns the message descriptor for Root or ExpandAny steps,
+// otherwise it returns nil.
+func (s Step) MessageDescriptor() protoreflect.MessageDescriptor {
+ md, _ := s.desc.(protoreflect.MessageDescriptor)
+ return md
+}
+
+// FieldDescriptor returns the field descriptor for FieldAccess steps,
+// otherwise it returns nil.
+func (s Step) FieldDescriptor() protoreflect.FieldDescriptor {
+ fd, _ := s.desc.(protoreflect.FieldDescriptor)
+ return fd
+}
+
+// ListIndex returns the list index for ListIndex steps,
+// otherwise it returns 0.
+func (s Step) ListIndex() int {
+ if _, ok := s.desc.(*listIndex); !ok {
+ return 0
+ }
+ return int(s.key.Int())
+}
+
+// MapKey returns the map key for MapIndex steps,
+// otherwise it returns an invalid map key.
+func (s Step) MapKey() protoreflect.MapKey {
+ if _, ok := s.desc.(*mapIndex); !ok {
+ return protoreflect.MapKey{}
+ }
+ return s.key.MapKey()
+}
+
+// Kind reports which kind of step this is.
+func (s Step) Kind() StepKind {
+ switch s.desc.(type) {
+ case protoreflect.MessageDescriptor:
+ if s.key.Bool() {
+ return RootKind
+ } else {
+ return ExpandAnyKind
+ }
+ case protoreflect.FieldDescriptor:
+ return FieldAccessKind
+ case *unknownAccess:
+ return FieldAccessKind
+ case *listIndex:
+ return ListIndexKind
+ case *mapIndex:
+ return MapIndexKind
+ default:
+ return 0
+ }
+}
+
+// StepKind identifies the kind of step operation.
+type StepKind int
+
+const (
+ _ StepKind = iota
+
+ // RootKind identifies a step as the Root step operation.
+ RootKind
+ // FieldAccessKind identifies a step as the FieldAccess step operation.
+ FieldAccessKind
+ // UnknownAccessKind identifies a step as the UnknownAccess step operation.
+ UnknownAccessKind
+ // ListIndexKind identifies a step as the ListIndex step operation.
+ ListIndexKind
+ // MapIndexKind identifies a step as the MapIndex step operation.
+ MapIndexKind
+ // ExpandAnyKind identifies a step as the ExpandAny step operation.
+ ExpandAnyKind
+)
+
+func (k StepKind) String() string {
+ switch k {
+ case RootKind:
+ return "Root"
+ case FieldAccessKind:
+ return "FieldAccess"
+ case UnknownAccessKind:
+ return "UnknownAccess"
+ case ListIndexKind:
+ return "ListIndex"
+ case MapIndexKind:
+ return "MapIndex"
+ case ExpandAnyKind:
+ return "ExpandAny"
+ default:
+ return fmt.Sprintf("<unknown:%d>", k)
+ }
+}
diff --git a/reflect/protorange/example_test.go b/reflect/protorange/example_test.go
new file mode 100644
index 0000000..df63f99
--- /dev/null
+++ b/reflect/protorange/example_test.go
@@ -0,0 +1,327 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package protorange_test
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "google.golang.org/protobuf/encoding/prototext"
+ "google.golang.org/protobuf/internal/detrand"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protopath"
+ "google.golang.org/protobuf/reflect/protorange"
+ "google.golang.org/protobuf/reflect/protoreflect"
+
+ newspb "google.golang.org/protobuf/internal/testprotos/news"
+ anypb "google.golang.org/protobuf/types/known/anypb"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+func init() {
+ detrand.Disable()
+}
+
+func mustMarshal(m proto.Message) []byte {
+ b, err := proto.Marshal(m)
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
+
+// Range through every message and clear the unknown fields.
+func Example_discardUnknown() {
+ m := &newspb.Article{}
+
+ protorange.Range(m.ProtoReflect(), func(p protopath.Path) error {
+ m, ok := p.Index(-1).Value.Interface().(protoreflect.Message)
+ if ok && len(m.GetUnknown()) > 0 {
+ m.SetUnknown(nil)
+ }
+ return nil
+ })
+
+ fmt.Println(prototext.Format(m))
+
+ // Output:
+ // fawefawe
+}
+
+// Range through every message and populate unpopulated required fields
+// with the default value.
+func Example_initializeRequired() {
+ m := &newspb.Article{}
+
+ protorange.Range(m.ProtoReflect(), func(p protopath.Path) error {
+ m, ok := p.Index(-1).Value.Interface().(protoreflect.Message)
+ if ok {
+ fds := m.Descriptor().Fields()
+ reqNums := m.Descriptor().RequiredNumbers()
+ for i := 0; i < reqNums.Len(); i++ {
+ fd := fds.ByNumber(reqNums.Get(i))
+ if !m.Has(fd) {
+ m.Set(fd, m.NewField(fd))
+ }
+ }
+ }
+ return nil
+ })
+
+ fmt.Println(prototext.Format(m))
+
+ // Output:
+ // fawefawe
+}
+
+// Print the relative paths as Range iterates through a message
+// in a depth-first order.
+func Example_printPaths() {
+ m := &newspb.Article{
+ Author: "Russ Cox",
+ Date: timestamppb.New(time.Date(2019, time.November, 8, 0, 0, 0, 0, time.UTC)),
+ Title: "Go Turns 10",
+ Content: "Happy birthday, Go! This weekend we celebrate the 10th anniversary of the Go release...",
+ Status: newspb.Status_PUBLISHED,
+ Tags: []string{"community", "birthday"},
+ Attachments: []*anypb.Any{{
+ TypeUrl: "google.golang.org.BinaryAttachment",
+ Value: mustMarshal(&newspb.BinaryAttachment{
+ Name: "gopher-birthday.png",
+ Data: []byte("<binary data>"),
+ }),
+ }},
+ }
+
+ // Traverse over all reachable values and print the path.
+ protorange.Options{
+ Deterministic: true,
+ }.Range(m.ProtoReflect(), func(p protopath.Path) error {
+ fmt.Println(p.Path[1:])
+ return nil
+ })
+
+ // Output:
+ // .author
+ // .date
+ // .date.seconds
+ // .title
+ // .content
+ // .status
+ // .tags
+ // .tags[0]
+ // .tags[1]
+ // .attachments
+ // .attachments[0]
+ // .attachments[0].(google.golang.org.BinaryAttachment)
+ // .attachments[0].(google.golang.org.BinaryAttachment).name
+ // .attachments[0].(google.golang.org.BinaryAttachment).data
+}
+
+// Implement a basic text formatter by ranging through all populated values
+// in a message in depth-first order.
+func Example_formatText() {
+ m := &newspb.Article{
+ Author: "Brad Fitzpatrick",
+ Date: timestamppb.New(time.Date(2018, time.February, 16, 0, 0, 0, 0, time.UTC)),
+ Title: "Go 1.10 is released",
+ Content: "Happy Friday, happy weekend! Today the Go team is happy to announce the release of Go 1.10...",
+ Status: newspb.Status_PUBLISHED,
+ Tags: []string{"go1.10", "release"},
+ Attachments: []*anypb.Any{{
+ TypeUrl: "google.golang.org.KeyValueAttachment",
+ Value: mustMarshal(&newspb.KeyValueAttachment{
+ Name: "checksums.txt",
+ Data: map[string]string{
+ "go1.10.src.tar.gz": "07cbb9d0091b846c6aea40bf5bc0cea7",
+ "go1.10.darwin-amd64.pkg": "cbb38bb6ff6ea86279e01745984445bf",
+ "go1.10.linux-amd64.tar.gz": "6b3d0e4a5c77352cf4275573817f7566",
+ "go1.10.windows-amd64.msi": "57bda02030f58f5d2bf71943e1390123",
+ },
+ }),
+ }},
+ }
+
+ // Print a message in a humanly readable format.
+ var indent []byte
+ protorange.Options{
+ Deterministic: true,
+ }.RangeStack(m.ProtoReflect(), protorange.StackFuncs{
+ PushFunc: func(p protopath.Path) error {
+ // Print the key.
+ var fd protoreflect.FieldDescriptor
+ last := p.Index(-1)
+ beforeLast := p.Index(-2)
+ switch last.Step.Kind() {
+ case protopath.FieldAccessKind:
+ fd = last.Step.FieldDescriptor()
+ fmt.Printf("%s%s: ", indent, fd.Name())
+ case protopath.ListIndexKind:
+ fd = beforeLast.Step.FieldDescriptor() // lists always appear in the context of a repeated field
+ fmt.Printf("%s%d: ", indent, last.ListIndex())
+ case protopath.MapIndexKind:
+ fd = beforeLast.Step.FieldDescriptor() // maps always appear in the context of a repeated field
+ fmt.Printf("%s%v: ", indent, last.MapKey().Interface())
+ case protopath.ExpandAnyKind:
+ fmt.Printf("%s[%v]: ", indent, last.Value().Message().Descriptor().FullName())
+ case protopath.UnknownAccessKind:
+ fmt.Printf("%s?: ", indent)
+ }
+
+ // Starting printing the value.
+ switch v := last.Value.Interface().(type) {
+ case protoreflect.Message:
+ fmt.Printf("{\n")
+ indent = append(indent, '\t')
+ case protoreflect.List:
+ fmt.Printf("[\n")
+ indent = append(indent, '\t')
+ case protoreflect.Map:
+ fmt.Printf("{\n")
+ indent = append(indent, '\t')
+ case protoreflect.EnumNumber:
+ var ev protoreflect.EnumValueDescriptor
+ if fd != nil {
+ ev = fd.Enum().Values().ByNumber(v)
+ }
+ if ev != nil {
+ fmt.Printf("%v\n", ev.Name())
+ } else {
+ fmt.Printf("%v\n", v)
+ }
+ case string, []byte:
+ fmt.Printf("%q\n", v)
+ default:
+ fmt.Printf("%v\n", v)
+ }
+ return nil
+ },
+ PopFunc: func(p protopath.Path) error {
+ // Finish printing the value.
+ last := p.Index(-1)
+ switch last.Value.Interface().(type) {
+ case protoreflect.Message:
+ indent = indent[:len(indent)-1]
+ fmt.Printf("%s}\n", indent)
+ case protoreflect.List:
+ indent = indent[:len(indent)-1]
+ fmt.Printf("%s]\n", indent)
+ case protoreflect.Map:
+ indent = indent[:len(indent)-1]
+ fmt.Printf("%s}\n", indent)
+ }
+ return nil
+ },
+ })
+
+ // Output:
+ // {
+ // author: "Brad Fitzpatrick"
+ // date: {
+ // seconds: 1518739200
+ // }
+ // title: "Go 1.10 is released"
+ // content: "Happy Friday, happy weekend! Today the Go team is happy to announce the release of Go 1.10..."
+ // status: PUBLISHED
+ // tags: [
+ // 0: "go1.10"
+ // 1: "release"
+ // ]
+ // attachments: [
+ // 0: {
+ // [google.golang.org.KeyValueAttachment]: {
+ // name: "checksums.txt"
+ // data: {
+ // go1.10.darwin-amd64.pkg: "cbb38bb6ff6ea86279e01745984445bf"
+ // go1.10.linux-amd64.tar.gz: "6b3d0e4a5c77352cf4275573817f7566"
+ // go1.10.src.tar.gz: "07cbb9d0091b846c6aea40bf5bc0cea7"
+ // go1.10.windows-amd64.msi: "57bda02030f58f5d2bf71943e1390123"
+ // }
+ // }
+ // }
+ // ]
+ // }
+}
+
+// Scan all protobuf string values for a sensitive word and replace it with
+// a suitable alternative.
+func Example_sanitizeStrings() {
+ m := &newspb.Article{
+ Author: "Hermione Granger",
+ Date: timestamppb.New(time.Date(1998, time.May, 2, 0, 0, 0, 0, time.UTC)),
+ Title: "Harry Potter vanquishes Voldemort once and for all!",
+ Content: "In a final duel between Harry Potter and Lord Voldemort earlier this evening...",
+ Tags: []string{"HarryPotter", "LordVoldemort"},
+ Attachments: []*anypb.Any{{
+ TypeUrl: "google.golang.org.KeyValueAttachment",
+ Value: mustMarshal(&newspb.KeyValueAttachment{
+ Name: "aliases.txt",
+ Data: map[string]string{
+ "Harry Potter": "Boy Who Lived",
+ "Tom Riddle": "Lord Voldemort",
+ },
+ }),
+ }},
+ }
+
+ protorange.Range(m.ProtoReflect(), func(p protopath.Path) error {
+ const (
+ sensitive = "Voldemort"
+ alternative = "<He Who Must Not Be Named>"
+ )
+
+ // Check if there is a sensitive word to redact.
+ last := p.Index(-1)
+ s, ok := last.Value.Interface().(string)
+ if !ok || !strings.Contains(s, sensitive) {
+ return nil
+ }
+ s = strings.Replace(s, sensitive, alternative, -1)
+
+ // Store the redacted string back into the message.
+ beforeLast := p.Index(-2)
+ switch last.Step.Kind() {
+ case protopath.FieldAccessKind:
+ m := beforeLast.Value.Message()
+ fd := last.Step.FieldDescriptor()
+ m.Set(fd, protoreflect.ValueOfString(s))
+ case protopath.ListIndexKind:
+ ls := beforeLast.Value.List()
+ i := last.Step.ListIndex()
+ ls.Set(i, protoreflect.ValueOfString(s))
+ case protopath.MapIndexKind:
+ ms := beforeLast.Value.Map()
+ k := last.Step.MapKey()
+ ms.Set(k, protoreflect.ValueOfString(s))
+ }
+ return nil
+ })
+
+ fmt.Println(prototext.Format(m))
+
+ // Output:
+ // author: "Hermione Granger"
+ // date: {
+ // seconds: 894067200
+ // }
+ // title: "Harry Potter vanquishes <He Who Must Not Be Named> once and for all!"
+ // content: "In a final duel between Harry Potter and Lord <He Who Must Not Be Named> earlier this evening..."
+ // tags: "HarryPotter"
+ // tags: "Lord<He Who Must Not Be Named>"
+ // attachments: {
+ // [google.golang.org.KeyValueAttachment]: {
+ // name: "aliases.txt"
+ // data: {
+ // key: "Harry Potter"
+ // value: "Boy Who Lived"
+ // }
+ // data: {
+ // key: "Tom Riddle"
+ // value: "Lord <He Who Must Not Be Named>"
+ // }
+ // }
+ // }
+}
diff --git a/reflect/protorange/range.go b/reflect/protorange/range.go
new file mode 100644
index 0000000..b894ce0
--- /dev/null
+++ b/reflect/protorange/range.go
@@ -0,0 +1,337 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package protorange provides functionality to traverse a message value.
+package protorange
+
+import (
+ "bytes"
+ "errors"
+ "sort"
+
+ "google.golang.org/protobuf/internal/fieldnum"
+ "google.golang.org/protobuf/internal/mapsort"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protopath"
+ "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/reflect/protoregistry"
+)
+
+func pushStep(p *protopath.PathValues, s protopath.Step, v protoreflect.Value) {
+ p.Path = append(p.Path, s)
+ p.Values = append(p.Values, v)
+}
+
+func popStep(p *protopath.PathValues) {
+ p.Path = p.Path[:len(p.Path)-1]
+ p.Values = p.Values[:len(p.Values)-1]
+}
+
+// Stack is an interface to hold the current traversal state.
+// The methods may only mutate values that are in the current path.
+type Stack interface {
+ // Push is called while ranging right after it traverses into a value.
+ // Push may return Break to stop traversal of the current value's children.
+ // Returning any non-nil error terminates the entire range operation.
+ Push(protopath.PathValues) error
+
+ // Pop is called while ranging right before it traverses out of a value.
+ // Every Push call is eventually followed by a corresponding Pop call,
+ // even if the range operation is in the process of terminating.
+ // Returning any non-nil error terminates the entire range operation.
+ Pop(protopath.PathValues) error
+}
+
+// StackFuncs is an implementation of Stack that forwards every method of Stack
+// to function pointers. A nil function does nothing and returns nil.
+type StackFuncs struct {
+ PushFunc func(protopath.PathValues) error
+ PopFunc func(protopath.PathValues) error
+}
+
+// Push implements Stack.Push.
+func (s StackFuncs) Push(p protopath.PathValues) error {
+ if s.PushFunc != nil {
+ return s.PushFunc(p)
+ }
+ return nil
+}
+
+// Pop implements Stack.Pop.
+func (s StackFuncs) Pop(p protopath.PathValues) error {
+ if s.PopFunc != nil {
+ return s.PopFunc(p)
+ }
+ return nil
+}
+
+// Range performs a depth-first traversal over reachable values in a message.
+// It calls f as it traverses into every value.
+//
+// See Options.RangeStack for details.
+func Range(m protoreflect.Message, f func(protopath.PathValues) error) error {
+ return Options{}.Range(m, f)
+}
+
+// Options configures traversal of a message value tree.
+type Options struct {
+ // Deterministic specifies whether to visit message fields and map entries
+ // in a deterministic ordering.
+ Deterministic bool
+
+ // Resolver is used for looking up types when expanding google.protobuf.Any
+ // messages. If nil, this defaults to using protoregistry.GlobalTypes.
+ Resolver interface {
+ protoregistry.ExtensionTypeResolver
+ protoregistry.MessageTypeResolver
+ }
+}
+
+// Range performs a depth-first pre-order traversal over reachable values
+// in a message. It calls f as it traverses into every value.
+//
+// See Options.RangeStack for details.
+func (o Options) Range(m protoreflect.Message, f func(protopath.PathValues) error) error {
+ return o.RangeStack(m, StackFuncs{PushFunc: f})
+}
+
+// RangeStack performs a depth-first traversal over reachable values in a message.
+// It calls s.Push and s.Pop as it traverses into and out of every value.
+// The first push and the last pop are to push/pop a protopath.Root step.
+// If push or pop return any non-nil error (other than Break or Terminate),
+// it is returned by RangeStack.
+//
+// The rules for traversing a message is as follows:
+//
+// • For messages, iterate over every populated known and extension field.
+// Each field is preceded by a push of a protopath.MessageField step,
+// followed by recursive application of the rules on the field value,
+// and succeeded by a pop of that step. If the message has unknown fields,
+// then push an UnknownAccess step followed immediately by pop of that step.
+//
+// • As an exception to the above rule, if the current message is a
+// google.protobuf.Any message, expand the underlying message (if resolvable).
+// The expanded message is preceded by a push of a protopath.ExpandedAny step,
+// followed by recursive application of the rules on the underlying message,
+// and succeeded by a pop of that step. Mutations to the expanded message
+// are written back to the Any message when popping back out.
+//
+// • For lists, iterate over every element. Each element is preceded by a push
+// of a protopath.ListIndex step, followed by recursive application of the rules
+// on the list element, and succeeded by a pop of that step.
+//
+// • For maps, iterate over every entry. Each entry is preceded by a push
+// of a protopath.MapIndex step, followed by recursive application of the rules
+// on the map entry value, and succeeded by a pop of that step.
+func (o Options) RangeStack(m protoreflect.Message, s Stack) error {
+ var err error
+ p := new(protopath.PathValues)
+ if o.Resolver == nil {
+ o.Resolver = protoregistry.GlobalTypes
+ }
+
+ pushStep(p, protopath.Root(m.Descriptor()), protoreflect.ValueOfMessage(m))
+ err = amendError(err, s.Push(*p))
+ if err == nil {
+ err = o.rangeMessage(p, m, s)
+ }
+ err = amendError(err, s.Pop(*p))
+ popStep(p)
+
+ if err == Break || err == Terminate {
+ err = nil
+ }
+ return err
+}
+
+func (o Options) rangeMessage(p *protopath.PathValues, m protoreflect.Message, s Stack) (err error) {
+ if ok, err := o.rangeAnyMessage(p, m, s); ok {
+ return err
+ }
+
+ o.rangeMessageFields(p, m, func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
+ pushStep(p, protopath.FieldAccess(fd), v)
+ err = amendError(err, s.Push(*p))
+ if err == nil {
+ switch {
+ case fd.IsMap():
+ err = o.rangeMap(p, fd, v.Map(), s)
+ case fd.IsList():
+ err = o.rangeList(p, fd, v.List(), s)
+ case fd.Message() != nil:
+ err = o.rangeMessage(p, v.Message(), s)
+ }
+ }
+ err = amendError(err, s.Pop(*p))
+ popStep(p)
+ return err == nil
+ })
+
+ if b := m.GetUnknown(); len(b) > 0 && err == nil {
+ pushStep(p, protopath.UnknownAccess(), protoreflect.ValueOfBytes(b))
+ err = amendError(err, s.Push(*p))
+ err = amendError(err, s.Pop(*p))
+ popStep(p)
+ }
+
+ if err == Break {
+ err = nil
+ }
+ return err
+}
+
+func (o Options) rangeAnyMessage(p *protopath.PathValues, m protoreflect.Message, s Stack) (ok bool, err error) {
+ md := m.Descriptor()
+ if md.FullName() != "google.protobuf.Any" {
+ return false, nil
+ }
+
+ fds := md.Fields()
+ url := m.Get(fds.ByNumber(fieldnum.Any_TypeUrl)).String()
+ val := m.Get(fds.ByNumber(fieldnum.Any_Value)).Bytes()
+ mt, errFind := o.Resolver.FindMessageByURL(url)
+ if errFind != nil {
+ return false, nil
+ }
+
+ // Unmarshal the raw encoded message value into a structured message value.
+ m2 := mt.New()
+ errUnmarshal := proto.UnmarshalOptions{
+ Merge: true,
+ AllowPartial: true,
+ Resolver: o.Resolver,
+ }.Unmarshal(val, m2.Interface())
+ if errUnmarshal != nil {
+ return false, nil
+ }
+
+ // Marshal Any before ranging to detect possible mutations.
+ b1, errMarshal := proto.MarshalOptions{
+ AllowPartial: true,
+ Deterministic: true,
+ }.Marshal(m2.Interface())
+ if errMarshal != nil {
+ return true, errMarshal
+ }
+
+ pushStep(p, protopath.ExpandAny(m2.Descriptor()), protoreflect.ValueOfMessage(m2))
+ err = amendError(err, s.Push(*p))
+ if err == nil {
+ err = o.rangeMessage(p, m2, s)
+ }
+ err = amendError(err, s.Pop(*p))
+ popStep(p)
+
+ // Marshal Any after ranging to detect possible mutations.
+ b2, errMarshal := proto.MarshalOptions{
+ AllowPartial: true,
+ Deterministic: true,
+ }.Marshal(m2.Interface())
+ if errMarshal != nil {
+ return true, errMarshal
+ }
+
+ // Mutations detected, write the new sequence of bytes to the Any message.
+ if !bytes.Equal(b1, b2) {
+ m.Set(fds.ByNumber(fieldnum.Any_Value), protoreflect.ValueOfBytes(b2))
+ }
+
+ if err == Break {
+ err = nil
+ }
+ return true, err
+}
+
+func (o Options) rangeMessageFields(p *protopath.PathValues, m protoreflect.Message, f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) {
+ if !o.Deterministic {
+ m.Range(f)
+ return
+ }
+
+ var fds []protoreflect.FieldDescriptor
+ m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
+ fds = append(fds, fd)
+ return true
+ })
+ sort.Slice(fds, func(i, j int) bool {
+ return fds[i].Number() < fds[j].Number()
+ })
+
+ for _, fd := range fds {
+ if !f(fd, m.Get(fd)) {
+ return
+ }
+ }
+}
+
+func (o Options) rangeList(p *protopath.PathValues, fd protoreflect.FieldDescriptor, ls protoreflect.List, s Stack) (err error) {
+ for i := 0; i < ls.Len() && err == nil; i++ {
+ v := ls.Get(i)
+ pushStep(p, protopath.ListIndex(i), v)
+ err = amendError(err, s.Push(*p))
+ if err == nil && fd.Message() != nil {
+ err = o.rangeMessage(p, v.Message(), s)
+ }
+ err = amendError(err, s.Pop(*p))
+ popStep(p)
+ }
+
+ if err == Break {
+ err = nil
+ }
+ return err
+}
+
+func (o Options) rangeMap(p *protopath.PathValues, fd protoreflect.FieldDescriptor, ms protoreflect.Map, s Stack) (err error) {
+ o.rangeMapEntries(ms, fd.MapKey().Kind(), func(k protoreflect.MapKey, v protoreflect.Value) bool {
+ pushStep(p, protopath.MapIndex(k), v)
+ err = amendError(err, s.Push(*p))
+ if err == nil && fd.MapValue().Message() != nil {
+ err = o.rangeMessage(p, v.Message(), s)
+ }
+ err = amendError(err, s.Pop(*p))
+ popStep(p)
+ return err == nil
+ })
+
+ if err == Break {
+ err = nil
+ }
+ return err
+}
+
+func (o Options) rangeMapEntries(ms protoreflect.Map, k protoreflect.Kind, f func(protoreflect.MapKey, protoreflect.Value) bool) {
+ if !o.Deterministic {
+ ms.Range(f)
+ return
+ }
+ mapsort.Range(ms, k, f)
+}
+
+var (
+ // Break breaks traversal of children in the current value.
+ // It has no effect when traversing values that are not composite types
+ // (e.g., messages, lists, and maps).
+ Break = errors.New("break traversal of children in current value")
+
+ // Terminate terminates the entire range operation.
+ // All necessary Pop operations continue to be called.
+ Terminate = errors.New("terminate range operation")
+)
+
+// amendErrors amends the previous error with the current error if it is
+// considered more serious. The precedence order for errors is:
+// nil < Break < Terminate < previous non-nil < current non-nil
+func amendError(prev, curr error) error {
+ switch {
+ case curr == nil:
+ return prev
+ case curr == Break && prev != nil:
+ return prev
+ case curr == Terminate && prev != nil && prev != Break:
+ return prev
+ default:
+ return curr
+ }
+}
To view, visit change 236540. To unsubscribe, or for help writing mail filters, visit settings.
Joe Tsai uploaded patch set #2 to this change.
reflect: add protopath and protorange packages
The protopath package provides a means to programmatically represent
a sequence of protobuf reflection operations.
The protorange package traverses through a message and
calls a user-provided function as it iterates.
This feature sets the groundwork for the often requested feature
of being able to exclude certain fields when merging or serializing.
package protopath
type Path []Step
type Step struct{ ... }
func AnyExpand(md protoreflect.MessageDescriptor) Step
func FieldAccess(fd protoreflect.FieldDescriptor) Step
func ListIndex(i int) Step
func MapIndex(k protoreflect.MapKey) Step
func Root(md protoreflect.MessageDescriptor) Step
func UnknownAccess() Step
func (s Step) Kind() StepKind
func (s Step) FieldDescriptor() protoreflect.FieldDescriptor
func (s Step) MessageDescriptor() protoreflect.MessageDescriptor
func (s Step) ListIndex() int
func (s Step) MapIndex() protoreflect.MapKey
func (s Step) String() string
type StepKind int
RootStep StepKind
FieldAccessStep
UnknownAccessStep
ListIndexStep
MapIndexStep
AnyExpandStep
type Values struct {
Path Path
Values []protoreflect.Value
}
func (p Values) Index(i int) (out struct{ ... })
func (p Values) Len() int
func (p Values) String() string
package protorange
var Break error
var Terminate error
func Range(m protoreflect.Message, f func(protopath.Values) error) error
type Options struct {
Stable bool
Resolver interface { ... }
}
func (o Options) Range(m protoreflect.Message, push, pop func(protopath.Values) error) error
Change-Id: I29cbd5142fe169d78367d54a95d37801888b64f4
---
M internal/encoding/text/encode.go
M internal/msgfmt/format.go
A internal/testprotos/news/news.pb.go
A internal/testprotos/news/news.proto
A reflect/protopath/path.go
A reflect/protopath/step.go
A reflect/protorange/example_test.go
A reflect/protorange/range.go
8 files changed, 1,448 insertions(+), 4 deletions(-)
To view, visit change 236540. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Damien Neil.
Joe Tsai uploaded patch set #3 to this change.
reflect: add protopath and protorange packages
A reflect/protopath/path.go
A reflect/protopath/step.go
A reflect/protorange/example_test.go
A reflect/protorange/range.go
A reflect/protorange/range_test.go
9 files changed, 1,701 insertions(+), 4 deletions(-)
To view, visit change 236540. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Damien Neil.
Joe Tsai uploaded patch set #4 to this change.
reflect: add protopath and protorange packages
The protopath package provides a means to programmatically represent
a sequence of protobuf reflection operations.
The protorange package traverses through a message and
calls a user-provided function as it iterates.
This feature sets the groundwork for the often requested feature
of being able to exclude certain fields when merging or serializing.
package protopath
type Path []Step
type Step struct{ ... }
func Root(protoreflect.MessageDescriptor) Step
func FieldAccess(protoreflect.FieldDescriptor) Step
func UnknownAccess() Step
func ListIndex(int) Step
func MapIndex(protoreflect.MapKey) Step
func AnyExpand(protoreflect.MessageDescriptor) Step
func (Step) Kind() StepKind
func (Step) FieldDescriptor() protoreflect.FieldDescriptor
func (Step) MessageDescriptor() protoreflect.MessageDescriptor
func (Step) ListIndex() int
func (Step) MapIndex() protoreflect.MapKey
func (Step) String() string
type StepKind int
const RootStep StepKind
const FieldAccessStep StepKind
const UnknownAccessStep StepKind
const ListIndexStep StepKind
const MapIndexStep StepKind
const AnyExpandStep StepKind
type Values struct {
Path Path
Values []protoreflect.Value
}
func (Values) Index(int) (out struct{ ... })
func (Values) Len() int
func (Values) String() string
package protorange
var Break error
var Terminate error
func Range(protoreflect.Message, func(protopath.Values) error) error
type Options struct {
Stable bool
Resolver interface { ... }
}
func (Options) Range(m protoreflect.Message, push, pop func(protopath.Values) error) error
Change-Id: I29cbd5142fe169d78367d54a95d37801888b64f4
---
M internal/encoding/text/encode.go
M internal/msgfmt/format.go
A internal/testprotos/news/news.pb.go
A internal/testprotos/news/news.proto
A reflect/protopath/path.go
A reflect/protopath/step.go
A reflect/protorange/example_test.go
A reflect/protorange/range.go
A reflect/protorange/range_test.go
9 files changed, 1,701 insertions(+), 4 deletions(-)
To view, visit change 236540. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Joe Tsai.
Patch set 4:Code-Review +2
4 comments:
Patchset:
Nice.
File internal/testprotos/news/news.proto:
Patch Set #4, Line 32: bytes data = 2;
Trailing space.
File reflect/protorange/range.go:
Patch Set #4, Line 74: // then push an UnknownAccess step followed immediately by pop of that step.
Document interactions between modifying messages while traversing them.
We could leave this explicitly undefined, but I think some support for modifying the tree is too useful to ignore. (And I note that there's some code below to ensure that modifying the contents of an Any before traversing it works properly.)
Perhaps modifications to the last value in a path in a push func will be visible to the traversal, and any other modification is explicitly undefined?
Patch Set #4, Line 90: func (o Options) Range(m protoreflect.Message, push, pop func(protopath.Values) error) error {
Document ownership of the Values passed to push/pop: These must not be retained after push/pop return.
To view, visit change 236540. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Joe Tsai.
Joe Tsai uploaded patch set #5 to this change.
reflect: add protopath and protorange packages
A reflect/protopath/path.go
A reflect/protopath/step.go
A reflect/protorange/example_test.go
A reflect/protorange/range.go
A reflect/protorange/range_test.go
9 files changed, 1,714 insertions(+), 4 deletions(-)
To view, visit change 236540. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Joe Tsai.
3 comments:
File internal/testprotos/news/news.proto:
Patch Set #4, Line 32: bytes data = 2;
Trailing space.
Done
File reflect/protorange/range.go:
Patch Set #4, Line 74: // then push an UnknownAccess step followed immediately by pop of that step.
Document interactions between modifying messages while traversing them. […]
Done
Patch Set #4, Line 90: func (o Options) Range(m protoreflect.Message, push, pop func(protopath.Values) error) error {
Document ownership of the Values passed to push/pop: These must not be retained after push/pop retur […]
Done
To view, visit change 236540. To unsubscribe, or for help writing mail filters, visit settings.
Joe Tsai submitted this change.
reflect: add protopath and protorange packages
func (Options) Range(m protoreflect.Message, push, pop func(protopath.Values) error) error
Change-Id: I29cbd5142fe169d78367d54a95d37801888b64f4
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/236540
Trust: Joe Tsai <joe...@digital-static.net>
Reviewed-by: Damien Neil <dn...@google.com>
---
M internal/encoding/text/encode.go
M internal/msgfmt/format.go
A internal/testprotos/news/news.pb.go
A internal/testprotos/news/news.proto
A reflect/protopath/path.go
A reflect/protopath/step.go
A reflect/protorange/example_test.go
A reflect/protorange/range.go
A reflect/protorange/range_test.go
9 files changed, 1,714 insertions(+), 4 deletions(-)
diff --git a/internal/encoding/text/encode.go b/internal/encoding/text/encode.go
index aa66bdd..da289cc 100644
--- a/internal/encoding/text/encode.go
+++ b/internal/encoding/text/encode.go
@@ -263,3 +263,8 @@
func (e *Encoder) Reset(es encoderState) {
e.encoderState = es
}
+
+// AppendString appends the escaped form of the input string to b.
+func AppendString(b []byte, s string) []byte {
+ return appendString(b, s, false)
+}
diff --git a/internal/msgfmt/format.go b/internal/msgfmt/format.go
index 6561b8b..a319550 100644
--- a/internal/msgfmt/format.go
+++ b/internal/msgfmt/format.go
@@ -30,8 +30,15 @@
return string(appendMessage(nil, m.ProtoReflect()))
}
+// FormatValue returns a formatted string for an arbitrary value.
+func FormatValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) string {
+ return string(appendValue(nil, v, fd))
+}
+
func appendValue(b []byte, v protoreflect.Value, fd protoreflect.FieldDescriptor) []byte {
switch v := v.Interface().(type) {
+ case nil:
+ return append(b, "<invalid>"...)
case bool, int32, int64, uint32, uint64, float32, float64:
return append(b, fmt.Sprint(v)...)
case string:
@@ -39,7 +46,7 @@
case []byte:
return append(b, strconv.Quote(string(v))...)
case protoreflect.EnumNumber:
- return appendEnum(b, v, fd.Enum())
+ return appendEnum(b, v, fd)
case protoreflect.Message:
return appendMessage(b, v)
case protoreflect.List:
@@ -51,9 +58,11 @@
}
}
-func appendEnum(b []byte, v protoreflect.EnumNumber, ed protoreflect.EnumDescriptor) []byte {
- if ev := ed.Values().ByNumber(v); ev != nil {
- return append(b, ev.Name()...)
+func appendEnum(b []byte, v protoreflect.EnumNumber, fd protoreflect.FieldDescriptor) []byte {
+ if fd != nil {
+ if ev := fd.Enum().Values().ByNumber(v); ev != nil {
+ return append(b, ev.Name()...)
+ }
}
return strconv.AppendInt(b, int64(v), 10)
}
diff --git a/internal/testprotos/news/news.pb.go b/internal/testprotos/news/news.pb.go
new file mode 100644
index 0000000..bb728a6
--- /dev/null
+++ b/internal/testprotos/news/news.pb.go
@@ -0,0 +1,421 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: internal/testprotos/news/news.proto
+
+package news
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ anypb "google.golang.org/protobuf/types/known/anypb"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ reflect "reflect"
+ sync "sync"
+)
+
+type Article_Status int32
+
+const (
+ Article_DRAFT Article_Status = 0
+ Article_PUBLISHED Article_Status = 1
+ Article_REVOKED Article_Status = 2
+)
+
+// Enum value maps for Article_Status.
+var (
+ Article_Status_name = map[int32]string{
+ 0: "DRAFT",
+ 1: "PUBLISHED",
+ 2: "REVOKED",
+ }
+ Article_Status_value = map[string]int32{
+ "DRAFT": 0,
+ "PUBLISHED": 1,
+ "REVOKED": 2,
+ }
+)
+
+func (x Article_Status) Enum() *Article_Status {
+ p := new(Article_Status)
+ *p = x
+ return p
+}
+
+func (x Article_Status) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (Article_Status) Descriptor() protoreflect.EnumDescriptor {
+ return file_internal_testprotos_news_news_proto_enumTypes[0].Descriptor()
+}
+
+func (Article_Status) Type() protoreflect.EnumType {
+ return &file_internal_testprotos_news_news_proto_enumTypes[0]
+}
+
+func (x Article_Status) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use Article_Status.Descriptor instead.
+func (Article_Status) EnumDescriptor() ([]byte, []int) {
+ return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{0, 0}
+}
+
+type Article struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Author string `protobuf:"bytes,1,opt,name=author,proto3" json:"author,omitempty"`
+ Date *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=date,proto3" json:"date,omitempty"`
+ Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"`
+ Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"`
+ Status Article_Status `protobuf:"varint,8,opt,name=status,proto3,enum=google.golang.org.Article_Status" json:"status,omitempty"`
+ Tags []string `protobuf:"bytes,7,rep,name=tags,proto3" json:"tags,omitempty"`
+ Attachments []*anypb.Any `protobuf:"bytes,6,rep,name=attachments,proto3" json:"attachments,omitempty"`
+}
+
+func (x *Article) Reset() {
+ *x = Article{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_internal_testprotos_news_news_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Article) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Article) ProtoMessage() {}
+
+func (x *Article) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_testprotos_news_news_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Article.ProtoReflect.Descriptor instead.
+func (*Article) Descriptor() ([]byte, []int) {
+ return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Article) GetAuthor() string {
+ if x != nil {
+ return x.Author
+ }
+ return ""
+}
+
+func (x *Article) GetDate() *timestamppb.Timestamp {
+ if x != nil {
+ return x.Date
+ }
+ return nil
+}
+
+func (x *Article) GetTitle() string {
+ if x != nil {
+ return x.Title
+ }
+ return ""
+}
+
+func (x *Article) GetContent() string {
+ if x != nil {
+ return x.Content
+ }
+ return ""
+}
+
+func (x *Article) GetStatus() Article_Status {
+ if x != nil {
+ return x.Status
+ }
+ return Article_DRAFT
+}
+
+func (x *Article) GetTags() []string {
+ if x != nil {
+ return x.Tags
+ }
+ return nil
+}
+
+func (x *Article) GetAttachments() []*anypb.Any {
+ if x != nil {
+ return x.Attachments
+ }
+ return nil
+}
+
+type BinaryAttachment struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
+}
+
+func (x *BinaryAttachment) Reset() {
+ *x = BinaryAttachment{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_internal_testprotos_news_news_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *BinaryAttachment) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BinaryAttachment) ProtoMessage() {}
+
+func (x *BinaryAttachment) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_testprotos_news_news_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BinaryAttachment.ProtoReflect.Descriptor instead.
+func (*BinaryAttachment) Descriptor() ([]byte, []int) {
+ return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *BinaryAttachment) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *BinaryAttachment) GetData() []byte {
+ if x != nil {
+ return x.Data
+ }
+ return nil
+}
+
+type KeyValueAttachment struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ Data map[string]string `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+}
+
+func (x *KeyValueAttachment) Reset() {
+ *x = KeyValueAttachment{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_internal_testprotos_news_news_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *KeyValueAttachment) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*KeyValueAttachment) ProtoMessage() {}
+
+func (x *KeyValueAttachment) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_testprotos_news_news_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use KeyValueAttachment.ProtoReflect.Descriptor instead.
+func (*KeyValueAttachment) Descriptor() ([]byte, []int) {
+ return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *KeyValueAttachment) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *KeyValueAttachment) GetData() map[string]string {
+ if x != nil {
+ return x.Data
+ }
+ return nil
+}
+
+var File_internal_testprotos_news_news_proto protoreflect.FileDescriptor
+
+var file_internal_testprotos_news_news_proto_rawDesc = []byte{
+ 0x0a, 0x23, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x6e, 0x65, 0x77, 0x73, 0x2f, 0x6e, 0x65, 0x77, 0x73, 0x2e,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x67, 0x6f,
+ 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+ 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb9, 0x02, 0x0a, 0x07, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65,
+ 0x12, 0x16, 0x0a, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x12, 0x2e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
+ 0x6d, 0x70, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c,
+ 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x18,
+ 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
+ 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+ 0x65, 0x2e, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x41, 0x72, 0x74,
+ 0x69, 0x63, 0x6c, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61,
+ 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
+ 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x36, 0x0a, 0x0b, 0x61, 0x74, 0x74, 0x61, 0x63,
+ 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67,
+ 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41,
+ 0x6e, 0x79, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22,
+ 0x2f, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x52, 0x41,
+ 0x46, 0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45,
+ 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x44, 0x10, 0x02,
+ 0x22, 0x3a, 0x0a, 0x10, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68,
+ 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xa6, 0x01, 0x0a,
+ 0x12, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d,
+ 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x43, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18,
+ 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x67,
+ 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c,
+ 0x75, 0x65, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x61, 0x74,
+ 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09,
+ 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76,
+ 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
+ 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+ 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x62, 0x75, 0x66, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73,
+ 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x6e, 0x65, 0x77, 0x73, 0x62, 0x06, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_internal_testprotos_news_news_proto_rawDescOnce sync.Once
+ file_internal_testprotos_news_news_proto_rawDescData = file_internal_testprotos_news_news_proto_rawDesc
+)
+
+func file_internal_testprotos_news_news_proto_rawDescGZIP() []byte {
+ file_internal_testprotos_news_news_proto_rawDescOnce.Do(func() {
+ file_internal_testprotos_news_news_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_testprotos_news_news_proto_rawDescData)
+ })
+ return file_internal_testprotos_news_news_proto_rawDescData
+}
+
+var file_internal_testprotos_news_news_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_internal_testprotos_news_news_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_internal_testprotos_news_news_proto_goTypes = []interface{}{
+ (Article_Status)(0), // 0: google.golang.org.Article.Status
+ (*Article)(nil), // 1: google.golang.org.Article
+ (*BinaryAttachment)(nil), // 2: google.golang.org.BinaryAttachment
+ (*KeyValueAttachment)(nil), // 3: google.golang.org.KeyValueAttachment
+ nil, // 4: google.golang.org.KeyValueAttachment.DataEntry
+ (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp
+ (*anypb.Any)(nil), // 6: google.protobuf.Any
+}
+var file_internal_testprotos_news_news_proto_depIdxs = []int32{
+ 5, // 0: google.golang.org.Article.date:type_name -> google.protobuf.Timestamp
+ 0, // 1: google.golang.org.Article.status:type_name -> google.golang.org.Article.Status
+ 6, // 2: google.golang.org.Article.attachments:type_name -> google.protobuf.Any
+ 4, // 3: google.golang.org.KeyValueAttachment.data:type_name -> google.golang.org.KeyValueAttachment.DataEntry
+ 4, // [4:4] is the sub-list for method output_type
+ 4, // [4:4] is the sub-list for method input_type
+ 4, // [4:4] is the sub-list for extension type_name
+ 4, // [4:4] is the sub-list for extension extendee
+ 0, // [0:4] is the sub-list for field type_name
+}
+
+func init() { file_internal_testprotos_news_news_proto_init() }
+func file_internal_testprotos_news_news_proto_init() {
+ if File_internal_testprotos_news_news_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_internal_testprotos_news_news_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Article); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_internal_testprotos_news_news_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*BinaryAttachment); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_internal_testprotos_news_news_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*KeyValueAttachment); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_internal_testprotos_news_news_proto_rawDesc,
+ NumEnums: 1,
+ NumMessages: 4,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_internal_testprotos_news_news_proto_goTypes,
+ DependencyIndexes: file_internal_testprotos_news_news_proto_depIdxs,
+ EnumInfos: file_internal_testprotos_news_news_proto_enumTypes,
+ MessageInfos: file_internal_testprotos_news_news_proto_msgTypes,
+ }.Build()
+ File_internal_testprotos_news_news_proto = out.File
+ file_internal_testprotos_news_news_proto_rawDesc = nil
+ file_internal_testprotos_news_news_proto_goTypes = nil
+ file_internal_testprotos_news_news_proto_depIdxs = nil
+}
diff --git a/internal/testprotos/news/news.proto b/internal/testprotos/news/news.proto
new file mode 100644
index 0000000..bf56f3b
--- /dev/null
+++ b/internal/testprotos/news/news.proto
@@ -0,0 +1,38 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+syntax = "proto3";
+
+package google.golang.org;
+
+import "google/protobuf/any.proto";
+import "google/protobuf/timestamp.proto";
+
+option go_package = "google.golang.org/protobuf/internal/testprotos/news";
+
+message Article {
+ enum Status {
+ DRAFT = 0;
+ PUBLISHED = 1;
+ REVOKED = 2;
+ }
+
+ string author = 1;
+ google.protobuf.Timestamp date = 2;
+ string title = 3;
+ string content = 4;
+ Status status = 8;
+ repeated string tags = 7;
+ repeated google.protobuf.Any attachments = 6;
+}
+
+message BinaryAttachment {
+ string name = 1;
+ bytes data = 2;
+}
+
+message KeyValueAttachment {
+ string name = 1;
+ map<string, string> data = 2;
+}
\ No newline at end of file
diff --git a/reflect/protopath/path.go b/reflect/protopath/path.go
new file mode 100644
index 0000000..07f839d
--- /dev/null
+++ b/reflect/protopath/path.go
@@ -0,0 +1,121 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package protopath provides functionality for
+// representing a sequence of protobuf reflection operations on a message.
+package protopath
+
+import (
+ "fmt"
+
+ "google.golang.org/protobuf/internal/msgfmt"
+ "google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// NOTE: The Path and Values are separate types here since there are use cases
+// where you would like to "address" some value in a message with just the path
+// and don't have the value information available.
+//
+// This is different from how "github.com/google/go-cmp/cmp".Path operates,
+// which combines both path and value information together.
+// Since the cmp package itself is the only one ever constructing a cmp.Path,
+// it will always have the value available.
+
+// Path is a sequence of protobuf reflection steps applied to some root
+// protobuf message value to arrive at the current value.
+// The first step must be a Root step.
+type Path []Step
+
+// TODO: Provide a Parse function that parses something similar to or
+// perhaps identical to the output of Path.String.
+
+// Index returns the ith step in the path and supports negative indexing.
+// A negative index starts counting from the tail of the Path such that -1
+// refers to the last step, -2 refers to the second-to-last step, and so on.
+// It returns a zero Step value if the index is out-of-bounds.
+func (p Path) Index(i int) Step {
+ if i < 0 {
+ i = len(p) + i
+ }
+ if i < 0 || i >= len(p) {
+ return Step{}
+ }
+ return p[i]
+}
+
+// String returns a structured representation of the path
+// by concatenating the string representation of every path step.
+func (p Path) String() string {
+ var b []byte
+ for _, s := range p {
+ b = s.appendString(b)
+ }
+ return string(b)
+}
+
+// Values is a Path paired with a sequence of values at each step.
+// The lengths of Path and Values must be identical.
+// The first step must be a Root step and
+// the first value must be a concrete message value.
+type Values struct {
+ Path Path
+ Values []protoreflect.Value
+}
+
+// Len reports the length of the path and values.
+// If the path and values have differing length, it returns the minimum length.
+func (p Values) Len() int {
+ n := len(p.Path)
+ if n > len(p.Values) {
+ n = len(p.Values)
+ }
+ return n
+}
+
+// Index returns the ith step and value and supports negative indexing.
+// A negative index starts counting from the tail of the Values such that -1
+// refers to the last pair, -2 refers to the second-to-last pair, and so on.
+func (p Values) Index(i int) (out struct {
+ Step Step
+ Value protoreflect.Value
+}) {
+ // NOTE: This returns a single struct instead of two return values so that
+ // callers can make use of the the value in an expression:
+ // vs.Index(i).Value.Interface()
+ n := p.Len()
+ if i < 0 {
+ i = n + i
+ }
+ if i < 0 || i >= n {
+ return out
+ }
+ out.Step = p.Path[i]
+ out.Value = p.Values[i]
+ return out
+}
+
+// String returns a humanly readable representation of the path and last value.
+// Do not depend on the output being stable.
+//
+// For example:
+// (path.to.MyMessage).list_field[5].map_field["hello"] = {hello: "world"}
+func (p Values) String() string {
+ n := p.Len()
+ if n == 0 {
+ return ""
+ }
+
+ // Determine the field descriptor associated with the last step.
+ var fd protoreflect.FieldDescriptor
+ last := p.Index(-1)
+ switch last.Step.kind {
+ case FieldAccessStep:
+ fd = last.Step.FieldDescriptor()
+ case MapIndexStep, ListIndexStep:
+ fd = p.Index(-2).Step.FieldDescriptor()
+ }
+
+ // Format the full path with the last value.
+ return fmt.Sprintf("%v = %v", p.Path[:n], msgfmt.FormatValue(last.Value, fd))
+}
diff --git a/reflect/protopath/step.go b/reflect/protopath/step.go
new file mode 100644
index 0000000..95ae85c
--- /dev/null
+++ b/reflect/protopath/step.go
@@ -0,0 +1,241 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package protopath
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "google.golang.org/protobuf/internal/encoding/text"
+ "google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// StepKind identifies the kind of step operation.
+// Each kind of step corresponds with some protobuf reflection operation.
+type StepKind int
+
+const (
+ invalidStep StepKind = iota
+ // RootStep identifies a step as the Root step operation.
+ RootStep
+ // FieldAccessStep identifies a step as the FieldAccess step operation.
+ FieldAccessStep
+ // UnknownAccessStep identifies a step as the UnknownAccess step operation.
+ UnknownAccessStep
+ // ListIndexStep identifies a step as the ListIndex step operation.
+ ListIndexStep
+ // MapIndexStep identifies a step as the MapIndex step operation.
+ MapIndexStep
+ // AnyExpandStep identifies a step as the AnyExpand step operation.
+ AnyExpandStep
+)
+
+func (k StepKind) String() string {
+ switch k {
+ case invalidStep:
+ return "<invalid>"
+ case RootStep:
+ return "Root"
+ case FieldAccessStep:
+ return "FieldAccess"
+ case UnknownAccessStep:
+ return "UnknownAccess"
+ case ListIndexStep:
+ return "ListIndex"
+ case MapIndexStep:
+ return "MapIndex"
+ case AnyExpandStep:
+ return "AnyExpand"
+ default:
+ return fmt.Sprintf("<unknown:%d>", k)
+ }
+}
+
+// Step is a union where only one step operation may be specified at a time.
+// The different kinds of steps are specified by the constants defined for
+// the StepKind type.
+type Step struct {
+ kind StepKind
+ desc protoreflect.Descriptor
+ key protoreflect.Value
+}
+
+// Root indicates the root message that a path is relative to.
+// It should always (and only ever) be the first step in a path.
+func Root(md protoreflect.MessageDescriptor) Step {
+ if md == nil {
+ panic("nil message descriptor")
+ }
+ return Step{kind: RootStep, desc: md}
+}
+
+// FieldAccess describes access of a field within a message.
+// Extension field accesses are also represented using a FieldAccess and
+// must be provided with a protoreflect.FieldDescriptor
+//
+// Within the context of Values,
+// the type of the previous step value is always a message, and
+// the type of the current step value is determined by the field descriptor.
+func FieldAccess(fd protoreflect.FieldDescriptor) Step {
+ if fd == nil {
+ panic("nil field descriptor")
+ } else if _, ok := fd.(protoreflect.ExtensionTypeDescriptor); !ok && fd.IsExtension() {
+ panic(fmt.Sprintf("extension field %q must implement protoreflect.ExtensionTypeDescriptor", fd.FullName()))
+ }
+ return Step{kind: FieldAccessStep, desc: fd}
+}
+
+// UnknownAccess describes access to the unknown fields within a message.
+//
+// Within the context of Values,
+// the type of the previous step value is always a message, and
+// the type of the current step value is always a bytes type.
+func UnknownAccess() Step {
+ return Step{kind: UnknownAccessStep}
+}
+
+// ListIndex describes index of an element within a list.
+//
+// Within the context of Values,
+// the type of the previous, previous step value is always a message,
+// the type of the previous step value is always a list, and
+// the type of the current step value is determined by the field descriptor.
+func ListIndex(i int) Step {
+ if i < 0 {
+ panic(fmt.Sprintf("invalid list index: %v", i))
+ }
+ return Step{kind: ListIndexStep, key: protoreflect.ValueOfInt64(int64(i))}
+}
+
+// MapIndex describes index of an entry within a map.
+// The key type is determined by field descriptor that the map belongs to.
+//
+// Within the context of Values,
+// the type of the previous previous step value is always a message,
+// the type of the previous step value is always a map, and
+// the type of the current step value is determined by the field descriptor.
+func MapIndex(k protoreflect.MapKey) Step {
+ if !k.IsValid() {
+ panic("invalid map index")
+ }
+ return Step{kind: MapIndexStep, key: k.Value()}
+}
+
+// AnyExpand describes expansion of a google.protobuf.Any message into
+// a structured representation of the underlying message.
+//
+// Within the context of Values,
+// the type of the previous step value is always a google.protobuf.Any message, and
+// the type of the current step value is always a message.
+func AnyExpand(md protoreflect.MessageDescriptor) Step {
+ if md == nil {
+ panic("nil message descriptor")
+ }
+ return Step{kind: AnyExpandStep, desc: md}
+}
+
+// MessageDescriptor returns the message descriptor for Root or AnyExpand steps,
+// otherwise it returns nil.
+func (s Step) MessageDescriptor() protoreflect.MessageDescriptor {
+ switch s.kind {
+ case RootStep, AnyExpandStep:
+ return s.desc.(protoreflect.MessageDescriptor)
+ default:
+ return nil
+ }
+}
+
+// FieldDescriptor returns the field descriptor for FieldAccess steps,
+// otherwise it returns nil.
+func (s Step) FieldDescriptor() protoreflect.FieldDescriptor {
+ switch s.kind {
+ case FieldAccessStep:
+ return s.desc.(protoreflect.FieldDescriptor)
+ default:
+ return nil
+ }
+}
+
+// ListIndex returns the list index for ListIndex steps,
+// otherwise it returns 0.
+func (s Step) ListIndex() int {
+ switch s.kind {
+ case ListIndexStep:
+ return int(s.key.Int())
+ default:
+ return 0
+ }
+}
+
+// MapIndex returns the map key for MapIndex steps,
+// otherwise it returns an invalid map key.
+func (s Step) MapIndex() protoreflect.MapKey {
+ switch s.kind {
+ case MapIndexStep:
+ return s.key.MapKey()
+ default:
+ return protoreflect.MapKey{}
+ }
+}
+
+// Kind reports which kind of step this is.
+func (s Step) Kind() StepKind {
+ return s.kind
+}
+
+func (s Step) String() string {
+ return string(s.appendString(nil))
+}
+
+func (s Step) appendString(b []byte) []byte {
+ switch s.kind {
+ case RootStep:
+ b = append(b, '(')
+ b = append(b, s.desc.FullName()...)
+ b = append(b, ')')
+ case FieldAccessStep:
+ b = append(b, '.')
+ if fd := s.desc.(protoreflect.FieldDescriptor); fd.IsExtension() {
+ b = append(b, '(')
+ b = append(b, strings.Trim(fd.TextName(), "[]")...)
+ b = append(b, ')')
+ } else {
+ b = append(b, fd.TextName()...)
+ }
+ case UnknownAccessStep:
+ b = append(b, '.')
+ b = append(b, '?')
+ case ListIndexStep:
+ b = append(b, '[')
+ b = strconv.AppendInt(b, s.key.Int(), 10)
+ b = append(b, ']')
+ case MapIndexStep:
+ b = append(b, '[')
+ switch k := s.key.Interface().(type) {
+ case bool:
+ b = strconv.AppendBool(b, bool(k)) // e.g., "true" or "false"
+ case int32:
+ b = strconv.AppendInt(b, int64(k), 10) // e.g., "-32"
+ case int64:
+ b = strconv.AppendInt(b, int64(k), 10) // e.g., "-64"
+ case uint32:
+ b = strconv.AppendUint(b, uint64(k), 10) // e.g., "32"
+ case uint64:
+ b = strconv.AppendUint(b, uint64(k), 10) // e.g., "64"
+ case string:
+ b = text.AppendString(b, k) // e.g., `"hello, world"`
+ }
+ b = append(b, ']')
+ case AnyExpandStep:
+ b = append(b, '.')
+ b = append(b, '(')
+ b = append(b, s.desc.FullName()...)
+ b = append(b, ')')
+ default:
+ b = append(b, "<invalid>"...)
+ }
+ return b
+}
diff --git a/reflect/protorange/example_test.go b/reflect/protorange/example_test.go
new file mode 100644
index 0000000..90ceec6
--- /dev/null
+++ b/reflect/protorange/example_test.go
@@ -0,0 +1,307 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package protorange_test
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/internal/detrand"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protopath"
+ "google.golang.org/protobuf/reflect/protorange"
+ "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/testing/protopack"
+ "google.golang.org/protobuf/types/known/anypb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ newspb "google.golang.org/protobuf/internal/testprotos/news"
+)
+
+func init() {
+ detrand.Disable()
+}
+
+func mustMarshal(m proto.Message) []byte {
+ b, err := proto.Marshal(m)
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
+
+// Range through every message and clear the unknown fields.
+func Example_discardUnknown() {
+ // Populate the article with unknown fields.
+ m := &newspb.Article{}
+ m.ProtoReflect().SetUnknown(protopack.Message{
+ protopack.Tag{1000, protopack.BytesType}, protopack.String("Hello, world!"),
+ }.Marshal())
+ fmt.Println("has unknown fields?", len(m.ProtoReflect().GetUnknown()) > 0)
+
+ // Range through the message and clear all unknown fields.
+ fmt.Println("clear unknown fields")
+ protorange.Range(m.ProtoReflect(), func(proto protopath.Values) error {
+ m, ok := proto.Index(-1).Value.Interface().(protoreflect.Message)
+ if ok && len(m.GetUnknown()) > 0 {
+ m.SetUnknown(nil)
+ }
+ return nil
+ })
+ fmt.Println("has unknown fields?", len(m.ProtoReflect().GetUnknown()) > 0)
+
+ // Output:
+ // has unknown fields? true
+ // clear unknown fields
+ // has unknown fields? false
+}
+
+// Print the relative paths as Range iterates through a message
+// in a depth-first order.
+func Example_printPaths() {
+ m := &newspb.Article{
+ Author: "Russ Cox",
+ Date: timestamppb.New(time.Date(2019, time.November, 8, 0, 0, 0, 0, time.UTC)),
+ Title: "Go Turns 10",
+ Content: "Happy birthday, Go! This weekend we celebrate the 10th anniversary of the Go release...",
+ Status: newspb.Article_PUBLISHED,
+ Tags: []string{"community", "birthday"},
+ Attachments: []*anypb.Any{{
+ TypeUrl: "google.golang.org.BinaryAttachment",
+ Value: mustMarshal(&newspb.BinaryAttachment{
+ Name: "gopher-birthday.png",
+ Data: []byte("<binary data>"),
+ }),
+ }},
+ }
+
+ // Traverse over all reachable values and print the path.
+ protorange.Range(m.ProtoReflect(), func(p protopath.Values) error {+ Status: newspb.Article_PUBLISHED,
+ Tags: []string{"go1.10", "release"},
+ Attachments: []*anypb.Any{{
+ TypeUrl: "google.golang.org.KeyValueAttachment",
+ Value: mustMarshal(&newspb.KeyValueAttachment{
+ Name: "checksums.txt",
+ Data: map[string]string{
+ "go1.10.src.tar.gz": "07cbb9d0091b846c6aea40bf5bc0cea7",
+ "go1.10.darwin-amd64.pkg": "cbb38bb6ff6ea86279e01745984445bf",
+ "go1.10.linux-amd64.tar.gz": "6b3d0e4a5c77352cf4275573817f7566",
+ "go1.10.windows-amd64.msi": "57bda02030f58f5d2bf71943e1390123",
+ },
+ }),
+ }},
+ }
+
+ // Print a message in a humanly readable format.
+ var indent []byte
+ protorange.Options{
+ Stable: true,
+ }.Range(m.ProtoReflect(),
+ func(p protopath.Values) error {
+ // Print the key.
+ var fd protoreflect.FieldDescriptor
+ last := p.Index(-1)
+ beforeLast := p.Index(-2)
+ switch last.Step.Kind() {
+ case protopath.FieldAccessStep:
+ fd = last.Step.FieldDescriptor()
+ fmt.Printf("%s%s: ", indent, fd.Name())
+ case protopath.ListIndexStep:
+ fd = beforeLast.Step.FieldDescriptor() // lists always appear in the context of a repeated field
+ fmt.Printf("%s%d: ", indent, last.Step.ListIndex())
+ case protopath.MapIndexStep:
+ fd = beforeLast.Step.FieldDescriptor() // maps always appear in the context of a repeated field
+ fmt.Printf("%s%v: ", indent, last.Step.MapIndex().Interface())
+ case protopath.AnyExpandStep:
+ fmt.Printf("%s[%v]: ", indent, last.Value.Message().Descriptor().FullName())
+ case protopath.UnknownAccessStep:
+ func(p protopath.Values) error {+ // attachments: [
+ // 0: {
+ // [google.golang.org.KeyValueAttachment]: {
+ // name: "checksums.txt"
+ // data: {
+ // go1.10.darwin-amd64.pkg: "cbb38bb6ff6ea86279e01745984445bf"
+ // go1.10.linux-amd64.tar.gz: "6b3d0e4a5c77352cf4275573817f7566"
+ // go1.10.src.tar.gz: "07cbb9d0091b846c6aea40bf5bc0cea7"
+ // go1.10.windows-amd64.msi: "57bda02030f58f5d2bf71943e1390123"
+ // }
+ // }
+ // }
+ // ]
+ // tags: [
+ // 0: "go1.10"
+ // 1: "release"
+ // ]
+ // status: PUBLISHED
+ // }
+}
+
+// Scan all protobuf string values for a sensitive word and replace it with
+// a suitable alternative.
+func Example_sanitizeStrings() {
+ m := &newspb.Article{
+ Author: "Hermione Granger",
+ Date: timestamppb.New(time.Date(1998, time.May, 2, 0, 0, 0, 0, time.UTC)),
+ Title: "Harry Potter vanquishes Voldemort once and for all!",
+ Content: "In a final duel between Harry Potter and Lord Voldemort earlier this evening...",
+ Tags: []string{"HarryPotter", "LordVoldemort"},
+ Attachments: []*anypb.Any{{
+ TypeUrl: "google.golang.org.KeyValueAttachment",
+ Value: mustMarshal(&newspb.KeyValueAttachment{
+ Name: "aliases.txt",
+ Data: map[string]string{
+ "Harry Potter": "The Boy Who Lived",
+ "Tom Riddle": "Lord Voldemort",
+ },
+ }),
+ }},
+ }
+
+ protorange.Range(m.ProtoReflect(), func(p protopath.Values) error {
+ const (
+ sensitive = "Voldemort"
+ alternative = "[He-Who-Must-Not-Be-Named]"
+ )
+
+ // Check if there is a sensitive word to redact.
+ last := p.Index(-1)
+ s, ok := last.Value.Interface().(string)
+ if !ok || !strings.Contains(s, sensitive) {
+ return nil
+ }
+ s = strings.Replace(s, sensitive, alternative, -1)
+
+ // Store the redacted string back into the message.
+ beforeLast := p.Index(-2)
+ switch last.Step.Kind() {
+ case protopath.FieldAccessStep:
+ m := beforeLast.Value.Message()
+ fd := last.Step.FieldDescriptor()
+ m.Set(fd, protoreflect.ValueOfString(s))
+ case protopath.ListIndexStep:
+ ls := beforeLast.Value.List()
+ i := last.Step.ListIndex()
+ ls.Set(i, protoreflect.ValueOfString(s))
+ case protopath.MapIndexStep:
+ ms := beforeLast.Value.Map()
+ k := last.Step.MapIndex()
+ ms.Set(k, protoreflect.ValueOfString(s))
+ }
+ return nil
+ })
+
+ fmt.Println(protojson.Format(m))
+
+ // Output:
+ // {
+ // "author": "Hermione Granger",
+ // "date": "1998-05-02T00:00:00Z",
+ // "title": "Harry Potter vanquishes [He-Who-Must-Not-Be-Named] once and for all!",
+ // "content": "In a final duel between Harry Potter and Lord [He-Who-Must-Not-Be-Named] earlier this evening...",
+ // "tags": [
+ // "HarryPotter",
+ // "Lord[He-Who-Must-Not-Be-Named]"
+ // ],
+ // "attachments": [
+ // {
+ // "@type": "google.golang.org.KeyValueAttachment",
+ // "name": "aliases.txt",
+ // "data": {
+ // "Harry Potter": "The Boy Who Lived",
+ // "Tom Riddle": "Lord [He-Who-Must-Not-Be-Named]"
+ // }
+ // }
+ // ]
+ // }
+}
diff --git a/reflect/protorange/range.go b/reflect/protorange/range.go
new file mode 100644
index 0000000..01750f7
--- /dev/null
+++ b/reflect/protorange/range.go
@@ -0,0 +1,315 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package protorange provides functionality to traverse a message value.
+package protorange
+
+import (
+ "bytes"
+ "errors"
+
+ "google.golang.org/protobuf/internal/genid"
+ "google.golang.org/protobuf/internal/order"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protopath"
+ "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/reflect/protoregistry"
+)
+
+var (
+ // Break breaks traversal of children in the current value.
+ // It has no effect when traversing values that are not composite types
+ // (e.g., messages, lists, and maps).
+ Break = errors.New("break traversal of children in current value")
+
+ // Terminate terminates the entire range operation.
+ // All necessary Pop operations continue to be called.
+ Terminate = errors.New("terminate range operation")
+)
+
+// Range performs a depth-first traversal over reachable values in a message.
+//
+// See Options.Range for details.
+func Range(m protoreflect.Message, f func(protopath.Values) error) error {
+ return Options{}.Range(m, f, nil)
+}
+
+// Options configures traversal of a message value tree.
+type Options struct {
+ // Stable specifies whether to visit message fields and map entries
+ // in a stable ordering. If false, then the ordering is undefined and
+ // may be non-deterministic.
+ //
+ // Message fields are visited in ascending order by field number.
+ // Map entries are visited in ascending order, where
+ // boolean keys are ordered such that false sorts before true,
+ // numeric keys are ordered based on the numeric value, and
+ // string keys are lexicographically ordered by Unicode codepoints.
+ Stable bool
+
+ // Resolver is used for looking up types when expanding google.protobuf.Any
+ // messages. If nil, this defaults to using protoregistry.GlobalTypes.
+ // To prevent expansion of Any messages, pass an empty protoregistry.Types:
+ //
+ // Options{Resolver: (*protoregistry.Types)(nil)}
+ //
+ Resolver interface {
+ protoregistry.ExtensionTypeResolver
+ protoregistry.MessageTypeResolver
+ }
+}
+
+// Range performs a depth-first traversal over reachable values in a message.
+// The first push and the last pop are to push/pop a protopath.Root step.
+// If push or pop return any non-nil error (other than Break or Terminate),
+// it terminates the traversal and is returned by Range.
+//
+// The rules for traversing a message is as follows:
+//
+// • For messages, iterate over every populated known and extension field.
+// Each field is preceded by a push of a protopath.FieldAccess step,
+// followed by recursive application of the rules on the field value,
+// and succeeded by a pop of that step.
+// If the message has unknown fields, then push an protopath.UnknownAccess step
+// followed immediately by pop of that step.
+//
+// • As an exception to the above rule, if the current message is a
+// google.protobuf.Any message, expand the underlying message (if resolvable).
+// The expanded message is preceded by a push of a protopath.AnyExpand step,
+// followed by recursive application of the rules on the underlying message,
+// and succeeded by a pop of that step. Mutations to the expanded message
+// are written back to the Any message when popping back out.
+//
+// • For lists, iterate over every element. Each element is preceded by a push
+// of a protopath.ListIndex step, followed by recursive application of the rules
+// on the list element, and succeeded by a pop of that step.
+//
+// • For maps, iterate over every entry. Each entry is preceded by a push
+// of a protopath.MapIndex step, followed by recursive application of the rules
+// on the map entry value, and succeeded by a pop of that step.
+//
+// Mutations should only be made to the last value, otherwise the effects on
+// traversal will be undefined. If the mutation is made to the last value
+// during to a push, then the effects of the mutation will affect traversal.
+// For example, if the last value is currently a message, and the push function
+// populates a few fields in that message, then the newly modified fields
+// will be traversed.
+//
+// The protopath.Values provided to push functions is only valid until the
+// corresponding pop call and the values provided to a pop call is only valid
+// for the duration of the pop call itself.
+func (o Options) Range(m protoreflect.Message, push, pop func(protopath.Values) error) error {
+ var err error
+ p := new(protopath.Values)
+ if o.Resolver == nil {
+ o.Resolver = protoregistry.GlobalTypes
+ }
+
+ pushStep(p, protopath.Root(m.Descriptor()), protoreflect.ValueOfMessage(m))
+ if push != nil {
+ err = amendError(err, push(*p))
+ }
+ if err == nil {
+ err = o.rangeMessage(p, m, push, pop)
+ }
+ if pop != nil {
+ err = amendError(err, pop(*p))
+ }
+ popStep(p)
+
+ if err == Break || err == Terminate {
+ err = nil
+ }
+ return err
+}
+
+func (o Options) rangeMessage(p *protopath.Values, m protoreflect.Message, push, pop func(protopath.Values) error) (err error) {
+ if ok, err := o.rangeAnyMessage(p, m, push, pop); ok {
+ return err
+ }
+
+ fieldOrder := order.AnyFieldOrder
+ if o.Stable {
+ fieldOrder = order.NumberFieldOrder
+ }
+ order.RangeFields(m, fieldOrder, func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
+ pushStep(p, protopath.FieldAccess(fd), v)
+ if push != nil {
+ err = amendError(err, push(*p))
+ }
+ if err == nil {
+ switch {
+ case fd.IsMap():
+ err = o.rangeMap(p, fd, v.Map(), push, pop)
+ case fd.IsList():
+ err = o.rangeList(p, fd, v.List(), push, pop)
+ case fd.Message() != nil:
+ err = o.rangeMessage(p, v.Message(), push, pop)
+ }
+ }
+ if pop != nil {
+ err = amendError(err, pop(*p))
+ }
+ popStep(p)
+ return err == nil
+ })
+
+ if b := m.GetUnknown(); len(b) > 0 && err == nil {
+ pushStep(p, protopath.UnknownAccess(), protoreflect.ValueOfBytes(b))
+ if push != nil {
+ err = amendError(err, push(*p))
+ }
+ if pop != nil {
+ err = amendError(err, pop(*p))
+ }
+ popStep(p)
+ }
+
+ if err == Break {
+ err = nil
+ }
+ return err
+}
+
+func (o Options) rangeAnyMessage(p *protopath.Values, m protoreflect.Message, push, pop func(protopath.Values) error) (ok bool, err error) {
+ md := m.Descriptor()
+ if md.FullName() != "google.protobuf.Any" {
+ return false, nil
+ }
+
+ fds := md.Fields()
+ url := m.Get(fds.ByNumber(genid.Any_TypeUrl_field_number)).String()
+ val := m.Get(fds.ByNumber(genid.Any_Value_field_number)).Bytes()
+ mt, errFind := o.Resolver.FindMessageByURL(url)
+ if errFind != nil {
+ return false, nil
+ }
+
+ // Unmarshal the raw encoded message value into a structured message value.
+ m2 := mt.New()
+ errUnmarshal := proto.UnmarshalOptions{
+ Merge: true,
+ AllowPartial: true,
+ Resolver: o.Resolver,
+ }.Unmarshal(val, m2.Interface())
+ if errUnmarshal != nil {
+ // If the the underlying message cannot be unmarshaled,
+ // then just treat this as an normal message type.
+ return false, nil
+ }
+
+ // Marshal Any before ranging to detect possible mutations.
+ b1, errMarshal := proto.MarshalOptions{
+ AllowPartial: true,
+ Deterministic: true,
+ }.Marshal(m2.Interface())
+ if errMarshal != nil {
+ return true, errMarshal
+ }
+
+ pushStep(p, protopath.AnyExpand(m2.Descriptor()), protoreflect.ValueOfMessage(m2))
+ if push != nil {
+ err = amendError(err, push(*p))
+ }
+ if err == nil {
+ err = o.rangeMessage(p, m2, push, pop)
+ }
+ if pop != nil {
+ err = amendError(err, pop(*p))
+ }
+ popStep(p)
+
+ // Marshal Any after ranging to detect possible mutations.
+ b2, errMarshal := proto.MarshalOptions{
+ AllowPartial: true,
+ Deterministic: true,
+ }.Marshal(m2.Interface())
+ if errMarshal != nil {
+ return true, errMarshal
+ }
+
+ // Mutations detected, write the new sequence of bytes to the Any message.
+ if !bytes.Equal(b1, b2) {
+ m.Set(fds.ByNumber(genid.Any_Value_field_number), protoreflect.ValueOfBytes(b2))
+ }
+
+ if err == Break {
+ err = nil
+ }
+ return true, err
+}
+
+func (o Options) rangeList(p *protopath.Values, fd protoreflect.FieldDescriptor, ls protoreflect.List, push, pop func(protopath.Values) error) (err error) {
+ for i := 0; i < ls.Len() && err == nil; i++ {
+ v := ls.Get(i)
+ pushStep(p, protopath.ListIndex(i), v)
+ if push != nil {
+ err = amendError(err, push(*p))
+ }
+ if err == nil && fd.Message() != nil {
+ err = o.rangeMessage(p, v.Message(), push, pop)
+ }
+ if pop != nil {
+ err = amendError(err, pop(*p))
+ }
+ popStep(p)
+ }
+
+ if err == Break {
+ err = nil
+ }
+ return err
+}
+
+func (o Options) rangeMap(p *protopath.Values, fd protoreflect.FieldDescriptor, ms protoreflect.Map, push, pop func(protopath.Values) error) (err error) {
+ keyOrder := order.AnyKeyOrder
+ if o.Stable {
+ keyOrder = order.GenericKeyOrder
+ }
+ order.RangeEntries(ms, keyOrder, func(k protoreflect.MapKey, v protoreflect.Value) bool {
+ pushStep(p, protopath.MapIndex(k), v)
+ if push != nil {
+ err = amendError(err, push(*p))
+ }
+ if err == nil && fd.MapValue().Message() != nil {
+ err = o.rangeMessage(p, v.Message(), push, pop)
+ }
+ if pop != nil {
+ err = amendError(err, pop(*p))
+ }
+ popStep(p)
+ return err == nil
+ })
+
+ if err == Break {
+ err = nil
+ }
+ return err
+}
+
+func pushStep(p *protopath.Values, s protopath.Step, v protoreflect.Value) {
+ p.Path = append(p.Path, s)
+ p.Values = append(p.Values, v)
+}
+
+func popStep(p *protopath.Values) {
+ p.Path = p.Path[:len(p.Path)-1]
+ p.Values = p.Values[:len(p.Values)-1]
+}
+
+// amendErrors amends the previous error with the current error if it is
+// considered more serious. The precedence order for errors is:
+// nil < Break < Terminate < previous non-nil < current non-nil
+func amendError(prev, curr error) error {
+ switch {
+ case curr == nil:
+ return prev
+ case curr == Break && prev != nil:
+ return prev
+ case curr == Terminate && prev != nil && prev != Break:
+ return prev
+ default:
+ return curr
+ }
+}
diff --git a/reflect/protorange/range_test.go b/reflect/protorange/range_test.go
new file mode 100644
index 0000000..a8ca6a0
--- /dev/null
+++ b/reflect/protorange/range_test.go
@@ -0,0 +1,253 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package protorange
+
+import (
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protopath"
+ "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/reflect/protoregistry"
+ "google.golang.org/protobuf/testing/protocmp"
+
+ newspb "google.golang.org/protobuf/internal/testprotos/news"
+ anypb "google.golang.org/protobuf/types/known/anypb"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+func mustMarshal(m proto.Message) []byte {
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
+
+var transformReflectValue = cmp.Transformer("", func(v protoreflect.Value) interface{} {
+ switch v := v.Interface().(type) {
+ case protoreflect.Message:
+ return v.Interface()
+ case protoreflect.Map:
+ ms := map[interface{}]protoreflect.Value{}
+ v.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
+ ms[k.Interface()] = v
+ return true
+ })
+ return ms
+ case protoreflect.List:
+ ls := []protoreflect.Value{}
+ for i := 0; i < v.Len(); i++ {
+ ls = append(ls, v.Get(i))
+ }
+ return ls
+ default:
+ return v
+ }
+})
+
+func TestRange(t *testing.T) {
+ m2 := (&newspb.KeyValueAttachment{
+ Name: "checksums.txt",
+ Data: map[string]string{
+ "go1.10.src.tar.gz": "07cbb9d0091b846c6aea40bf5bc0cea7",
+ "go1.10.darwin-amd64.pkg": "cbb38bb6ff6ea86279e01745984445bf",
+ "go1.10.linux-amd64.tar.gz": "6b3d0e4a5c77352cf4275573817f7566",
+ "go1.10.windows-amd64.msi": "57bda02030f58f5d2bf71943e1390123",
+ },
+ }).ProtoReflect()
+ m := (&newspb.Article{
+ Author: "Brad Fitzpatrick",
+ Date: timestamppb.New(time.Date(2018, time.February, 16, 0, 0, 0, 0, time.UTC)),
+ Title: "Go 1.10 is released",
+ Content: "Happy Friday, happy weekend! Today the Go team is happy to announce the release of Go 1.10...",
+ Status: newspb.Article_PUBLISHED,
+ Tags: []string{"go1.10", "release"},
+ Attachments: []*anypb.Any{{
+ TypeUrl: "google.golang.org.KeyValueAttachment",
+ Value: mustMarshal(m2.Interface()),
+ }},
+ }).ProtoReflect()
+
+ // Nil push and pop functions should not panic.
+ noop := func(protopath.Values) error { return nil }
+ Options{}.Range(m, nil, nil)
+ Options{}.Range(m, noop, nil)
+ Options{}.Range(m, nil, noop)
+
+ getByName := func(m protoreflect.Message, s protoreflect.Name) protoreflect.Value {
+ fds := m.Descriptor().Fields()
+ return m.Get(fds.ByName(s))
+ }
+
+ wantPaths := []string{
+ ``,
+ `.author`,
+ `.date`,
+ `.date.seconds`,
+ `.title`,
+ `.content`,
+ `.attachments`,
+ `.attachments[0]`,
+ `.attachments[0].(google.golang.org.KeyValueAttachment)`,
+ `.attachments[0].(google.golang.org.KeyValueAttachment).name`,
+ `.attachments[0].(google.golang.org.KeyValueAttachment).data`,
+ `.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.darwin-amd64.pkg"]`,
+ `.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.linux-amd64.tar.gz"]`,
+ `.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.src.tar.gz"]`,
+ `.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.windows-amd64.msi"]`,
+ `.tags`,
+ `.tags[0]`,
+ `.tags[1]`,
+ `.status`,
+ }
+ wantValues := []protoreflect.Value{
+ protoreflect.ValueOfMessage(m),
+ getByName(m, "author"),
+ getByName(m, "date"),
+ getByName(getByName(m, "date").Message(), "seconds"),
+ getByName(m, `title`),
+ getByName(m, `content`),
+ getByName(m, `attachments`),
+ getByName(m, `attachments`).List().Get(0),
+ protoreflect.ValueOfMessage(m2),
+ getByName(m2, `name`),
+ getByName(m2, `data`),
+ getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.darwin-amd64.pkg").MapKey()),
+ getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.linux-amd64.tar.gz").MapKey()),
+ getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.src.tar.gz").MapKey()),
+ getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.windows-amd64.msi").MapKey()),
+ getByName(m, `tags`),
+ getByName(m, `tags`).List().Get(0),
+ getByName(m, `tags`).List().Get(1),
+ getByName(m, `status`),
+ }
+
+ tests := []struct {
+ resolver interface {
+ protoregistry.ExtensionTypeResolver
+ protoregistry.MessageTypeResolver
+ }
+
+ errorAt int
+ breakAt int
+ terminateAt int
+
+ wantPaths []string
+ wantValues []protoreflect.Value
+ wantError error
+ }{{
+ wantPaths: wantPaths,
+ wantValues: wantValues,
+ }, {
+ resolver: (*protoregistry.Types)(nil),
+ wantPaths: append(append(wantPaths[:8:8],
+ `.attachments[0].type_url`,
+ `.attachments[0].value`,
+ ), wantPaths[15:]...),
+ wantValues: append(append(wantValues[:8:8],
+ protoreflect.ValueOfString("google.golang.org.KeyValueAttachment"),
+ protoreflect.ValueOfBytes(mustMarshal(m2.Interface())),
+ ), wantValues[15:]...),
+ }, {
+ errorAt: 5, // return error within newspb.Article
+ wantPaths: wantPaths[:5],
+ wantValues: wantValues[:5],
+ wantError: cmpopts.AnyError,
+ }, {
+ terminateAt: 11, // terminate within newspb.KeyValueAttachment
+ wantPaths: wantPaths[:11],
+ wantValues: wantValues[:11],
+ }, {
+ breakAt: 11, // break within newspb.KeyValueAttachment
+ wantPaths: append(wantPaths[:11:11], wantPaths[15:]...),
+ wantValues: append(wantValues[:11:11], wantValues[15:]...),
+ }, {
+ errorAt: 17, // return error within newspb.Article.Tags
+ wantPaths: wantPaths[:17],
+ wantValues: wantValues[:17],
+ wantError: cmpopts.AnyError,
+ }, {
+ breakAt: 17, // break within newspb.Article.Tags
+ wantPaths: append(wantPaths[:17:17], wantPaths[18:]...),
+ wantValues: append(wantValues[:17:17], wantValues[18:]...),
+ }, {
+ terminateAt: 17, // terminate within newspb.Article.Tags
+ wantPaths: wantPaths[:17],
+ wantValues: wantValues[:17],
+ }, {
+ errorAt: 13, // return error within newspb.KeyValueAttachment.Data
+ wantPaths: wantPaths[:13],
+ wantValues: wantValues[:13],
+ wantError: cmpopts.AnyError,
+ }, {
+ breakAt: 13, // break within newspb.KeyValueAttachment.Data
+ wantPaths: append(wantPaths[:13:13], wantPaths[15:]...),
+ wantValues: append(wantValues[:13:13], wantValues[15:]...),
+ }, {
+ terminateAt: 13, // terminate within newspb.KeyValueAttachment.Data
+ wantPaths: wantPaths[:13],
+ wantValues: wantValues[:13],
+ }}
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ var gotPaths []string
+ var gotValues []protoreflect.Value
+ var stackPaths []string
+ var stackValues []protoreflect.Value
+ gotError := Options{
+ Stable: true,
+ Resolver: tt.resolver,
+ }.Range(m,
+ func(p protopath.Values) error {
+ gotPaths = append(gotPaths, p.Path[1:].String())
+ stackPaths = append(stackPaths, p.Path[1:].String())
+ gotValues = append(gotValues, p.Index(-1).Value)
+ stackValues = append(stackValues, p.Index(-1).Value)
+ switch {
+ case tt.errorAt > 0 && tt.errorAt == len(gotPaths):
+ return cmpopts.AnyError
+ case tt.breakAt > 0 && tt.breakAt == len(gotPaths):
+ return Break
+ case tt.terminateAt > 0 && tt.terminateAt == len(gotPaths):
+ return Terminate
+ default:
+ return nil
+ }
+ },
+ func(p protopath.Values) error {
+ gotPath := p.Path[1:].String()
+ wantPath := stackPaths[len(stackPaths)-1]
+ if wantPath != gotPath {
+ t.Errorf("%d: pop path mismatch: got %v, want %v", len(gotPaths), gotPath, wantPath)
+ }
+ gotValue := p.Index(-1).Value
+ wantValue := stackValues[len(stackValues)-1]
+ if diff := cmp.Diff(wantValue, gotValue, transformReflectValue, protocmp.Transform()); diff != "" {
+ t.Errorf("%d: pop value mismatch (-want +got):\n%v", len(gotValues), diff)
+ }
+ stackPaths = stackPaths[:len(stackPaths)-1]
+ stackValues = stackValues[:len(stackValues)-1]
+ return nil
+ },
+ )
+ if n := len(stackPaths) + len(stackValues); n > 0 {
+ t.Errorf("stack mismatch: got %d unpopped items", n)
+ }
+ if diff := cmp.Diff(tt.wantPaths, gotPaths); diff != "" {
+ t.Errorf("paths mismatch (-want +got):\n%s", diff)
+ }
+ if diff := cmp.Diff(tt.wantValues, gotValues, transformReflectValue, protocmp.Transform()); diff != "" {
+ t.Errorf("values mismatch (-want +got):\n%s", diff)
+ }
+ if !cmp.Equal(gotError, tt.wantError, cmpopts.EquateErrors()) {
+ t.Errorf("error mismatch: got %v, want %v", gotError, tt.wantError)
+ }
+ })
+ }
+}
To view, visit change 236540. To unsubscribe, or for help writing mail filters, visit settings.