378 lines
12 KiB
Go
Raw Normal View History

// Copyright 2019 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 protocmp provides protobuf specific options for the
// [github.com/google/go-cmp/cmp] package.
//
// The primary feature is the [Transform] option, which transform [proto.Message]
// types into a [Message] map that is suitable for cmp to introspect upon.
// All other options in this package must be used in conjunction with [Transform].
package protocmp
import (
"reflect"
"strconv"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/internal/genid"
internal/msgfmt: use msgfmt package to format messages Port message formatting logic in testing/protocmp to internal/msgfmt and improve upon its output. This formatter is optimized for humanly readable output. It takes the best parts of both the JSON and proto text formats. The good of prototext: * It supports emitting unknown fields (useful for debugging). * It is relatively concise in the common-case since keys do not need to be represented as quoted strings (e.g., "key" vs key). The bad of prototext: * Requires relatively large dependency on encoding/prototext. * Our implementation lacks support for serializing packed lists. * Lacks support for readable maps. * Lacks support for readable Timestamp and Duration. The good of protojson: * First-class support for readable maps. * First-class support for readable Timestamp and Duration. The bad of protojson: * Requires relatively large dependency on encoding/protojson. * Lacks support for emitting unknown fields. * More verbose in the common-case as keys are quoted strings. The msgfmt package has the benefits of both protojson and prototext, but none of the detriments. It is a relatively simple implementation. This output is only intended for human consumption with no associated deserialization implementation. To avoid any illusion that this is identical to either the proto text or JSON formats, this always emits surrounding "{}" for top-level messages and the keys are not quoted strings. This CL does not use this format for generated Message.String methods as there is concerns about being inconsistent with the String methods as implemented in other languages. Having it be a seperate package makes it trivial to switch over to this if desired. Change-Id: I8b3581904d1624e84bf1b1954d2f01e5774b7f87 Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/223752 Reviewed-by: Damien Neil <dneil@google.com>
2020-02-28 18:37:16 -08:00
"google.golang.org/protobuf/internal/msgfmt"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/runtime/protoiface"
"google.golang.org/protobuf/runtime/protoimpl"
)
var (
enumV2Type = reflect.TypeOf((*protoreflect.Enum)(nil)).Elem()
messageV1Type = reflect.TypeOf((*protoiface.MessageV1)(nil)).Elem()
messageV2Type = reflect.TypeOf((*proto.Message)(nil)).Elem()
)
// Enum is a dynamic representation of a protocol buffer enum that is
// suitable for [cmp.Equal] and [cmp.Diff] to compare upon.
type Enum struct {
num protoreflect.EnumNumber
ed protoreflect.EnumDescriptor
}
// Descriptor returns the enum descriptor.
// It returns nil for a zero Enum value.
func (e Enum) Descriptor() protoreflect.EnumDescriptor {
return e.ed
}
// Number returns the enum value as an integer.
func (e Enum) Number() protoreflect.EnumNumber {
return e.num
}
// Equal reports whether e1 and e2 represent the same enum value.
func (e1 Enum) Equal(e2 Enum) bool {
if e1.ed.FullName() != e2.ed.FullName() {
return false
}
return e1.num == e2.num
}
// String returns the name of the enum value if known (e.g., "ENUM_VALUE"),
// otherwise it returns the formatted decimal enum number (e.g., "14").
func (e Enum) String() string {
if ev := e.ed.Values().ByNumber(e.num); ev != nil {
return string(ev.Name())
}
return strconv.Itoa(int(e.num))
}
const (
// messageTypeKey indicates the protobuf message type.
// The value type is always messageMeta.
// From the public API, it presents itself as only the type, but the
// underlying data structure holds arbitrary metadata about the message.
messageTypeKey = "@type"
// messageInvalidKey indicates that the message is invalid.
// The value is always the boolean "true".
messageInvalidKey = "@invalid"
)
type messageMeta struct {
m proto.Message
md protoreflect.MessageDescriptor
xds map[string]protoreflect.ExtensionDescriptor
}
func (t messageMeta) String() string {
return string(t.md.FullName())
}
func (t1 messageMeta) Equal(t2 messageMeta) bool {
return t1.md.FullName() == t2.md.FullName()
}
// Message is a dynamic representation of a protocol buffer message that is
// suitable for [cmp.Equal] and [cmp.Diff] to directly operate upon.
//
// Every populated known field (excluding extension fields) is stored in the map
// with the key being the short name of the field (e.g., "field_name") and
// the value determined by the kind and cardinality of the field.
//
// Singular scalars are represented by the same Go type as [protoreflect.Value],
// singular messages are represented by the [Message] type,
// singular enums are represented by the [Enum] type,
// list fields are represented as a Go slice, and
// map fields are represented as a Go map.
//
// Every populated extension field is stored in the map with the key being the
// full name of the field surrounded by brackets (e.g., "[extension.full.name]")
// and the value determined according to the same rules as known fields.
//
// Every unknown field is stored in the map with the key being the field number
// encoded as a decimal string (e.g., "132") and the value being the raw bytes
// of the encoded field (as the [protoreflect.RawFields] type).
//
// Message values must not be created by or mutated by users.
type Message map[string]any
// Unwrap returns the original message value.
// It returns nil if this Message was not constructed from another message.
func (m Message) Unwrap() proto.Message {
mm, _ := m[messageTypeKey].(messageMeta)
return mm.m
}
// Descriptor return the message descriptor.
// It returns nil for a zero Message value.
func (m Message) Descriptor() protoreflect.MessageDescriptor {
mm, _ := m[messageTypeKey].(messageMeta)
return mm.md
}
// ProtoReflect returns a reflective view of m.
// It only implements the read-only operations of [protoreflect.Message].
// Calling any mutating operations on m panics.
func (m Message) ProtoReflect() protoreflect.Message {
return (reflectMessage)(m)
}
// ProtoMessage is a marker method from the legacy message interface.
func (m Message) ProtoMessage() {}
// Reset is the required Reset method from the legacy message interface.
func (m Message) Reset() {
panic("invalid mutation of a read-only message")
}
// String returns a formatted string for the message.
// It is intended for human debugging and has no guarantees about its
// exact format or the stability of its output.
func (m Message) String() string {
switch {
case m == nil:
return "<nil>"
case !m.ProtoReflect().IsValid():
return "<invalid>"
default:
internal/msgfmt: use msgfmt package to format messages Port message formatting logic in testing/protocmp to internal/msgfmt and improve upon its output. This formatter is optimized for humanly readable output. It takes the best parts of both the JSON and proto text formats. The good of prototext: * It supports emitting unknown fields (useful for debugging). * It is relatively concise in the common-case since keys do not need to be represented as quoted strings (e.g., "key" vs key). The bad of prototext: * Requires relatively large dependency on encoding/prototext. * Our implementation lacks support for serializing packed lists. * Lacks support for readable maps. * Lacks support for readable Timestamp and Duration. The good of protojson: * First-class support for readable maps. * First-class support for readable Timestamp and Duration. The bad of protojson: * Requires relatively large dependency on encoding/protojson. * Lacks support for emitting unknown fields. * More verbose in the common-case as keys are quoted strings. The msgfmt package has the benefits of both protojson and prototext, but none of the detriments. It is a relatively simple implementation. This output is only intended for human consumption with no associated deserialization implementation. To avoid any illusion that this is identical to either the proto text or JSON formats, this always emits surrounding "{}" for top-level messages and the keys are not quoted strings. This CL does not use this format for generated Message.String methods as there is concerns about being inconsistent with the String methods as implemented in other languages. Having it be a seperate package makes it trivial to switch over to this if desired. Change-Id: I8b3581904d1624e84bf1b1954d2f01e5774b7f87 Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/223752 Reviewed-by: Damien Neil <dneil@google.com>
2020-02-28 18:37:16 -08:00
return msgfmt.Format(m)
}
}
type transformer struct {
resolver protoregistry.MessageTypeResolver
}
func newTransformer(opts ...option) *transformer {
xf := &transformer{
resolver: protoregistry.GlobalTypes,
}
for _, opt := range opts {
opt(xf)
}
return xf
}
type option func(*transformer)
// MessageTypeResolver overrides the resolver used for messages packed
// inside Any. The default is protoregistry.GlobalTypes, which is
// sufficient for all compiled-in Protobuf messages. Overriding the
// resolver is useful in tests that dynamically create Protobuf
// descriptors and messages, e.g. in proxies using dynamicpb.
func MessageTypeResolver(r protoregistry.MessageTypeResolver) option {
return func(xf *transformer) {
xf.resolver = r
}
}
// Transform returns a [cmp.Option] that converts each [proto.Message] to a [Message].
// The transformation does not mutate nor alias any converted messages.
//
// The google.protobuf.Any message is automatically unmarshaled such that the
// "value" field is a [Message] representing the underlying message value
// assuming it could be resolved and properly unmarshaled.
//
// This does not directly transform higher-order composite Go types.
// For example, []*foopb.Message is not transformed into []Message,
// but rather the individual message elements of the slice are transformed.
func Transform(opts ...option) cmp.Option {
xf := newTransformer(opts...)
// addrType returns a pointer to t if t isn't a pointer or interface.
addrType := func(t reflect.Type) reflect.Type {
if k := t.Kind(); k == reflect.Interface || k == reflect.Ptr {
return t
}
return reflect.PtrTo(t)
}
// TODO: Should this transform protoreflect.Enum types to Enum as well?
return cmp.FilterPath(func(p cmp.Path) bool {
ps := p.Last()
if isMessageType(addrType(ps.Type())) {
return true
}
// Check whether the concrete values of an interface both satisfy
// the Message interface.
if ps.Type().Kind() == reflect.Interface {
vx, vy := ps.Values()
if !vx.IsValid() || vx.IsNil() || !vy.IsValid() || vy.IsNil() {
return false
}
return isMessageType(addrType(vx.Elem().Type())) && isMessageType(addrType(vy.Elem().Type()))
}
return false
}, cmp.Transformer("protocmp.Transform", func(v any) Message {
// For user convenience, shallow copy the message value if necessary
// in order for it to implement the message interface.
if rv := reflect.ValueOf(v); rv.IsValid() && rv.Kind() != reflect.Ptr && !isMessageType(rv.Type()) {
pv := reflect.New(rv.Type())
pv.Elem().Set(rv)
v = pv.Interface()
}
m := protoimpl.X.MessageOf(v)
switch {
case m == nil:
return nil
case !m.IsValid():
return Message{messageTypeKey: messageMeta{m: m.Interface(), md: m.Descriptor()}, messageInvalidKey: true}
default:
return xf.transformMessage(m)
}
}))
}
func isMessageType(t reflect.Type) bool {
// Avoid transforming the Message itself.
if t == reflect.TypeOf(Message(nil)) || t == reflect.TypeOf((*Message)(nil)) {
return false
}
return t.Implements(messageV1Type) || t.Implements(messageV2Type)
}
func (xf *transformer) transformMessage(m protoreflect.Message) Message {
mx := Message{}
mt := messageMeta{m: m.Interface(), md: m.Descriptor(), xds: make(map[string]protoreflect.FieldDescriptor)}
// Handle known and extension fields.
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
s := fd.TextName()
if fd.IsExtension() {
mt.xds[s] = fd
}
switch {
case fd.IsList():
mx[s] = xf.transformList(fd, v.List())
case fd.IsMap():
mx[s] = xf.transformMap(fd, v.Map())
default:
mx[s] = xf.transformSingular(fd, v)
}
return true
})
// Handle unknown fields.
for b := m.GetUnknown(); len(b) > 0; {
num, _, n := protowire.ConsumeField(b)
s := strconv.Itoa(int(num))
b2, _ := mx[s].(protoreflect.RawFields)
mx[s] = append(b2, b[:n]...)
b = b[n:]
}
// Expand Any messages.
if mt.md.FullName() == genid.Any_message_fullname {
s, _ := mx[string(genid.Any_TypeUrl_field_name)].(string)
b, _ := mx[string(genid.Any_Value_field_name)].([]byte)
mt, err := xf.resolver.FindMessageByURL(s)
if mt != nil && err == nil {
m2 := mt.New()
err := proto.UnmarshalOptions{AllowPartial: true}.Unmarshal(b, m2.Interface())
if err == nil {
mx[string(genid.Any_Value_field_name)] = xf.transformMessage(m2)
}
}
}
mx[messageTypeKey] = mt
return mx
}
func (xf *transformer) transformList(fd protoreflect.FieldDescriptor, lv protoreflect.List) any {
t := protoKindToGoType(fd.Kind())
rv := reflect.MakeSlice(reflect.SliceOf(t), lv.Len(), lv.Len())
for i := 0; i < lv.Len(); i++ {
v := reflect.ValueOf(xf.transformSingular(fd, lv.Get(i)))
rv.Index(i).Set(v)
}
return rv.Interface()
}
func (xf *transformer) transformMap(fd protoreflect.FieldDescriptor, mv protoreflect.Map) any {
kfd := fd.MapKey()
vfd := fd.MapValue()
kt := protoKindToGoType(kfd.Kind())
vt := protoKindToGoType(vfd.Kind())
rv := reflect.MakeMapWithSize(reflect.MapOf(kt, vt), mv.Len())
mv.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
kv := reflect.ValueOf(xf.transformSingular(kfd, k.Value()))
vv := reflect.ValueOf(xf.transformSingular(vfd, v))
rv.SetMapIndex(kv, vv)
return true
})
return rv.Interface()
}
func (xf *transformer) transformSingular(fd protoreflect.FieldDescriptor, v protoreflect.Value) any {
switch fd.Kind() {
case protoreflect.EnumKind:
return Enum{num: v.Enum(), ed: fd.Enum()}
case protoreflect.MessageKind, protoreflect.GroupKind:
return xf.transformMessage(v.Message())
case protoreflect.BytesKind:
// The protoreflect API does not specify whether an empty bytes is
// guaranteed to be nil or not. Always return non-nil bytes to avoid
// leaking information about the concrete proto.Message implementation.
if len(v.Bytes()) == 0 {
return []byte{}
}
return v.Bytes()
default:
return v.Interface()
}
}
func protoKindToGoType(k protoreflect.Kind) reflect.Type {
switch k {
case protoreflect.BoolKind:
return reflect.TypeOf(bool(false))
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
return reflect.TypeOf(int32(0))
case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
return reflect.TypeOf(int64(0))
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
return reflect.TypeOf(uint32(0))
case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
return reflect.TypeOf(uint64(0))
case protoreflect.FloatKind:
return reflect.TypeOf(float32(0))
case protoreflect.DoubleKind:
return reflect.TypeOf(float64(0))
case protoreflect.StringKind:
return reflect.TypeOf(string(""))
case protoreflect.BytesKind:
return reflect.TypeOf([]byte(nil))
case protoreflect.EnumKind:
return reflect.TypeOf(Enum{})
case protoreflect.MessageKind, protoreflect.GroupKind:
return reflect.TypeOf(Message{})
default:
panic("invalid kind")
}
}