move more stuff from cmd to package (#59)
This commit is contained in:
parent
9a4bbacdd6
commit
7cabe7a9d0
|
|
@ -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 <no version set>"
|
||||
|
|
@ -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))
|
||||
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)
|
||||
}
|
||||
str, err := formatter(tmpl)
|
||||
if err != nil {
|
||||
fail(err, "Failed to print template for message %s", s)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
78
grpcurl.go
78
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue