From 8e51c5e2d326aee51627c94cc7860650102ad2cc Mon Sep 17 00:00:00 2001 From: Adam Babik Date: Thu, 23 Jul 2020 16:54:06 +0200 Subject: [PATCH] cmd/grpcurl: add -allow-unknown-fields option (#147) --- cmd/grpcurl/grpcurl.go | 14 +++++++-- format.go | 64 ++++++++++++++++++++++++++++++++++++------ format_test.go | 4 +-- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/cmd/grpcurl/grpcurl.go b/cmd/grpcurl/grpcurl.go index e1f57de..fd00511 100644 --- a/cmd/grpcurl/grpcurl.go +++ b/cmd/grpcurl/grpcurl.go @@ -100,6 +100,10 @@ var ( 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.`)) + allowUnknownFields = flags.Bool("allow-unknown-fields", false, prettify(` + When true, the request contents, if 'json' format is used, allows + unkown fields to be present. They will be ignored when parsing + the request.`)) connectTimeout = flags.Float64("connect-timeout", 0, prettify(` The maximum time, in seconds, to wait for connection to be established. Defaults to 10 seconds.`)) @@ -615,7 +619,8 @@ func main() { // for messages, also show a template in JSON, to make it easier to // create a request to invoke an RPC tmpl := grpcurl.MakeTemplate(dsc) - _, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format(*format), descSource, true, false, nil) + options := grpcurl.FormatOptions{EmitJSONDefaultFields: true} + _, formatter, err := grpcurl.RequestParserAndFormatter(grpcurl.Format(*format), descSource, nil, options) if err != nil { fail(err, "Failed to construct formatter for %q", *format) } @@ -647,7 +652,12 @@ func main() { // 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) + options := grpcurl.FormatOptions{ + EmitJSONDefaultFields: *emitDefaults, + IncludeTextSeparator: includeSeparators, + AllowUnknownFields: *allowUnknownFields, + } + rf, formatter, err := grpcurl.RequestParserAndFormatter(grpcurl.Format(*format), descSource, in, options) if err != nil { fail(err, "Failed to construct request parser and formatter for %q", *format) } diff --git a/format.go b/format.go index 73162b7..54cd5f8 100644 --- a/format.go +++ b/format.go @@ -55,6 +55,15 @@ func NewJSONRequestParser(in io.Reader, resolver jsonpb.AnyResolver) RequestPars } } +// 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 { @@ -342,21 +351,58 @@ 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) { - switch format { - case FormatJSON: - resolver := AnyResolverFromDescriptorSource(descSource) - return NewJSONRequestParser(in, resolver), NewJSONFormatter(emitJSONDefaultFields, anyResolverWithFallback{AnyResolver: resolver}), nil - case FormatText: - return NewTextRequestParser(in), NewTextFormatter(includeTextSeparator), nil - default: - return nil, nil, fmt.Errorf("unknown format: %s", format) - } + return RequestParserAndFormatter(format, descSource, in, FormatOptions{ + EmitJSONDefaultFields: emitJSONDefaultFields, + IncludeTextSeparator: includeTextSeparator, + }) } // DefaultEventHandler logs events to a writer. This is not thread-safe, but is diff --git a/format_test.go b/format_test.go index 30be9e2..3e517e3 100644 --- a/format_test.go +++ b/format_test.go @@ -69,7 +69,7 @@ func TestRequestParser(t *testing.T) { for i, tc := range testCases { name := fmt.Sprintf("#%d, %s, %d message(s)", i+1, tc.format, len(tc.expectedOutput)) - rf, _, err := RequestParserAndFormatterFor(tc.format, source, false, false, strings.NewReader(tc.input)) + rf, _, err := RequestParserAndFormatter(tc.format, source, strings.NewReader(tc.input), FormatOptions{}) if err != nil { t.Errorf("Failed to create parser and formatter: %v", err) continue @@ -126,7 +126,7 @@ func TestHandler(t *testing.T) { name += ", verbose" } - _, formatter, err := RequestParserAndFormatterFor(format, source, false, !verbose, nil) + _, formatter, err := RequestParserAndFormatter(format, source, nil, FormatOptions{IncludeTextSeparator: !verbose}) if err != nil { t.Errorf("Failed to create parser and formatter: %v", err) continue