add more control over request metadata; add message templates (#17)

* add more control over request metadata between reflection calls and main RPC invocation
* add flag to print a message template (when describing message types)
* relax some command-line argument issues to warnings
This commit is contained in:
Joshua Humphries 2018-03-08 11:29:06 -05:00 committed by GitHub
parent 99a3346b94
commit f7eb17819d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 103 additions and 7 deletions

View File

@ -14,7 +14,9 @@ import (
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
"github.com/jhump/protoreflect/desc"
"github.com/jhump/protoreflect/dynamic"
"github.com/jhump/protoreflect/grpcreflect"
"golang.org/x/net/context"
"google.golang.org/grpc"
@ -49,6 +51,8 @@ var (
with -plaintext option. Must also provide -cert option.`)
protoset multiString
addlHeaders multiString
rpcHeaders multiString
reflHeaders multiString
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
@ -68,16 +72,26 @@ var (
network links or due to incorrect stream method usage.`)
emitDefaults = flag.Bool("emit-defaults", false,
`Emit default values from JSON-encoded responses.`)
msgTemplate = flag.Bool("msg-template", false,
`When describing messages, show a JSON template for the message type.`)
verbose = flag.Bool("v", false,
`Enable verbose output.`)
)
func init() {
// TODO: Allow separate headers for relflection/invocation
flag.Var(&addlHeaders, "H",
`Additional request headers in 'name: value' format. May specify more
than one via multiple -H flags. These headers will also be included in
reflection requests to a server.`)
`Additional headers in 'name: value' format. May specify more than one
via multiple flags. These headers will also be included in reflection
requests requests to a server.`)
flag.Var(&rpcHeaders, "rpc-header",
`Additional RPC headers in 'name: value' format. May specify more than
one via multiple flags. These headers will *only* be used when invoking
the requested RPC method. They are excluded from reflection requests.`)
flag.Var(&reflHeaders, "reflect-header",
`Additional reflection headers in 'name: value' format. May specify more
than one via multiple flags. These headers will only be used during
reflection requests and will be excluded when invoking the requested RPC
method.`)
flag.Var(&protoset, "protoset",
`The name of a file containing an encoded FileDescriptorSet. This file's
contents will be used to determine the RPC schema instead of querying
@ -155,7 +169,10 @@ func main() {
args = args[1:]
} else {
if *data != "" {
fail(nil, "The -d argument is not used with 'list' or 'describe' verb.")
warn("The -d argument is not used with 'list' or 'describe' verb.")
}
if len(rpcHeaders) > 0 {
warn("The -rpc-header argument is not used with 'list' or 'describe' verb.")
}
if len(args) > 0 {
symbol = args[0]
@ -172,6 +189,9 @@ func main() {
if len(protoset) == 0 && target == "" {
fail(nil, "No host:port specified and no protoset specified.")
}
if len(protoset) > 0 && len(reflHeaders) > 0 {
warn("The -reflect-header argument is not used when -protoset files are used ")
}
ctx := context.Background()
if *maxTime != "" {
@ -231,7 +251,7 @@ func main() {
fail(err, "Failed to process proto descriptor sets")
}
} else {
md := grpcurl.MetadataFromHeaders(addlHeaders)
md := grpcurl.MetadataFromHeaders(append(addlHeaders, reflHeaders...))
refCtx := metadata.NewOutgoingContext(ctx, md)
cc = dial()
refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(cc))
@ -329,6 +349,16 @@ func main() {
fail(err, "Failed to describe symbol %q", s)
}
fmt.Println(txt)
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))
fmt.Println("\nMessage template:")
jsm := jsonpb.Marshaler{Indent: " ", EmitDefaults: true}
jsm.Marshal(os.Stdout, tmpl)
fmt.Println()
}
}
} else {
@ -344,7 +374,7 @@ func main() {
}
h := &handler{dec: dec, descSource: descSource}
err := grpcurl.InvokeRpc(ctx, descSource, cc, symbol, addlHeaders, h, h.getRequestData)
err := grpcurl.InvokeRpc(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), h, h.getRequestData)
if err != nil {
fail(err, "Error invoking method %q", symbol)
}
@ -389,6 +419,11 @@ the method's request type will be sent.
}
func warn(msg string, args ...interface{}) {
msg = fmt.Sprintf("Warning: %s\n", msg)
fmt.Fprintf(os.Stderr, msg, args...)
}
func fail(err error, msg string, args ...interface{}) {
if err != nil {
msg += ": %v"
@ -466,3 +501,64 @@ func (h *handler) OnReceiveTrailers(stat *status.Status, md metadata.MD) {
fmt.Printf("\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
}