summaryrefslogtreecommitdiff
path: root/vendor/github.com/fullstorydev/grpcurl/format.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/fullstorydev/grpcurl/format.go')
-rw-r--r--vendor/github.com/fullstorydev/grpcurl/format.go554
1 files changed, 554 insertions, 0 deletions
diff --git a/vendor/github.com/fullstorydev/grpcurl/format.go b/vendor/github.com/fullstorydev/grpcurl/format.go
new file mode 100644
index 0000000..e7f576b
--- /dev/null
+++ b/vendor/github.com/fullstorydev/grpcurl/format.go
@@ -0,0 +1,554 @@
+package grpcurl
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "reflect"
+ "strings"
+ "sync"
+
+ "github.com/golang/protobuf/jsonpb" //lint:ignore SA1019 we have to import these because some of their types appear in exported API
+ "github.com/golang/protobuf/proto" //lint:ignore SA1019 same as above
+ "github.com/jhump/protoreflect/desc" //lint:ignore SA1019 same as above
+ "github.com/jhump/protoreflect/dynamic" //lint:ignore SA1019 same as above
+ "google.golang.org/grpc/codes"
+ "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},
+ }
+}
+
+// NewJSONRequestParserWithUnmarshaler is like NewJSONRequestParser but
+// accepts a protobuf jsonpb.Unmarshaler instead of jsonpb.AnyResolver.
+func NewJSONRequestParserWithUnmarshaler(in io.Reader, unmarshaler jsonpb.Unmarshaler) RequestParser {
+ return &jsonRequestParser{
+ dec: json.NewDecoder(in),
+ unmarshaler: unmarshaler,
+ }
+}
+
+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 = '\x1e'
+)
+
+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,
+ AnyResolver: resolver,
+ }
+ // Workaround for indentation issue in jsonpb with Any messages.
+ // Bug was originally fixed in https://github.com/golang/protobuf/pull/834
+ // but later re-introduced before the module was deprecated and frozen.
+ // If jsonpb is ever replaced with google.golang.org/protobuf/encoding/protojson
+ // this workaround will no longer be needed.
+ formatter := func(message proto.Message) (string, error) {
+ output, err := marshaler.MarshalToString(message)
+ if err != nil {
+ return "", err
+ }
+ var buf bytes.Buffer
+ if err := json.Indent(&buf, []byte(output), "", " "); err != nil {
+ return "", err
+ }
+ return buf.String(), nil
+ }
+ return formatter
+}
+
+// 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
+}
+
+var protoTextMarshaler = proto.TextMarshaler{ExpandAny: true}
+
+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 := protoTextMarshaler.Marshal(&buf, m); err != nil {
+ return "", err
+ }
+
+ // no trailing newline needed
+ str := buf.String()
+ if len(str) > 0 && str[len(str)-1] == '\n' {
+ str = str[:len(str)-1]
+ }
+
+ tf.numFormatted++
+
+ return str, nil
+}
+
+// Format of request data. The allowed values are 'json' or 'text'.
+type Format string
+
+const (
+ // FormatJSON specifies input data in JSON format. Multiple request values
+ // may be concatenated (messages with a JSON representation other than
+ // object must be separated by whitespace, such as a newline)
+ FormatJSON = Format("json")
+
+ // FormatText specifies input data must be in the protobuf text format.
+ // Multiple request values must be separated by the "record separator"
+ // ASCII character: 0x1E. The stream should not end in a record separator.
+ // If it does, it will be interpreted as a final, blank message after the
+ // separator.
+ FormatText = Format("text")
+)
+
+// AnyResolverFromDescriptorSource returns an AnyResolver that will search for
+// types using the given descriptor source.
+func AnyResolverFromDescriptorSource(source DescriptorSource) jsonpb.AnyResolver {
+ return &anyResolver{source: source}
+}
+
+// AnyResolverFromDescriptorSourceWithFallback returns an AnyResolver that will
+// search for types using the given descriptor source and then fallback to a
+// special message if the type is not found. The fallback type will render to
+// JSON with a "@type" property, just like an Any message, but also with a
+// custom "@value" property that includes the binary encoded payload.
+func AnyResolverFromDescriptorSourceWithFallback(source DescriptorSource) jsonpb.AnyResolver {
+ res := anyResolver{source: source}
+ return &anyResolverWithFallback{AnyResolver: &res}
+}
+
+type anyResolver struct {
+ source DescriptorSource
+
+ er dynamic.ExtensionRegistry
+
+ mu sync.RWMutex
+ mf *dynamic.MessageFactory
+ resolved map[string]func() proto.Message
+}
+
+func (r *anyResolver) Resolve(typeUrl string) (proto.Message, error) {
+ mname := typeUrl
+ if slash := strings.LastIndex(mname, "/"); slash >= 0 {
+ mname = mname[slash+1:]
+ }
+
+ r.mu.RLock()
+ factory := r.resolved[mname]
+ r.mu.RUnlock()
+
+ // already resolved?
+ if factory != nil {
+ return factory(), nil
+ }
+
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ // double-check, in case we were racing with another goroutine
+ // that resolved this one
+ factory = r.resolved[mname]
+ if factory != nil {
+ return factory(), nil
+ }
+
+ // use descriptor source to resolve message type
+ d, err := r.source.FindSymbol(mname)
+ if err != nil {
+ return nil, err
+ }
+ md, ok := d.(*desc.MessageDescriptor)
+ if !ok {
+ return nil, fmt.Errorf("unknown message: %s", typeUrl)
+ }
+ // populate any extensions for this message, too (if there are any)
+ if exts, err := r.source.AllExtensionsForType(mname); err == nil {
+ if err := r.er.AddExtension(exts...); err != nil {
+ return nil, err
+ }
+ }
+
+ if r.mf == nil {
+ r.mf = dynamic.NewMessageFactoryWithExtensionRegistry(&r.er)
+ }
+
+ factory = func() proto.Message {
+ return r.mf.NewMessage(md)
+ }
+ if r.resolved == nil {
+ r.resolved = map[string]func() proto.Message{}
+ }
+ r.resolved[mname] = factory
+ return factory(), nil
+}
+
+// anyResolverWithFallback can provide a fallback value for unknown
+// messages that will format itself to JSON using an "@value" field
+// that has the base64-encoded data for the unknown message value.
+type anyResolverWithFallback struct {
+ jsonpb.AnyResolver
+}
+
+func (r anyResolverWithFallback) Resolve(typeUrl string) (proto.Message, error) {
+ msg, err := r.AnyResolver.Resolve(typeUrl)
+ if err == nil {
+ return msg, err
+ }
+
+ // Try "default" resolution logic. This mirrors the default behavior
+ // of jsonpb, which checks to see if the given message name is registered
+ // in the proto package.
+ mname := typeUrl
+ if slash := strings.LastIndex(mname, "/"); slash >= 0 {
+ mname = mname[slash+1:]
+ }
+ //lint:ignore SA1019 new non-deprecated API requires other code changes; deferring...
+ mt := proto.MessageType(mname)
+ if mt != nil {
+ return reflect.New(mt.Elem()).Interface().(proto.Message), nil
+ }
+
+ // finally, fallback to a special placeholder that can marshal itself
+ // to JSON using a special "@value" property to show base64-encoded
+ // data for the embedded message
+ return &unknownAny{TypeUrl: typeUrl, Error: fmt.Sprintf("%s is not recognized; see @value for raw binary message data", mname)}, nil
+}
+
+type unknownAny struct {
+ TypeUrl string `json:"@type"`
+ Error string `json:"@error"`
+ Value string `json:"@value"`
+}
+
+func (a *unknownAny) MarshalJSONPB(jsm *jsonpb.Marshaler) ([]byte, error) {
+ if jsm.Indent != "" {
+ return json.MarshalIndent(a, "", jsm.Indent)
+ }
+ return json.Marshal(a)
+}
+
+func (a *unknownAny) Unmarshal(b []byte) error {
+ a.Value = base64.StdEncoding.EncodeToString(b)
+ return nil
+}
+
+func (a *unknownAny) Reset() {
+ a.Value = ""
+}
+
+func (a *unknownAny) String() string {
+ b, err := a.MarshalJSONPB(&jsonpb.Marshaler{})
+ if err != nil {
+ return fmt.Sprintf("ERROR: %v", err.Error())
+ }
+ return string(b)
+}
+
+func (a *unknownAny) ProtoMessage() {
+}
+
+var _ proto.Message = (*unknownAny)(nil)
+
+// FormatOptions is a set of flags that are passed to a JSON or text formatter.
+type FormatOptions struct {
+ // EmitJSONDefaultFields flag, when true, includes empty/default values in the output.
+ // FormatJSON only flag.
+ EmitJSONDefaultFields bool
+
+ // AllowUnknownFields is an option for the parser. When true,
+ // it accepts input which includes unknown fields. These unknown fields
+ // are skipped instead of returning an error.
+ // FormatJSON only flag.
+ AllowUnknownFields bool
+
+ // IncludeTextSeparator 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).
+ // It might be useful when the output is piped to another grpcurl process.
+ // FormatText only flag.
+ IncludeTextSeparator bool
+}
+
+// RequestParserAndFormatter 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).
+// It accepts a set of options. The field EmitJSONDefaultFields and IncludeTextSeparator
+// are options for JSON and protobuf text formats, respectively. The AllowUnknownFields field
+// is a JSON-only format flag.
+// Requests will be parsed from the given in.
+func RequestParserAndFormatter(format Format, descSource DescriptorSource, in io.Reader, opts FormatOptions) (RequestParser, Formatter, error) {
+ switch format {
+ case FormatJSON:
+ resolver := AnyResolverFromDescriptorSource(descSource)
+ unmarshaler := jsonpb.Unmarshaler{AnyResolver: resolver, AllowUnknownFields: opts.AllowUnknownFields}
+ return NewJSONRequestParserWithUnmarshaler(in, unmarshaler), NewJSONFormatter(opts.EmitJSONDefaultFields, anyResolverWithFallback{AnyResolver: resolver}), nil
+ case FormatText:
+ return NewTextRequestParser(in), NewTextFormatter(opts.IncludeTextSeparator), nil
+ default:
+ return nil, nil, fmt.Errorf("unknown format: %s", format)
+ }
+}
+
+// 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.
+// This function is deprecated. Please use RequestParserAndFormatter instead.
+// DEPRECATED
+func RequestParserAndFormatterFor(format Format, descSource DescriptorSource, emitJSONDefaultFields, includeTextSeparator bool, in io.Reader) (RequestParser, Formatter, error) {
+ return RequestParserAndFormatter(format, descSource, in, FormatOptions{
+ EmitJSONDefaultFields: emitJSONDefaultFields,
+ IncludeTextSeparator: includeTextSeparator,
+ })
+}
+
+// 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
+ Formatter Formatter
+ // 0 = default
+ // 1 = verbose
+ // 2 = very verbose
+ VerbosityLevel int
+
+ // 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.
+//
+// Deprecated: NewDefaultEventHandler exists for compatibility.
+// It doesn't allow fine control over the `VerbosityLevel`
+// and provides only 0 and 1 options (which corresponds to the `verbose` argument).
+// Use DefaultEventHandler{} initializer directly.
+func NewDefaultEventHandler(out io.Writer, descSource DescriptorSource, formatter Formatter, verbose bool) *DefaultEventHandler {
+ verbosityLevel := 0
+ if verbose {
+ verbosityLevel = 1
+ }
+ return &DefaultEventHandler{
+ Out: out,
+ Formatter: formatter,
+ VerbosityLevel: verbosityLevel,
+ }
+}
+
+var _ InvocationEventHandler = (*DefaultEventHandler)(nil)
+
+func (h *DefaultEventHandler) OnResolveMethod(md *desc.MethodDescriptor) {
+ if h.VerbosityLevel > 0 {
+ txt, err := GetDescriptorText(md, nil)
+ if err == nil {
+ fmt.Fprintf(h.Out, "\nResolved method descriptor:\n%s\n", txt)
+ }
+ }
+}
+
+func (h *DefaultEventHandler) OnSendHeaders(md metadata.MD) {
+ if h.VerbosityLevel > 0 {
+ fmt.Fprintf(h.Out, "\nRequest metadata to send:\n%s\n", MetadataToString(md))
+ }
+}
+
+func (h *DefaultEventHandler) OnReceiveHeaders(md metadata.MD) {
+ if h.VerbosityLevel > 0 {
+ fmt.Fprintf(h.Out, "\nResponse headers received:\n%s\n", MetadataToString(md))
+ }
+}
+
+func (h *DefaultEventHandler) OnReceiveResponse(resp proto.Message) {
+ h.NumResponses++
+ if h.VerbosityLevel > 1 {
+ fmt.Fprintf(h.Out, "\nEstimated response size: %d bytes\n", proto.Size(resp))
+ }
+ if h.VerbosityLevel > 0 {
+ 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.VerbosityLevel > 0 {
+ fmt.Fprintf(h.Out, "\nResponse trailers received:\n%s\n", MetadataToString(md))
+ }
+}
+
+// PrintStatus prints details about the given status to the given writer. The given
+// formatter is used to print any detail messages that may be included in the status.
+// If the given status has a code of OK, "OK" is printed and that is all. Otherwise,
+// "ERROR:" is printed along with a line showing the code, one showing the message
+// string, and each detail message if any are present. The detail messages will be
+// printed as proto text format or JSON, depending on the given formatter.
+func PrintStatus(w io.Writer, stat *status.Status, formatter Formatter) {
+ if stat.Code() == codes.OK {
+ fmt.Fprintln(w, "OK")
+ return
+ }
+ fmt.Fprintf(w, "ERROR:\n Code: %s\n Message: %s\n", stat.Code().String(), stat.Message())
+
+ statpb := stat.Proto()
+ if len(statpb.Details) > 0 {
+ fmt.Fprintf(w, " Details:\n")
+ for i, det := range statpb.Details {
+ prefix := fmt.Sprintf(" %d)", i+1)
+ fmt.Fprintf(w, "%s\t", prefix)
+ prefix = strings.Repeat(" ", len(prefix)) + "\t"
+
+ output, err := formatter(det)
+ if err != nil {
+ fmt.Fprintf(w, "Error parsing detail message: %v\n", err)
+ } else {
+ lines := strings.Split(output, "\n")
+ for i, line := range lines {
+ if i == 0 {
+ // first line is already indented
+ fmt.Fprintf(w, "%s\n", line)
+ } else {
+ fmt.Fprintf(w, "%s%s\n", prefix, line)
+ }
+ }
+ }
+ }
+ }
+}