mirror of
https://github.com/fullstorydev/grpcurl.git
synced 2026-05-22 19:51:44 +03:00
add option to support text format (#54)
* augments grpcurl package API in order to handle multiple formats * deprecates old signature for InvokeRpc * add command-line flag to use protobuf text format instead of JSON * use AnyResolver when marshaling to/from JSON
This commit is contained in:
303
cmd/grpcurl/formatting_test.go
Normal file
303
cmd/grpcurl/formatting_test.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"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")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create descriptor source: %v", err)
|
||||
}
|
||||
|
||||
msg, err := makeProto()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create message: %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
format string
|
||||
input string
|
||||
expectedOutput []proto.Message
|
||||
}{
|
||||
{
|
||||
format: "json",
|
||||
input: "",
|
||||
},
|
||||
{
|
||||
format: "json",
|
||||
input: messageAsJSON,
|
||||
expectedOutput: []proto.Message{msg},
|
||||
},
|
||||
{
|
||||
format: "json",
|
||||
input: messageAsJSON + messageAsJSON + messageAsJSON,
|
||||
expectedOutput: []proto.Message{msg, msg, msg},
|
||||
},
|
||||
{
|
||||
// unlike JSON, empty input yields one empty message (vs. zero messages)
|
||||
format: "text",
|
||||
input: "",
|
||||
expectedOutput: []proto.Message{&structpb.Value{}},
|
||||
},
|
||||
{
|
||||
format: "text",
|
||||
input: messageAsText,
|
||||
expectedOutput: []proto.Message{msg},
|
||||
},
|
||||
{
|
||||
format: "text",
|
||||
input: messageAsText + string(textSeparatorChar),
|
||||
expectedOutput: []proto.Message{msg, &structpb.Value{}},
|
||||
},
|
||||
{
|
||||
format: "text",
|
||||
input: messageAsText + string(textSeparatorChar) + messageAsText + string(textSeparatorChar) + messageAsText,
|
||||
expectedOutput: []proto.Message{msg, msg, msg},
|
||||
},
|
||||
}
|
||||
|
||||
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))
|
||||
numReqs := 0
|
||||
for {
|
||||
var req structpb.Value
|
||||
err := rf.next(&req)
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
t.Errorf("%s, msg %d: unexpected error: %v", name, numReqs, err)
|
||||
}
|
||||
if !proto.Equal(&req, tc.expectedOutput[numReqs]) {
|
||||
t.Errorf("%s, msg %d: incorrect message;\nexpecting:\n%v\ngot:\n%v", name, numReqs, tc.expectedOutput[numReqs], &req)
|
||||
}
|
||||
numReqs++
|
||||
}
|
||||
if rf.numRequests() != numReqs {
|
||||
t.Errorf("%s: factory reported wrong number of requests: expecting %d, got %d", name, numReqs, rf.numRequests())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create descriptor source: %v", err)
|
||||
}
|
||||
d, err := source.FindSymbol("TestService.GetFiles")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to find method 'TestService.GetFiles': %v", err)
|
||||
}
|
||||
md, ok := d.(*desc.MethodDescriptor)
|
||||
if !ok {
|
||||
t.Fatalf("wrong kind of descriptor found: %T", d)
|
||||
}
|
||||
|
||||
reqHeaders := metadata.Pairs("foo", "123", "bar", "456")
|
||||
respHeaders := metadata.Pairs("foo", "abc", "bar", "def", "baz", "xyz")
|
||||
respTrailers := metadata.Pairs("a", "1", "b", "2", "c", "3")
|
||||
rsp, err := makeProto()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create response message: %v", err)
|
||||
}
|
||||
|
||||
for _, format := range []string{"json", "text"} {
|
||||
for _, numMessages := range []int{1, 3} {
|
||||
for _, verbose := range []bool{true, false} {
|
||||
name := fmt.Sprintf("%s, %d message(s)", format, numMessages)
|
||||
if verbose {
|
||||
name += ", verbose"
|
||||
}
|
||||
|
||||
_, formatter := formatDetails(format, source, verbose, nil)
|
||||
|
||||
var buf bytes.Buffer
|
||||
h := handler{
|
||||
out: &buf,
|
||||
descSource: source,
|
||||
verbose: verbose,
|
||||
formatter: formatter,
|
||||
}
|
||||
|
||||
h.OnResolveMethod(md)
|
||||
h.OnSendHeaders(reqHeaders)
|
||||
h.OnReceiveHeaders(respHeaders)
|
||||
for i := 0; i < numMessages; i++ {
|
||||
h.OnReceiveResponse(rsp)
|
||||
}
|
||||
h.OnReceiveTrailers(nil, respTrailers)
|
||||
|
||||
expectedOutput := ""
|
||||
if verbose {
|
||||
expectedOutput += verbosePrefix
|
||||
}
|
||||
for i := 0; i < numMessages; i++ {
|
||||
if verbose {
|
||||
expectedOutput += verboseResponseHeader
|
||||
}
|
||||
if format == "json" {
|
||||
expectedOutput += messageAsJSON
|
||||
} else {
|
||||
if i > 0 && !verbose {
|
||||
expectedOutput += string(textSeparatorChar)
|
||||
}
|
||||
expectedOutput += messageAsText
|
||||
}
|
||||
}
|
||||
if verbose {
|
||||
expectedOutput += verboseSuffix
|
||||
}
|
||||
|
||||
out := buf.String()
|
||||
if !compare(out, expectedOutput) {
|
||||
t.Errorf("%s: Incorrect output.", name) // Expected:\n%s\nGot:\n%s", name, expectedOutput, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compare checks that actual and expected are equal, returning true if so.
|
||||
// A simple equality check (==) does not suffice because jsonpb formats
|
||||
// structpb.Value strangely. So if that formatting gets fixed, we don't
|
||||
// want this test in grpcurl to suddenly start failing. So we check each
|
||||
// line and compare the lines after stripping whitespace (which removes
|
||||
// the jsonpb format anomalies).
|
||||
func compare(actual, expected string) bool {
|
||||
actualLines := strings.Split(actual, "\n")
|
||||
expectedLines := strings.Split(expected, "\n")
|
||||
if len(actualLines) != len(expectedLines) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(actualLines); i++ {
|
||||
if strings.TrimSpace(actualLines[i]) != strings.TrimSpace(expectedLines[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func makeProto() (proto.Message, error) {
|
||||
var rsp structpb.Value
|
||||
err := jsonpb.UnmarshalString(`{
|
||||
"foo": ["abc", "def", "ghi"],
|
||||
"bar": { "a": 1, "b": 2 },
|
||||
"baz": true,
|
||||
"null": null
|
||||
}`, &rsp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rsp, nil
|
||||
}
|
||||
|
||||
var (
|
||||
verbosePrefix = `
|
||||
Resolved method descriptor:
|
||||
{
|
||||
"name": "GetFiles",
|
||||
"inputType": ".TestRequest",
|
||||
"outputType": ".TestResponse",
|
||||
"options": {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Request metadata to send:
|
||||
bar: 456
|
||||
foo: 123
|
||||
|
||||
Response headers received:
|
||||
bar: def
|
||||
baz: xyz
|
||||
foo: abc
|
||||
`
|
||||
verboseSuffix = `
|
||||
Response trailers received:
|
||||
a: 1
|
||||
b: 2
|
||||
c: 3
|
||||
`
|
||||
verboseResponseHeader = `
|
||||
Response contents:
|
||||
`
|
||||
messageAsJSON = `{
|
||||
"bar": {
|
||||
"a": 1,
|
||||
"b": 2
|
||||
},
|
||||
"baz": true,
|
||||
"foo": [
|
||||
"abc",
|
||||
"def",
|
||||
"ghi"
|
||||
],
|
||||
"null": null
|
||||
}
|
||||
`
|
||||
messageAsText = `struct_value: <
|
||||
fields: <
|
||||
key: "bar"
|
||||
value: <
|
||||
struct_value: <
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
number_value: 1
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "b"
|
||||
value: <
|
||||
number_value: 2
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "baz"
|
||||
value: <
|
||||
bool_value: true
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "foo"
|
||||
value: <
|
||||
list_value: <
|
||||
values: <
|
||||
string_value: "abc"
|
||||
>
|
||||
values: <
|
||||
string_value: "def"
|
||||
>
|
||||
values: <
|
||||
string_value: "ghi"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "null"
|
||||
value: <
|
||||
null_value: NULL_VALUE
|
||||
>
|
||||
>
|
||||
>
|
||||
`
|
||||
)
|
||||
@@ -1,12 +1,15 @@
|
||||
// Command grpcurl makes GRPC requests (a la cURL, but HTTP/2). It can use a supplied descriptor file or
|
||||
// service reflection to translate JSON request data into the appropriate protobuf request data and vice
|
||||
// versa for presenting the response contents.
|
||||
// Command grpcurl makes GRPC requests (a la cURL, but HTTP/2). It can use a supplied descriptor
|
||||
// file, protobuf sources, or service reflection to translate JSON or text request data into the
|
||||
// appropriate protobuf messages and vice versa for presenting the response contents.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -62,12 +65,22 @@ var (
|
||||
rpcHeaders multiString
|
||||
reflHeaders multiString
|
||||
authority = flag.String("authority", "",
|
||||
":authority pseudo header value to be passed along with underlying HTTP/2 requests. It defaults to `host [ \":\" port ]` part of the target url.")
|
||||
`:authority pseudo header value to be passed along with underlying HTTP/2
|
||||
requests. It defaults to 'host [ ":" port ]' part of the target url.`)
|
||||
data = flag.String("d", "",
|
||||
`JSON request contents. If the value is '@' then the request contents are
|
||||
read from stdin. For calls that accept a stream of requests, the
|
||||
`Data for request contents. If the value is '@' then the request contents
|
||||
are read from stdin. For calls that accept a stream of requests, the
|
||||
contents should include all such request messages concatenated together
|
||||
(optionally separated by whitespace).`)
|
||||
format = flag.String("format", "json",
|
||||
`The format of request data. The allowed values are 'json' or 'text'. For
|
||||
'json', the input data must be 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). For 'text', the input
|
||||
data must be in the protobuf text format, in which case multiple request
|
||||
values must be separated by the "record separate" 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.`)
|
||||
connectTimeout = flag.String("connect-timeout", "",
|
||||
`The maximum time, in seconds, to wait for connection to be established.
|
||||
Defaults to 10 seconds.`)
|
||||
@@ -81,9 +94,9 @@ var (
|
||||
preventing batch jobs that use grpcurl from hanging due to slow or bad
|
||||
network links or due to incorrect stream method usage.`)
|
||||
emitDefaults = flag.Bool("emit-defaults", false,
|
||||
`Emit default values from JSON-encoded responses.`)
|
||||
`Emit default values for JSON-encoded responses.`)
|
||||
msgTemplate = flag.Bool("msg-template", false,
|
||||
`When describing messages, show a JSON template for the message type.`)
|
||||
`When describing messages, show a template of input data.`)
|
||||
verbose = flag.Bool("v", false,
|
||||
`Enable verbose output.`)
|
||||
serverName = flag.String("servername", "", "Override servername when validating TLS certificate.")
|
||||
@@ -168,6 +181,9 @@ func main() {
|
||||
if (*key == "") != (*cert == "") {
|
||||
fail(nil, "The -cert and -key arguments must be used together and both be present.")
|
||||
}
|
||||
if *format != "json" && *format != "text" {
|
||||
fail(nil, "The -format option must be 'json' or 'text.")
|
||||
}
|
||||
|
||||
args := flag.Args()
|
||||
|
||||
@@ -417,10 +433,17 @@ func main() {
|
||||
// create a request to invoke an RPC
|
||||
tmpl := makeTemplate(dynamic.NewMessage(dsc))
|
||||
fmt.Println("\nMessage template:")
|
||||
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)
|
||||
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()
|
||||
}
|
||||
@@ -431,28 +454,36 @@ func main() {
|
||||
if cc == nil {
|
||||
cc = dial()
|
||||
}
|
||||
var dec *json.Decoder
|
||||
var in io.Reader
|
||||
if *data == "@" {
|
||||
dec = json.NewDecoder(os.Stdin)
|
||||
in = os.Stdin
|
||||
} else {
|
||||
dec = json.NewDecoder(strings.NewReader(*data))
|
||||
in = strings.NewReader(*data)
|
||||
}
|
||||
|
||||
h := &handler{dec: dec, descSource: descSource}
|
||||
err := grpcurl.InvokeRpc(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), h, h.getRequestData)
|
||||
rf, formatter := formatDetails(*format, descSource, *verbose, in)
|
||||
h := handler{
|
||||
out: os.Stdout,
|
||||
descSource: descSource,
|
||||
formatter: formatter,
|
||||
verbose: *verbose,
|
||||
}
|
||||
|
||||
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 := ""
|
||||
if h.reqCount != 1 {
|
||||
reqCount := rf.numRequests()
|
||||
if reqCount != 1 {
|
||||
reqSuffix = "s"
|
||||
}
|
||||
if h.respCount != 1 {
|
||||
respSuffix = "s"
|
||||
}
|
||||
if *verbose {
|
||||
fmt.Printf("Sent %d request%s and received %d response%s\n", h.reqCount, reqSuffix, h.respCount, respSuffix)
|
||||
fmt.Printf("Sent %d request%s and received %d response%s\n", reqCount, reqSuffix, h.respCount, 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())
|
||||
@@ -512,64 +543,88 @@ func fail(err error, msg string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
dec *json.Decoder
|
||||
out io.Writer
|
||||
descSource grpcurl.DescriptorSource
|
||||
reqCount int
|
||||
respCount int
|
||||
stat *status.Status
|
||||
formatter func(proto.Message) (string, error)
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func (h *handler) OnResolveMethod(md *desc.MethodDescriptor) {
|
||||
if *verbose {
|
||||
if h.verbose {
|
||||
txt, err := grpcurl.GetDescriptorText(md, h.descSource)
|
||||
if err == nil {
|
||||
fmt.Printf("\nResolved method descriptor:\n%s\n", txt)
|
||||
fmt.Fprintf(h.out, "\nResolved method descriptor:\n%s\n", txt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (*handler) OnSendHeaders(md metadata.MD) {
|
||||
if *verbose {
|
||||
fmt.Printf("\nRequest metadata to send:\n%s\n", grpcurl.MetadataToString(md))
|
||||
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) getRequestData() ([]byte, error) {
|
||||
// we don't use a mutex, though this methods will be called from different goroutine
|
||||
// than other methods for bidi calls, because this method does not share any state
|
||||
// with the other methods.
|
||||
var msg json.RawMessage
|
||||
if err := h.dec.Decode(&msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.reqCount++
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (*handler) OnReceiveHeaders(md metadata.MD) {
|
||||
if *verbose {
|
||||
fmt.Printf("\nResponse headers received:\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 *verbose {
|
||||
fmt.Print("\nResponse contents:\n")
|
||||
if h.verbose {
|
||||
fmt.Fprint(h.out, "\nResponse contents:\n")
|
||||
}
|
||||
jsm := jsonpb.Marshaler{EmitDefaults: *emitDefaults, Indent: " "}
|
||||
respStr, err := jsm.MarshalToString(resp)
|
||||
respStr, err := h.formatter(resp)
|
||||
if err != nil {
|
||||
fail(err, "failed to generate JSON form of response message")
|
||||
fail(err, "failed to generate %s form of response message", *format)
|
||||
}
|
||||
fmt.Println(respStr)
|
||||
fmt.Fprintln(h.out, respStr)
|
||||
}
|
||||
|
||||
func (h *handler) OnReceiveTrailers(stat *status.Status, md metadata.MD) {
|
||||
h.stat = stat
|
||||
if *verbose {
|
||||
fmt.Printf("\nResponse trailers received:\n%s\n", grpcurl.MetadataToString(md))
|
||||
if h.verbose {
|
||||
fmt.Fprintf(h.out, "\nResponse trailers received:\n%s\n", grpcurl.MetadataToString(md))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,3 +688,116 @@ func makeTemplate(msg proto.Message) proto.Message {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user