move more stuff from cmd to package (#59)
This commit is contained in:
parent
9a4bbacdd6
commit
7cabe7a9d0
|
|
@ -4,9 +4,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -15,8 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/protobuf/jsonpb"
|
"github.com/fullstorydev/grpcurl"
|
||||||
"github.com/golang/protobuf/proto"
|
|
||||||
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
||||||
"github.com/jhump/protoreflect/desc"
|
"github.com/jhump/protoreflect/desc"
|
||||||
"github.com/jhump/protoreflect/dynamic"
|
"github.com/jhump/protoreflect/dynamic"
|
||||||
|
|
@ -28,9 +24,6 @@ import (
|
||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
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>"
|
var version = "dev build <no version set>"
|
||||||
|
|
@ -466,21 +459,17 @@ func main() {
|
||||||
if dsc, ok := dsc.(*desc.MessageDescriptor); ok && *msgTemplate {
|
if dsc, ok := dsc.(*desc.MessageDescriptor); ok && *msgTemplate {
|
||||||
// for messages, also show a template in JSON, to make it easier to
|
// for messages, also show a template in JSON, to make it easier to
|
||||||
// create a request to invoke an RPC
|
// create a request to invoke an RPC
|
||||||
tmpl := makeTemplate(dynamic.NewMessage(dsc))
|
tmpl := grpcurl.MakeTemplate(dynamic.NewMessage(dsc))
|
||||||
fmt.Println("\nMessage template:")
|
_, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format(*format), descSource, true, false, nil)
|
||||||
if *format == "json" {
|
if err != nil {
|
||||||
jsm := jsonpb.Marshaler{Indent: " ", EmitDefaults: true}
|
fail(err, "Failed to construct formatter for %q", *format)
|
||||||
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()
|
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)
|
in = strings.NewReader(*data)
|
||||||
}
|
}
|
||||||
|
|
||||||
rf, formatter := formatDetails(*format, descSource, *verbose, in)
|
// if not verbose output, then also include record delimiters
|
||||||
h := handler{
|
// between each message, so output could potentially be piped
|
||||||
out: os.Stdout,
|
// to another grpcurl process
|
||||||
descSource: descSource,
|
includeSeparators := !*verbose
|
||||||
formatter: formatter,
|
rf, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format(*format), descSource, *emitDefaults, includeSeparators, in)
|
||||||
verbose: *verbose,
|
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 {
|
if err != nil {
|
||||||
fail(err, "Error invoking method %q", symbol)
|
fail(err, "Error invoking method %q", symbol)
|
||||||
}
|
}
|
||||||
reqSuffix := ""
|
reqSuffix := ""
|
||||||
respSuffix := ""
|
respSuffix := ""
|
||||||
reqCount := rf.numRequests()
|
reqCount := rf.NumRequests()
|
||||||
if reqCount != 1 {
|
if reqCount != 1 {
|
||||||
reqSuffix = "s"
|
reqSuffix = "s"
|
||||||
}
|
}
|
||||||
if h.respCount != 1 {
|
if h.NumResponses != 1 {
|
||||||
respSuffix = "s"
|
respSuffix = "s"
|
||||||
}
|
}
|
||||||
if *verbose {
|
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 {
|
if h.Status.Code() != codes.OK {
|
||||||
fmt.Fprintf(os.Stderr, "ERROR:\n Code: %s\n Message: %s\n", h.stat.Code().String(), h.stat.Message())
|
fmt.Fprintf(os.Stderr, "ERROR:\n Code: %s\n Message: %s\n", h.Status.Code().String(), h.Status.Message())
|
||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -597,262 +588,3 @@ func fail(err error, msg string, args ...interface{}) {
|
||||||
exit(2)
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -12,12 +12,10 @@ import (
|
||||||
"github.com/golang/protobuf/ptypes/struct"
|
"github.com/golang/protobuf/ptypes/struct"
|
||||||
"github.com/jhump/protoreflect/desc"
|
"github.com/jhump/protoreflect/desc"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
|
|
||||||
"github.com/fullstorydev/grpcurl"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRequestFactory(t *testing.T) {
|
func TestRequestFactory(t *testing.T) {
|
||||||
source, err := grpcurl.DescriptorSourceFromProtoSets("../../testing/example.protoset")
|
source, err := DescriptorSourceFromProtoSets("testing/example.protoset")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create descriptor source: %v", err)
|
t.Fatalf("failed to create descriptor source: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -28,42 +26,42 @@ func TestRequestFactory(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
format string
|
format Format
|
||||||
input string
|
input string
|
||||||
expectedOutput []proto.Message
|
expectedOutput []proto.Message
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
format: "json",
|
format: FormatJSON,
|
||||||
input: "",
|
input: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: "json",
|
format: FormatJSON,
|
||||||
input: messageAsJSON,
|
input: messageAsJSON,
|
||||||
expectedOutput: []proto.Message{msg},
|
expectedOutput: []proto.Message{msg},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: "json",
|
format: FormatJSON,
|
||||||
input: messageAsJSON + messageAsJSON + messageAsJSON,
|
input: messageAsJSON + messageAsJSON + messageAsJSON,
|
||||||
expectedOutput: []proto.Message{msg, msg, msg},
|
expectedOutput: []proto.Message{msg, msg, msg},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// unlike JSON, empty input yields one empty message (vs. zero messages)
|
// unlike JSON, empty input yields one empty message (vs. zero messages)
|
||||||
format: "text",
|
format: FormatText,
|
||||||
input: "",
|
input: "",
|
||||||
expectedOutput: []proto.Message{&structpb.Value{}},
|
expectedOutput: []proto.Message{&structpb.Value{}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: "text",
|
format: FormatText,
|
||||||
input: messageAsText,
|
input: messageAsText,
|
||||||
expectedOutput: []proto.Message{msg},
|
expectedOutput: []proto.Message{msg},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: "text",
|
format: FormatText,
|
||||||
input: messageAsText + string(textSeparatorChar),
|
input: messageAsText + string(textSeparatorChar),
|
||||||
expectedOutput: []proto.Message{msg, &structpb.Value{}},
|
expectedOutput: []proto.Message{msg, &structpb.Value{}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: "text",
|
format: FormatText,
|
||||||
input: messageAsText + string(textSeparatorChar) + messageAsText + string(textSeparatorChar) + messageAsText,
|
input: messageAsText + string(textSeparatorChar) + messageAsText + string(textSeparatorChar) + messageAsText,
|
||||||
expectedOutput: []proto.Message{msg, msg, msg},
|
expectedOutput: []proto.Message{msg, msg, msg},
|
||||||
},
|
},
|
||||||
|
|
@ -71,11 +69,15 @@ func TestRequestFactory(t *testing.T) {
|
||||||
|
|
||||||
for i, tc := range testCases {
|
for i, tc := range testCases {
|
||||||
name := fmt.Sprintf("#%d, %s, %d message(s)", i+1, tc.format, len(tc.expectedOutput))
|
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
|
numReqs := 0
|
||||||
for {
|
for {
|
||||||
var req structpb.Value
|
var req structpb.Value
|
||||||
err := rf.next(&req)
|
err := rf.Next(&req)
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
|
@ -86,8 +88,8 @@ func TestRequestFactory(t *testing.T) {
|
||||||
}
|
}
|
||||||
numReqs++
|
numReqs++
|
||||||
}
|
}
|
||||||
if rf.numRequests() != numReqs {
|
if rf.NumRequests() != numReqs {
|
||||||
t.Errorf("%s: factory reported wrong number of requests: expecting %d, got %d", name, numReqs, rf.numRequests())
|
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).
|
// 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.
|
// This verifies that we get the right output in both JSON and proto text modes.
|
||||||
func TestHandler(t *testing.T) {
|
func TestHandler(t *testing.T) {
|
||||||
source, err := grpcurl.DescriptorSourceFromProtoSets("../../testing/example.protoset")
|
source, err := DescriptorSourceFromProtoSets("testing/example.protoset")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create descriptor source: %v", err)
|
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)
|
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 _, numMessages := range []int{1, 3} {
|
||||||
for _, verbose := range []bool{true, false} {
|
for _, verbose := range []bool{true, false} {
|
||||||
name := fmt.Sprintf("%s, %d message(s)", format, numMessages)
|
name := fmt.Sprintf("%s, %d message(s)", format, numMessages)
|
||||||
|
|
@ -124,15 +126,14 @@ func TestHandler(t *testing.T) {
|
||||||
name += ", verbose"
|
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
|
var buf bytes.Buffer
|
||||||
h := handler{
|
h := NewDefaultEventHandler(&buf, source, formatter, verbose)
|
||||||
out: &buf,
|
|
||||||
descSource: source,
|
|
||||||
verbose: verbose,
|
|
||||||
formatter: formatter,
|
|
||||||
}
|
|
||||||
|
|
||||||
h.OnResolveMethod(md)
|
h.OnResolveMethod(md)
|
||||||
h.OnSendHeaders(reqHeaders)
|
h.OnSendHeaders(reqHeaders)
|
||||||
78
grpcurl.go
78
grpcurl.go
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
|
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
||||||
"github.com/jhump/protoreflect/desc"
|
"github.com/jhump/protoreflect/desc"
|
||||||
"github.com/jhump/protoreflect/desc/protoprint"
|
"github.com/jhump/protoreflect/desc/protoprint"
|
||||||
"github.com/jhump/protoreflect/dynamic"
|
"github.com/jhump/protoreflect/dynamic"
|
||||||
|
|
@ -351,6 +352,83 @@ func fullyConvertToDynamic(msgFact *dynamic.MessageFactory, msg proto.Message) (
|
||||||
return dm, nil
|
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
|
// ClientTransportCredentials builds transport credentials for a gRPC client using the
|
||||||
// given properties. If cacertFile is blank, only standard trusted certs are used to
|
// 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
|
// verify the server certs. If clientCertFile is blank, the client will not use a client
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue