From 7cabe7a9d035e524a92c27951d226fa3eb7aa90c Mon Sep 17 00:00:00 2001 From: Joshua Humphries Date: Fri, 19 Oct 2018 14:47:25 -0400 Subject: [PATCH] move more stuff from cmd to package (#59) --- cmd/grpcurl/grpcurl.go | 318 ++---------------- format.go | 293 ++++++++++++++++ .../formatting_test.go => format_test.go | 51 +-- grpcurl.go | 78 +++++ 4 files changed, 422 insertions(+), 318 deletions(-) create mode 100644 format.go rename cmd/grpcurl/formatting_test.go => format_test.go (85%) diff --git a/cmd/grpcurl/grpcurl.go b/cmd/grpcurl/grpcurl.go index ed12176..5a0031f 100644 --- a/cmd/grpcurl/grpcurl.go +++ b/cmd/grpcurl/grpcurl.go @@ -4,9 +4,6 @@ package main import ( - "bufio" - "bytes" - "encoding/json" "flag" "fmt" "io" @@ -15,8 +12,7 @@ import ( "strings" "time" - "github.com/golang/protobuf/jsonpb" - "github.com/golang/protobuf/proto" + "github.com/fullstorydev/grpcurl" descpb "github.com/golang/protobuf/protoc-gen-go/descriptor" "github.com/jhump/protoreflect/desc" "github.com/jhump/protoreflect/dynamic" @@ -28,9 +24,6 @@ import ( "google.golang.org/grpc/keepalive" "google.golang.org/grpc/metadata" reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - "google.golang.org/grpc/status" - - "github.com/fullstorydev/grpcurl" ) var version = "dev build " @@ -466,21 +459,17 @@ 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 := makeTemplate(dynamic.NewMessage(dsc)) - fmt.Println("\nMessage template:") - if *format == "json" { - jsm := jsonpb.Marshaler{Indent: " ", EmitDefaults: true} - err := jsm.Marshal(os.Stdout, tmpl) - if err != nil { - fail(err, "Failed to print template for message %s", s) - } - } else /* *format == "text" */ { - err := proto.MarshalText(os.Stdout, tmpl) - if err != nil { - fail(err, "Failed to print template for message %s", s) - } + tmpl := grpcurl.MakeTemplate(dynamic.NewMessage(dsc)) + _, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format(*format), descSource, true, false, nil) + if err != nil { + fail(err, "Failed to construct formatter for %q", *format) } - fmt.Println() + str, err := formatter(tmpl) + if err != nil { + fail(err, "Failed to print template for message %s", s) + } + fmt.Println("\nMessage template:") + fmt.Println(str) } } @@ -496,32 +485,34 @@ func main() { in = strings.NewReader(*data) } - rf, formatter := formatDetails(*format, descSource, *verbose, in) - h := handler{ - out: os.Stdout, - descSource: descSource, - formatter: formatter, - verbose: *verbose, + // if not verbose output, then also include record delimiters + // between each message, so output could potentially be piped + // to another grpcurl process + includeSeparators := !*verbose + rf, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format(*format), descSource, *emitDefaults, includeSeparators, in) + if err != nil { + fail(err, "Failed to construct request parser and formatter for %q", *format) } + h := grpcurl.NewDefaultEventHandler(os.Stdout, descSource, formatter, *verbose) - err := grpcurl.InvokeRPC(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), &h, rf.next) + err = grpcurl.InvokeRPC(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), h, rf.Next) if err != nil { fail(err, "Error invoking method %q", symbol) } reqSuffix := "" respSuffix := "" - reqCount := rf.numRequests() + reqCount := rf.NumRequests() if reqCount != 1 { reqSuffix = "s" } - if h.respCount != 1 { + if h.NumResponses != 1 { respSuffix = "s" } if *verbose { - fmt.Printf("Sent %d request%s and received %d response%s\n", reqCount, reqSuffix, h.respCount, respSuffix) + fmt.Printf("Sent %d request%s and received %d response%s\n", reqCount, reqSuffix, h.NumResponses, respSuffix) } - if h.stat.Code() != codes.OK { - fmt.Fprintf(os.Stderr, "ERROR:\n Code: %s\n Message: %s\n", h.stat.Code().String(), h.stat.Message()) + if h.Status.Code() != codes.OK { + fmt.Fprintf(os.Stderr, "ERROR:\n Code: %s\n Message: %s\n", h.Status.Code().String(), h.Status.Message()) exit(1) } } @@ -597,262 +588,3 @@ func fail(err error, msg string, args ...interface{}) { exit(2) } } - -func anyResolver(source grpcurl.DescriptorSource) (jsonpb.AnyResolver, error) { - files, err := grpcurl.GetAllFiles(source) - if err != nil { - return nil, err - } - - var er dynamic.ExtensionRegistry - for _, fd := range files { - er.AddExtensionsFromFile(fd) - } - mf := dynamic.NewMessageFactoryWithExtensionRegistry(&er) - return dynamic.AnyResolver(mf, files...), nil -} - -func formatDetails(format string, descSource grpcurl.DescriptorSource, verbose bool, in io.Reader) (requestFactory, func(proto.Message) (string, error)) { - if format == "json" { - resolver, err := anyResolver(descSource) - if err != nil { - fail(err, "Error creating message resolver") - } - marshaler := jsonpb.Marshaler{ - EmitDefaults: *emitDefaults, - Indent: " ", - AnyResolver: resolver, - } - return newJsonFactory(in, resolver), marshaler.MarshalToString - } - /* else *format == "text" */ - - // if not verbose output, then also include record delimiters - // before each message (other than the first) so output could - // potentially piped to another grpcurl process - tf := textFormatter{useSeparator: !verbose} - return newTextFactory(in), tf.format -} - -type handler struct { - out io.Writer - descSource grpcurl.DescriptorSource - respCount int - stat *status.Status - formatter func(proto.Message) (string, error) - verbose bool -} - -func (h *handler) OnResolveMethod(md *desc.MethodDescriptor) { - if h.verbose { - txt, err := grpcurl.GetDescriptorText(md, h.descSource) - if err == nil { - fmt.Fprintf(h.out, "\nResolved method descriptor:\n%s\n", txt) - } - } -} - -func (h *handler) OnSendHeaders(md metadata.MD) { - if h.verbose { - fmt.Fprintf(h.out, "\nRequest metadata to send:\n%s\n", grpcurl.MetadataToString(md)) - } -} - -func (h *handler) OnReceiveHeaders(md metadata.MD) { - if h.verbose { - fmt.Fprintf(h.out, "\nResponse headers received:\n%s\n", grpcurl.MetadataToString(md)) - } -} - -func (h *handler) OnReceiveResponse(resp proto.Message) { - h.respCount++ - if h.verbose { - fmt.Fprint(h.out, "\nResponse contents:\n") - } - respStr, err := h.formatter(resp) - if err != nil { - fail(err, "failed to generate %s form of response message", *format) - } - fmt.Fprintln(h.out, respStr) -} - -func (h *handler) OnReceiveTrailers(stat *status.Status, md metadata.MD) { - h.stat = stat - if h.verbose { - fmt.Fprintf(h.out, "\nResponse trailers received:\n%s\n", grpcurl.MetadataToString(md)) - } -} - -// 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 { - dm, ok := msg.(*dynamic.Message) - if !ok { - return msg - } - // 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() { - case descpb.FieldDescriptorProto_TYPE_FIXED32, - descpb.FieldDescriptorProto_TYPE_UINT32: - dm.AddRepeatedField(fd, uint32(0)) - - case descpb.FieldDescriptorProto_TYPE_SFIXED32, - descpb.FieldDescriptorProto_TYPE_SINT32, - descpb.FieldDescriptorProto_TYPE_INT32, - descpb.FieldDescriptorProto_TYPE_ENUM: - dm.AddRepeatedField(fd, int32(0)) - - case descpb.FieldDescriptorProto_TYPE_FIXED64, - descpb.FieldDescriptorProto_TYPE_UINT64: - dm.AddRepeatedField(fd, uint64(0)) - - case descpb.FieldDescriptorProto_TYPE_SFIXED64, - descpb.FieldDescriptorProto_TYPE_SINT64, - descpb.FieldDescriptorProto_TYPE_INT64: - dm.AddRepeatedField(fd, int64(0)) - - case descpb.FieldDescriptorProto_TYPE_STRING: - dm.AddRepeatedField(fd, "") - - case descpb.FieldDescriptorProto_TYPE_BYTES: - dm.AddRepeatedField(fd, []byte{}) - - case descpb.FieldDescriptorProto_TYPE_BOOL: - dm.AddRepeatedField(fd, false) - - case descpb.FieldDescriptorProto_TYPE_FLOAT: - dm.AddRepeatedField(fd, float32(0)) - - case descpb.FieldDescriptorProto_TYPE_DOUBLE: - dm.AddRepeatedField(fd, float64(0)) - - case descpb.FieldDescriptorProto_TYPE_MESSAGE, - descpb.FieldDescriptorProto_TYPE_GROUP: - dm.AddRepeatedField(fd, makeTemplate(dynamic.NewMessage(fd.GetMessageType()))) - } - } else if fd.GetMessageType() != nil { - dm.SetField(fd, makeTemplate(dynamic.NewMessage(fd.GetMessageType()))) - } - } - return dm -} - -type requestFactory interface { - next(proto.Message) error - numRequests() int -} - -type jsonFactory struct { - dec *json.Decoder - unmarshaler jsonpb.Unmarshaler - requestCount int -} - -func newJsonFactory(in io.Reader, resolver jsonpb.AnyResolver) *jsonFactory { - return &jsonFactory{ - dec: json.NewDecoder(in), - unmarshaler: jsonpb.Unmarshaler{AnyResolver: resolver}, - } -} - -func (f *jsonFactory) next(m proto.Message) error { - var msg json.RawMessage - if err := f.dec.Decode(&msg); err != nil { - return err - } - f.requestCount++ - return f.unmarshaler.Unmarshal(bytes.NewReader(msg), m) -} - -func (f *jsonFactory) numRequests() int { - return f.requestCount -} - -const ( - textSeparatorChar = 0x1e -) - -type textFactory struct { - r *bufio.Reader - err error - requestCount int -} - -func newTextFactory(in io.Reader) *textFactory { - return &textFactory{r: bufio.NewReader(in)} -} - -func (f *textFactory) next(m proto.Message) error { - if f.err != nil { - return f.err - } - - var b []byte - b, f.err = f.r.ReadBytes(textSeparatorChar) - if f.err != nil && f.err != io.EOF { - return f.err - } - // remove delimiter - if len(b) > 0 && b[len(b)-1] == textSeparatorChar { - b = b[:len(b)-1] - } - - f.requestCount++ - - return proto.UnmarshalText(string(b), m) -} - -func (f *textFactory) numRequests() int { - return f.requestCount -} - -type textFormatter struct { - useSeparator bool - numFormatted int -} - -func (tf *textFormatter) format(m proto.Message) (string, error) { - var buf bytes.Buffer - if tf.useSeparator && tf.numFormatted > 0 { - if err := buf.WriteByte(textSeparatorChar); err != nil { - return "", err - } - } - - // If message implements MarshalText method (such as a *dynamic.Message), - // it won't get details about whether or not to format to text compactly - // or with indentation. So first see if the message also implements a - // MarshalTextIndent method and use that instead if available. - type indentMarshaler interface { - MarshalTextIndent() ([]byte, error) - } - - if indenter, ok := m.(indentMarshaler); ok { - b, err := indenter.MarshalTextIndent() - if err != nil { - return "", err - } - if _, err := buf.Write(b); err != nil { - return "", err - } - } else if err := proto.MarshalText(&buf, m); err != nil { - return "", err - } - - // no trailing newline needed - str := buf.String() - if str[len(str)-1] == '\n' { - str = str[:len(str)-1] - } - - tf.numFormatted++ - - return str, nil -} diff --git a/format.go b/format.go new file mode 100644 index 0000000..862789a --- /dev/null +++ b/format.go @@ -0,0 +1,293 @@ +package grpcurl + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + + "github.com/golang/protobuf/jsonpb" + "github.com/golang/protobuf/proto" + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/dynamic" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// RequestParser processes input into messages. +type RequestParser interface { + // Next parses input data into the given request message. If called after + // input is exhausted, it returns io.EOF. If the caller re-uses the same + // instance in multiple calls to Next, it should call msg.Reset() in between + // each call. + Next(msg proto.Message) error + // NumRequests returns the number of messages that have been parsed and + // returned by a call to Next. + NumRequests() int +} + +type jsonRequestParser struct { + dec *json.Decoder + unmarshaler jsonpb.Unmarshaler + requestCount int +} + +// NewJSONRequestParser returns a RequestParser that reads data in JSON format +// from the given reader. The given resolver is used to assist with decoding of +// google.protobuf.Any messages. +// +// Input data that contains more than one message should just include all +// messages concatenated (though whitespace is necessary to separate some kinds +// of values in JSON). +// +// If the given reader has no data, the returned parser will return io.EOF on +// the very first call. +func NewJSONRequestParser(in io.Reader, resolver jsonpb.AnyResolver) RequestParser { + return &jsonRequestParser{ + dec: json.NewDecoder(in), + unmarshaler: jsonpb.Unmarshaler{AnyResolver: resolver}, + } +} + +func (f *jsonRequestParser) Next(m proto.Message) error { + var msg json.RawMessage + if err := f.dec.Decode(&msg); err != nil { + return err + } + f.requestCount++ + return f.unmarshaler.Unmarshal(bytes.NewReader(msg), m) +} + +func (f *jsonRequestParser) NumRequests() int { + return f.requestCount +} + +const ( + textSeparatorChar = 0x1e +) + +type textRequestParser struct { + r *bufio.Reader + err error + requestCount int +} + +// NewTextRequestParser returns a RequestParser that reads data in the protobuf +// text format from the given reader. +// +// Input data that contains more than one message should include an ASCII +// 'Record Separator' character (0x1E) between each message. +// +// Empty text is a valid text format and represents an empty message. So if the +// given reader has no data, the returned parser will yield an empty message +// for the first call to Next and then return io.EOF thereafter. This also means +// that if the input data ends with a record separator, then a final empty +// message will be parsed *after* the separator. +func NewTextRequestParser(in io.Reader) RequestParser { + return &textRequestParser{r: bufio.NewReader(in)} +} + +func (f *textRequestParser) Next(m proto.Message) error { + if f.err != nil { + return f.err + } + + var b []byte + b, f.err = f.r.ReadBytes(textSeparatorChar) + if f.err != nil && f.err != io.EOF { + return f.err + } + // remove delimiter + if len(b) > 0 && b[len(b)-1] == textSeparatorChar { + b = b[:len(b)-1] + } + + f.requestCount++ + + return proto.UnmarshalText(string(b), m) +} + +func (f *textRequestParser) NumRequests() int { + return f.requestCount +} + +// Formatter translates messages into string representations. +type Formatter func(proto.Message) (string, error) + +// NewJSONFormatter returns a formatter that returns JSON strings. The JSON will +// include empty/default values (instead of just omitted them) if emitDefaults +// is true. The given resolver is used to assist with encoding of +// google.protobuf.Any messages. +func NewJSONFormatter(emitDefaults bool, resolver jsonpb.AnyResolver) Formatter { + marshaler := jsonpb.Marshaler{ + EmitDefaults: emitDefaults, + Indent: " ", + AnyResolver: resolver, + } + return marshaler.MarshalToString +} + +// NewTextFormatter returns a formatter that returns strings in the protobuf +// text format. If includeSeparator is true then, when invoked to format +// multiple messages, all messages after the first one will be prefixed with the +// ASCII 'Record Separator' character (0x1E). +func NewTextFormatter(includeSeparator bool) Formatter { + tf := textFormatter{useSeparator: includeSeparator} + return tf.format +} + +type textFormatter struct { + useSeparator bool + numFormatted int +} + +func (tf *textFormatter) format(m proto.Message) (string, error) { + var buf bytes.Buffer + if tf.useSeparator && tf.numFormatted > 0 { + if err := buf.WriteByte(textSeparatorChar); err != nil { + return "", err + } + } + + // If message implements MarshalText method (such as a *dynamic.Message), + // it won't get details about whether or not to format to text compactly + // or with indentation. So first see if the message also implements a + // MarshalTextIndent method and use that instead if available. + type indentMarshaler interface { + MarshalTextIndent() ([]byte, error) + } + + if indenter, ok := m.(indentMarshaler); ok { + b, err := indenter.MarshalTextIndent() + if err != nil { + return "", err + } + if _, err := buf.Write(b); err != nil { + return "", err + } + } else if err := proto.MarshalText(&buf, m); err != nil { + return "", err + } + + // no trailing newline needed + str := buf.String() + if str[len(str)-1] == '\n' { + str = str[:len(str)-1] + } + + tf.numFormatted++ + + return str, nil +} + +type Format string + +const ( + FormatJSON = Format("json") + FormatText = Format("text") +) + +func anyResolver(source DescriptorSource) (jsonpb.AnyResolver, error) { + files, err := GetAllFiles(source) + if err != nil { + return nil, err + } + + var er dynamic.ExtensionRegistry + for _, fd := range files { + er.AddExtensionsFromFile(fd) + } + mf := dynamic.NewMessageFactoryWithExtensionRegistry(&er) + return dynamic.AnyResolver(mf, files...), nil +} + +// RequestParserAndFormatterFor returns a request parser and formatter for the +// given format. The given descriptor source may be used for parsing message +// data (if needed by the format). The flags emitJSONDefaultFields and +// includeTextSeparator are options for JSON and protobuf text formats, +// respectively. Requests will be parsed from the given in. +func RequestParserAndFormatterFor(format Format, descSource DescriptorSource, emitJSONDefaultFields, includeTextSeparator bool, in io.Reader) (RequestParser, Formatter, error) { + switch format { + case FormatJSON: + resolver, err := anyResolver(descSource) + if err != nil { + return nil, nil, fmt.Errorf("error creating message resolver: %v", err) + } + return NewJSONRequestParser(in, resolver), NewJSONFormatter(emitJSONDefaultFields, resolver), nil + case FormatText: + return NewTextRequestParser(in), NewTextFormatter(includeTextSeparator), nil + default: + return nil, nil, fmt.Errorf("unknown format: %s", format) + } +} + +// DefaultEventHandler logs events to a writer. This is not thread-safe, but is +// safe for use with InvokeRPC as long as NumResponses and Status are not read +// until the call to InvokeRPC completes. +type DefaultEventHandler struct { + out io.Writer + descSource DescriptorSource + formatter func(proto.Message) (string, error) + verbose bool + + // NumResponses is the number of responses that have been received. + NumResponses int + // Status is the status that was received at the end of an RPC. It is + // nil if the RPC is still in progress. + Status *status.Status +} + +// NewDefaultEventHandler returns an InvocationEventHandler that logs events to +// the given output. If verbose is true, all events are logged. Otherwise, only +// response messages are logged. +func NewDefaultEventHandler(out io.Writer, descSource DescriptorSource, formatter Formatter, verbose bool) *DefaultEventHandler { + return &DefaultEventHandler{ + out: out, + descSource: descSource, + formatter: formatter, + verbose: verbose, + } +} + +var _ InvocationEventHandler = (*DefaultEventHandler)(nil) + +func (h *DefaultEventHandler) OnResolveMethod(md *desc.MethodDescriptor) { + if h.verbose { + txt, err := GetDescriptorText(md, h.descSource) + if err == nil { + fmt.Fprintf(h.out, "\nResolved method descriptor:\n%s\n", txt) + } + } +} + +func (h *DefaultEventHandler) OnSendHeaders(md metadata.MD) { + if h.verbose { + fmt.Fprintf(h.out, "\nRequest metadata to send:\n%s\n", MetadataToString(md)) + } +} + +func (h *DefaultEventHandler) OnReceiveHeaders(md metadata.MD) { + if h.verbose { + fmt.Fprintf(h.out, "\nResponse headers received:\n%s\n", MetadataToString(md)) + } +} + +func (h *DefaultEventHandler) OnReceiveResponse(resp proto.Message) { + h.NumResponses++ + if h.verbose { + fmt.Fprint(h.out, "\nResponse contents:\n") + } + if respStr, err := h.formatter(resp); err != nil { + fmt.Fprintf(h.out, "Failed to format response message %d: %v\n", h.NumResponses, err) + } else { + fmt.Fprintln(h.out, respStr) + } +} + +func (h *DefaultEventHandler) OnReceiveTrailers(stat *status.Status, md metadata.MD) { + h.Status = stat + if h.verbose { + fmt.Fprintf(h.out, "\nResponse trailers received:\n%s\n", MetadataToString(md)) + } +} diff --git a/cmd/grpcurl/formatting_test.go b/format_test.go similarity index 85% rename from cmd/grpcurl/formatting_test.go rename to format_test.go index ba7a58b..e1d4ca9 100644 --- a/cmd/grpcurl/formatting_test.go +++ b/format_test.go @@ -1,4 +1,4 @@ -package main +package grpcurl import ( "bytes" @@ -12,12 +12,10 @@ import ( "github.com/golang/protobuf/ptypes/struct" "github.com/jhump/protoreflect/desc" "google.golang.org/grpc/metadata" - - "github.com/fullstorydev/grpcurl" ) func TestRequestFactory(t *testing.T) { - source, err := grpcurl.DescriptorSourceFromProtoSets("../../testing/example.protoset") + source, err := DescriptorSourceFromProtoSets("testing/example.protoset") if err != nil { t.Fatalf("failed to create descriptor source: %v", err) } @@ -28,42 +26,42 @@ func TestRequestFactory(t *testing.T) { } testCases := []struct { - format string + format Format input string expectedOutput []proto.Message }{ { - format: "json", + format: FormatJSON, input: "", }, { - format: "json", + format: FormatJSON, input: messageAsJSON, expectedOutput: []proto.Message{msg}, }, { - format: "json", + format: FormatJSON, input: messageAsJSON + messageAsJSON + messageAsJSON, expectedOutput: []proto.Message{msg, msg, msg}, }, { // unlike JSON, empty input yields one empty message (vs. zero messages) - format: "text", + format: FormatText, input: "", expectedOutput: []proto.Message{&structpb.Value{}}, }, { - format: "text", + format: FormatText, input: messageAsText, expectedOutput: []proto.Message{msg}, }, { - format: "text", + format: FormatText, input: messageAsText + string(textSeparatorChar), expectedOutput: []proto.Message{msg, &structpb.Value{}}, }, { - format: "text", + format: FormatText, input: messageAsText + string(textSeparatorChar) + messageAsText + string(textSeparatorChar) + messageAsText, expectedOutput: []proto.Message{msg, msg, msg}, }, @@ -71,11 +69,15 @@ func TestRequestFactory(t *testing.T) { for i, tc := range testCases { name := fmt.Sprintf("#%d, %s, %d message(s)", i+1, tc.format, len(tc.expectedOutput)) - rf, _ := formatDetails(tc.format, source, false, strings.NewReader(tc.input)) + rf, _, err := RequestParserAndFormatterFor(tc.format, source, false, false, strings.NewReader(tc.input)) + if err != nil { + t.Errorf("Failed to create parser and formatter: %v", err) + continue + } numReqs := 0 for { var req structpb.Value - err := rf.next(&req) + err := rf.Next(&req) if err == io.EOF { break } else if err != nil { @@ -86,8 +88,8 @@ func TestRequestFactory(t *testing.T) { } numReqs++ } - if rf.numRequests() != numReqs { - t.Errorf("%s: factory reported wrong number of requests: expecting %d, got %d", name, numReqs, rf.numRequests()) + if rf.NumRequests() != numReqs { + t.Errorf("%s: factory reported wrong number of requests: expecting %d, got %d", name, numReqs, rf.NumRequests()) } } } @@ -95,7 +97,7 @@ func TestRequestFactory(t *testing.T) { // Handler prints response data (and headers/trailers in verbose mode). // This verifies that we get the right output in both JSON and proto text modes. func TestHandler(t *testing.T) { - source, err := grpcurl.DescriptorSourceFromProtoSets("../../testing/example.protoset") + source, err := DescriptorSourceFromProtoSets("testing/example.protoset") if err != nil { t.Fatalf("failed to create descriptor source: %v", err) } @@ -116,7 +118,7 @@ func TestHandler(t *testing.T) { t.Fatalf("failed to create response message: %v", err) } - for _, format := range []string{"json", "text"} { + for _, format := range []Format{FormatJSON, FormatText} { for _, numMessages := range []int{1, 3} { for _, verbose := range []bool{true, false} { name := fmt.Sprintf("%s, %d message(s)", format, numMessages) @@ -124,15 +126,14 @@ func TestHandler(t *testing.T) { name += ", verbose" } - _, formatter := formatDetails(format, source, verbose, nil) + _, formatter, err := RequestParserAndFormatterFor(format, source, false, !verbose, nil) + if err != nil { + t.Errorf("Failed to create parser and formatter: %v", err) + continue + } var buf bytes.Buffer - h := handler{ - out: &buf, - descSource: source, - verbose: verbose, - formatter: formatter, - } + h := NewDefaultEventHandler(&buf, source, formatter, verbose) h.OnResolveMethod(md) h.OnSendHeaders(reqHeaders) diff --git a/grpcurl.go b/grpcurl.go index e221f1f..a9579da 100644 --- a/grpcurl.go +++ b/grpcurl.go @@ -20,6 +20,7 @@ import ( "time" "github.com/golang/protobuf/proto" + descpb "github.com/golang/protobuf/protoc-gen-go/descriptor" "github.com/jhump/protoreflect/desc" "github.com/jhump/protoreflect/desc/protoprint" "github.com/jhump/protoreflect/dynamic" @@ -351,6 +352,83 @@ 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) +} + +func makeTemplate(msg proto.Message, path []*desc.MessageDescriptor) proto.Message { + dm, ok := msg.(*dynamic.Message) + if !ok { + 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 + } + } + + 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() { + case descpb.FieldDescriptorProto_TYPE_FIXED32, + descpb.FieldDescriptorProto_TYPE_UINT32: + dm.AddRepeatedField(fd, uint32(0)) + + case descpb.FieldDescriptorProto_TYPE_SFIXED32, + descpb.FieldDescriptorProto_TYPE_SINT32, + descpb.FieldDescriptorProto_TYPE_INT32, + descpb.FieldDescriptorProto_TYPE_ENUM: + dm.AddRepeatedField(fd, int32(0)) + + case descpb.FieldDescriptorProto_TYPE_FIXED64, + descpb.FieldDescriptorProto_TYPE_UINT64: + dm.AddRepeatedField(fd, uint64(0)) + + case descpb.FieldDescriptorProto_TYPE_SFIXED64, + descpb.FieldDescriptorProto_TYPE_SINT64, + descpb.FieldDescriptorProto_TYPE_INT64: + dm.AddRepeatedField(fd, int64(0)) + + case descpb.FieldDescriptorProto_TYPE_STRING: + dm.AddRepeatedField(fd, "") + + case descpb.FieldDescriptorProto_TYPE_BYTES: + dm.AddRepeatedField(fd, []byte{}) + + case descpb.FieldDescriptorProto_TYPE_BOOL: + dm.AddRepeatedField(fd, false) + + case descpb.FieldDescriptorProto_TYPE_FLOAT: + dm.AddRepeatedField(fd, float32(0)) + + case descpb.FieldDescriptorProto_TYPE_DOUBLE: + dm.AddRepeatedField(fd, float64(0)) + + case descpb.FieldDescriptorProto_TYPE_MESSAGE, + descpb.FieldDescriptorProto_TYPE_GROUP: + dm.AddRepeatedField(fd, makeTemplate(dynamic.NewMessage(fd.GetMessageType()), path)) + } + } else if fd.GetMessageType() != nil { + dm.SetField(fd, makeTemplate(dynamic.NewMessage(fd.GetMessageType()), path)) + } + } + return dm +} + // ClientTransportCredentials builds transport credentials for a gRPC client using the // given properties. If cacertFile is blank, only standard trusted certs are used to // verify the server certs. If clientCertFile is blank, the client will not use a client