make MakeTemplate more robust

This commit is contained in:
Josh Humphries 2018-10-22 11:57:41 -04:00
parent 7cabe7a9d0
commit c2641f183b
3 changed files with 111 additions and 22 deletions

View File

@ -15,7 +15,6 @@ import (
"github.com/fullstorydev/grpcurl"
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
"github.com/jhump/protoreflect/desc"
"github.com/jhump/protoreflect/dynamic"
"github.com/jhump/protoreflect/grpcreflect"
"golang.org/x/net/context"
"google.golang.org/grpc"
@ -459,7 +458,7 @@ func main() {
if dsc, ok := dsc.(*desc.MessageDescriptor); ok && *msgTemplate {
// for messages, also show a template in JSON, to make it easier to
// create a request to invoke an RPC
tmpl := grpcurl.MakeTemplate(dynamic.NewMessage(dsc))
tmpl := grpcurl.MakeTemplate(dsc)
_, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format(*format), descSource, true, false, nil)
if err != nil {
fail(err, "Failed to construct formatter for %q", *format)

View File

@ -21,6 +21,9 @@ import (
"github.com/golang/protobuf/proto"
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/empty"
"github.com/golang/protobuf/ptypes/struct"
"github.com/jhump/protoreflect/desc"
"github.com/jhump/protoreflect/desc/protoprint"
"github.com/jhump/protoreflect/dynamic"
@ -352,35 +355,75 @@ func fullyConvertToDynamic(msgFact *dynamic.MessageFactory, msg proto.Message) (
return dm, nil
}
// MakeTemplate fleshes out the given message so that it is a suitable template
// for creating an instance of that message in JSON. In particular, it ensures
// that any repeated fields (which include map fields) are not empty, so they
// will render with a single element (to show the types and optionally nested
// fields). It also ensures that nested messages are not nil by setting them to
// a message that is also fleshed out as a template message.
func MakeTemplate(msg proto.Message) proto.Message {
return makeTemplate(msg, nil)
// MakeTemplate returns a message instance for the given descriptor that is a
// suitable template for creating an instance of that message in JSON. In
// particular, it ensures that any repeated fields (which include map fields)
// are not empty, so they will render with a single element (to show the types
// and optionally nested fields). It also ensures that nested messages are not
// nil by setting them to a message that is also fleshed out as a template
// message.
func MakeTemplate(md *desc.MessageDescriptor) proto.Message {
return makeTemplate(md, nil)
}
func makeTemplate(msg proto.Message, path []*desc.MessageDescriptor) proto.Message {
dm, ok := msg.(*dynamic.Message)
if !ok {
func makeTemplate(md *desc.MessageDescriptor, path []*desc.MessageDescriptor) proto.Message {
switch md.GetFullyQualifiedName() {
case "google.protobuf.Any":
// empty type URL is not allowed by JSON representation
// so we must give it a dummy type
msg, _ := ptypes.MarshalAny(&empty.Empty{})
return msg
}
// if a message is recursive structure, we don't want to blow the stack
for _, md := range path {
if md == dm.GetMessageDescriptor() {
// already visited this type; avoid infinite recursion
return msg
case "google.protobuf.Value":
// unset kind is not allowed by JSON representation
// so we must give it something
return &structpb.Value{
Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
Fields: map[string]*structpb.Value{
"google.protobuf.Value": {Kind: &structpb.Value_StringValue{
StringValue: "supports arbitrary JSON",
}},
},
}},
}
case "google.protobuf.ListValue":
return &structpb.ListValue{
Values: []*structpb.Value{
{
Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
Fields: map[string]*structpb.Value{
"google.protobuf.ListValue": {Kind: &structpb.Value_StringValue{
StringValue: "is an array of arbitrary JSON values",
}},
},
}},
},
},
}
case "google.protobuf.Struct":
return &structpb.Struct{
Fields: map[string]*structpb.Value{
"google.protobuf.Struct": {Kind: &structpb.Value_StringValue{
StringValue: "supports arbitrary JSON objects",
}},
},
}
}
dm := dynamic.NewMessage(md)
// if the message is a recursive structure, we don't want to blow the stack
for _, seen := range path {
if seen == md {
// already visited this type; avoid infinite recursion
return dm
}
}
path = append(path, dm.GetMessageDescriptor())
// for repeated fields, add a single element with default value
// and for message fields, add a message with all default fields
// that also has non-nil message and non-empty repeated fields
for _, fd := range dm.GetMessageDescriptor().GetFields() {
if fd.IsRepeated() {
switch fd.GetType() {
@ -420,10 +463,10 @@ func makeTemplate(msg proto.Message, path []*desc.MessageDescriptor) proto.Messa
case descpb.FieldDescriptorProto_TYPE_MESSAGE,
descpb.FieldDescriptorProto_TYPE_GROUP:
dm.AddRepeatedField(fd, makeTemplate(dynamic.NewMessage(fd.GetMessageType()), path))
dm.AddRepeatedField(fd, makeTemplate(fd.GetMessageType(), path))
}
} else if fd.GetMessageType() != nil {
dm.SetField(fd, makeTemplate(dynamic.NewMessage(fd.GetMessageType()), path))
dm.SetField(fd, makeTemplate(fd.GetMessageType(), path))
}
}
return dm

View File

@ -1,6 +1,7 @@
package grpcurl_test
import (
"encoding/json"
"fmt"
"io"
"net"
@ -11,6 +12,7 @@ import (
"time"
"github.com/golang/protobuf/jsonpb"
jsonpbtest "github.com/golang/protobuf/jsonpb/jsonpb_test_proto"
"github.com/golang/protobuf/proto"
"github.com/jhump/protoreflect/desc"
"github.com/jhump/protoreflect/grpcreflect"
@ -306,6 +308,51 @@ func fileNames(files []*desc.FileDescriptor) []string {
return names
}
const expectKnownType = `{
"dur": "0s",
"ts": "1970-01-01T00:00:00Z",
"dbl": 0,
"flt": 0,
"i64": "0",
"u64": "0",
"i32": 0,
"u32": 0,
"bool": false,
"str": "",
"bytes": null,
"st": {"google.protobuf.Struct": "supports arbitrary JSON objects"},
"an": {"@type": "type.googleapis.com/google.protobuf.Empty", "value": {}},
"lv": [{"google.protobuf.ListValue": "is an array of arbitrary JSON values"}],
"val": {"google.protobuf.Value": "supports arbitrary JSON"}
}`
func TestMakeTemplateKnownTypes(t *testing.T) {
descriptor, err := desc.LoadMessageDescriptorForMessage((*jsonpbtest.KnownTypes)(nil))
if err != nil {
t.Fatalf("failed to load descriptor: %v", err)
}
message := MakeTemplate(descriptor)
jsm := jsonpb.Marshaler{EmitDefaults: true}
out, err := jsm.MarshalToString(message)
if err != nil {
t.Fatalf("failed to marshal to JSON: %v", err)
}
// make sure template JSON matches expected
var actual, expected interface{}
if err := json.Unmarshal([]byte(out), &actual); err != nil {
t.Fatalf("failed to parse actual JSON: %v", err)
}
if err := json.Unmarshal([]byte(expectKnownType), &expected); err != nil {
t.Fatalf("failed to parse expected JSON: %v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("template message is not as expected; want:\n%s\ngot:\n%s", expectKnownType, out)
}
}
func TestDescribe(t *testing.T) {
for _, ds := range descSources {
t.Run(ds.name, func(t *testing.T) {