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
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/236540
Trust: Joe Tsai <joetsai@digital-static.net>
Reviewed-by: Damien Neil <dneil@google.com>
This commit is contained in:
Joe Tsai 2020-05-11 17:47:32 -07:00
parent 31610fd913
commit e4fcb9f7e7
9 changed files with 1714 additions and 4 deletions

View File

@ -263,3 +263,8 @@ func (e *Encoder) Snapshot() encoderState {
func (e *Encoder) Reset(es encoderState) { func (e *Encoder) Reset(es encoderState) {
e.encoderState = es 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)
}

View File

@ -30,8 +30,15 @@ func Format(m proto.Message) string {
return string(appendMessage(nil, m.ProtoReflect())) 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 { func appendValue(b []byte, v protoreflect.Value, fd protoreflect.FieldDescriptor) []byte {
switch v := v.Interface().(type) { switch v := v.Interface().(type) {
case nil:
return append(b, "<invalid>"...)
case bool, int32, int64, uint32, uint64, float32, float64: case bool, int32, int64, uint32, uint64, float32, float64:
return append(b, fmt.Sprint(v)...) return append(b, fmt.Sprint(v)...)
case string: case string:
@ -39,7 +46,7 @@ func appendValue(b []byte, v protoreflect.Value, fd protoreflect.FieldDescriptor
case []byte: case []byte:
return append(b, strconv.Quote(string(v))...) return append(b, strconv.Quote(string(v))...)
case protoreflect.EnumNumber: case protoreflect.EnumNumber:
return appendEnum(b, v, fd.Enum()) return appendEnum(b, v, fd)
case protoreflect.Message: case protoreflect.Message:
return appendMessage(b, v) return appendMessage(b, v)
case protoreflect.List: case protoreflect.List:
@ -51,9 +58,11 @@ func appendValue(b []byte, v protoreflect.Value, fd protoreflect.FieldDescriptor
} }
} }
func appendEnum(b []byte, v protoreflect.EnumNumber, ed protoreflect.EnumDescriptor) []byte { func appendEnum(b []byte, v protoreflect.EnumNumber, fd protoreflect.FieldDescriptor) []byte {
if ev := ed.Values().ByNumber(v); ev != nil { if fd != nil {
return append(b, ev.Name()...) if ev := fd.Enum().Values().ByNumber(v); ev != nil {
return append(b, ev.Name()...)
}
} }
return strconv.AppendInt(b, int64(v), 10) return strconv.AppendInt(b, int64(v), 10)
} }

View File

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

View File

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

121
reflect/protopath/path.go Normal file
View File

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

241
reflect/protopath/step.go Normal file
View File

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

View File

@ -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 {
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.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:
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
},
func(p protopath.Values) 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..."
// 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]"
// }
// }
// ]
// }
}

315
reflect/protorange/range.go Normal file
View File

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

View File

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