From 1e8e50f4f86c755c1a1147a929d9d51058f3bd1d Mon Sep 17 00:00:00 2001 From: Joshua Humphries Date: Mon, 22 Oct 2018 22:59:11 -0400 Subject: [PATCH] make MakeTemplate more robust (#60) --- cmd/grpcurl/grpcurl.go | 3 +- grpcurl.go | 83 ++++++++++++++++++++++++++++++++---------- grpcurl_test.go | 47 ++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 22 deletions(-) diff --git a/cmd/grpcurl/grpcurl.go b/cmd/grpcurl/grpcurl.go index 5a0031f..2f1b3ee 100644 --- a/cmd/grpcurl/grpcurl.go +++ b/cmd/grpcurl/grpcurl.go @@ -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) diff --git a/grpcurl.go b/grpcurl.go index a9579da..5d69437 100644 --- a/grpcurl.go +++ b/grpcurl.go @@ -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 diff --git a/grpcurl_test.go b/grpcurl_test.go index 2c6fab7..a0847f3 100644 --- a/grpcurl_test.go +++ b/grpcurl_test.go @@ -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) {