feat(cli): dynamically generate query CLI commands (#11725)
* WIP on auto-generating CLi * WIP * WIP * WIP * add pagination.go * handle more flag types * WIP on refactoring * WIP * working tests * add docs * echo all flags * add repeated tests * remove comment * fix compositeListValue issue Co-authored-by: Anil Kumar Kammari <anil@vitwit.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Anil Kumar Kammari
mergify[bot]
parent
e44a4a9d80
commit
1c8a2d9069
@@ -0,0 +1,20 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/v2/cli/flag"
|
||||
)
|
||||
|
||||
// Builder manages options for building CLI commands.
|
||||
type Builder struct {
|
||||
|
||||
// flag.Builder embeds the flag builder and its options.
|
||||
flag.Builder
|
||||
|
||||
// GetClientConn specifies how CLI commands will resolve a grpc.ClientConnInterface
|
||||
// from a given context.
|
||||
GetClientConn func(context.Context) grpc.ClientConnInterface
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
type addressStringType struct{}
|
||||
|
||||
func (a addressStringType) NewValue(_ context.Context, _ *Builder) pflag.Value {
|
||||
return &addressValue{}
|
||||
}
|
||||
|
||||
func (a addressStringType) DefaultValue() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type addressValue struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func (a addressValue) Get() protoreflect.Value {
|
||||
return protoreflect.ValueOfString(a.value)
|
||||
}
|
||||
|
||||
func (a addressValue) String() string {
|
||||
return a.value
|
||||
}
|
||||
|
||||
func (a *addressValue) Set(s string) error {
|
||||
a.value = s
|
||||
// TODO handle bech32 validation
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a addressValue) Type() string {
|
||||
return "bech32 account address key name"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"google.golang.org/protobuf/reflect/protodesc"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
)
|
||||
|
||||
// Builder manages options for building pflag flags for protobuf messages.
|
||||
type Builder struct {
|
||||
// TypeResolver specifies how protobuf types will be resolved. If it is
|
||||
// nil protoregistry.GlobalTypes will be used.
|
||||
TypeResolver interface {
|
||||
protoregistry.MessageTypeResolver
|
||||
protoregistry.ExtensionTypeResolver
|
||||
}
|
||||
|
||||
// FileResolver specifies how protobuf file descriptors will be resolved. If it is
|
||||
// nil protoregistry.GlobalFiles will be used.
|
||||
FileResolver protodesc.Resolver
|
||||
|
||||
messageFlagTypes map[protoreflect.FullName]Type
|
||||
scalarFlagTypes map[string]Type
|
||||
}
|
||||
|
||||
func (b *Builder) init() {
|
||||
if b.messageFlagTypes == nil {
|
||||
b.messageFlagTypes = map[protoreflect.FullName]Type{}
|
||||
b.messageFlagTypes["google.protobuf.Timestamp"] = timestampType{}
|
||||
b.messageFlagTypes["google.protobuf.Duration"] = durationType{}
|
||||
}
|
||||
|
||||
if b.scalarFlagTypes == nil {
|
||||
b.scalarFlagTypes = map[string]Type{}
|
||||
b.scalarFlagTypes["cosmos.AddressString"] = addressStringType{}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) DefineMessageFlagType(messageName protoreflect.FullName, flagType Type) {
|
||||
b.init()
|
||||
b.messageFlagTypes[messageName] = flagType
|
||||
}
|
||||
|
||||
func (b *Builder) DefineScalarFlagType(scalarName string, flagType Type) {
|
||||
b.init()
|
||||
b.scalarFlagTypes[scalarName] = flagType
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
type durationType struct{}
|
||||
|
||||
func (t durationType) NewValue(context.Context, *Builder) pflag.Value {
|
||||
return &durationValue{}
|
||||
}
|
||||
|
||||
func (t durationType) DefaultValue() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type durationValue struct {
|
||||
value *durationpb.Duration
|
||||
}
|
||||
|
||||
func (t durationValue) Get() protoreflect.Value {
|
||||
if t.value == nil {
|
||||
return protoreflect.Value{}
|
||||
}
|
||||
return protoreflect.ValueOfMessage(t.value.ProtoReflect())
|
||||
}
|
||||
|
||||
func (v durationValue) String() string {
|
||||
if v.value == nil {
|
||||
return ""
|
||||
}
|
||||
return v.value.AsDuration().String()
|
||||
}
|
||||
|
||||
func (v *durationValue) Set(s string) error {
|
||||
dur, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.value = durationpb.New(dur)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v durationValue) Type() string {
|
||||
return "duration"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
type enumType struct {
|
||||
enum protoreflect.EnumDescriptor
|
||||
}
|
||||
|
||||
func (b enumType) NewValue(context.Context, *Builder) pflag.Value {
|
||||
val := &enumValue{
|
||||
enum: b.enum,
|
||||
valMap: map[string]protoreflect.EnumValueDescriptor{},
|
||||
}
|
||||
n := b.enum.Values().Len()
|
||||
for i := 0; i < n; i++ {
|
||||
valDesc := b.enum.Values().Get(i)
|
||||
val.valMap[enumValueName(b.enum, valDesc)] = valDesc
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (b enumType) DefaultValue() string {
|
||||
defValue := ""
|
||||
if def := b.enum.Values().ByNumber(0); def != nil {
|
||||
defValue = enumValueName(b.enum, def)
|
||||
}
|
||||
return defValue
|
||||
}
|
||||
|
||||
type enumValue struct {
|
||||
enum protoreflect.EnumDescriptor
|
||||
value protoreflect.EnumNumber
|
||||
valMap map[string]protoreflect.EnumValueDescriptor
|
||||
}
|
||||
|
||||
func (e enumValue) Get() protoreflect.Value {
|
||||
return protoreflect.ValueOfEnum(e.value)
|
||||
}
|
||||
|
||||
func enumValueName(enum protoreflect.EnumDescriptor, enumValue protoreflect.EnumValueDescriptor) string {
|
||||
name := string(enumValue.Name())
|
||||
name = strings.TrimPrefix(name, strcase.ToScreamingSnake(string(enum.Name()))+"_")
|
||||
return strcase.ToKebab(name)
|
||||
}
|
||||
|
||||
func (e enumValue) String() string {
|
||||
return enumValueName(e.enum, e.enum.Values().ByNumber(e.value))
|
||||
}
|
||||
|
||||
func (e *enumValue) Set(s string) error {
|
||||
valDesc, ok := e.valMap[s]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s is not a valid value for enum %s", s, e.enum.FullName())
|
||||
}
|
||||
e.value = valDesc.Number()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e enumValue) Type() string {
|
||||
var vals []string
|
||||
n := e.enum.Values().Len()
|
||||
for i := 0; i < n; i++ {
|
||||
vals = append(vals, enumValueName(e.enum, e.enum.Values().Get(i)))
|
||||
}
|
||||
return fmt.Sprintf("%s (%s)", e.enum.Name(), strings.Join(vals, " | "))
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cosmos_proto "github.com/cosmos/cosmos-proto"
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/v2/internal/util"
|
||||
)
|
||||
|
||||
// FieldValueBinder wraps a flag value in a way that allows it to be bound
|
||||
// to a particular field in a protobuf message.
|
||||
type FieldValueBinder interface {
|
||||
Bind(message protoreflect.Message, field protoreflect.FieldDescriptor)
|
||||
}
|
||||
|
||||
// Options specifies options for specific flags.
|
||||
type Options struct {
|
||||
|
||||
// Prefix is a prefix to prepend to all flags.
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// AddFieldFlag adds a flag for the provided field to the flag set.
|
||||
func (b *Builder) AddFieldFlag(ctx context.Context, flagSet *pflag.FlagSet, field protoreflect.FieldDescriptor, options Options) FieldValueBinder {
|
||||
if field.Kind() == protoreflect.MessageKind && field.Message().FullName() == "cosmos.base.query.v1beta1.PageRequest" {
|
||||
return b.bindPageRequest(ctx, flagSet, field)
|
||||
}
|
||||
|
||||
name := options.Prefix + util.DescriptorKebabName(field)
|
||||
usage := util.DescriptorDocs(field)
|
||||
shorthand := ""
|
||||
|
||||
if typ := b.resolveFlagType(field); typ != nil {
|
||||
val := typ.NewValue(ctx, b)
|
||||
flagSet.AddFlag(&pflag.Flag{
|
||||
Name: name,
|
||||
Shorthand: shorthand,
|
||||
Usage: usage,
|
||||
DefValue: typ.DefaultValue(),
|
||||
Value: val,
|
||||
})
|
||||
switch val := val.(type) {
|
||||
case SimpleValue:
|
||||
return simpleValueBinder{val}
|
||||
case ListValue:
|
||||
return listValueBinder{val}
|
||||
default:
|
||||
panic(fmt.Errorf("%T does not implement SimpleValue or ListValue", val))
|
||||
}
|
||||
}
|
||||
|
||||
if field.IsList() {
|
||||
if value := bindSimpleListFlag(flagSet, field.Kind(), name, shorthand, usage); value != nil {
|
||||
return listValueBinder{value}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if value := bindSimpleFlag(flagSet, field.Kind(), name, shorthand, usage); value != nil {
|
||||
return simpleValueBinder{value}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) resolveFlagType(field protoreflect.FieldDescriptor) Type {
|
||||
typ := b.resolveFlagTypeBasic(field)
|
||||
if field.IsList() {
|
||||
if typ != nil {
|
||||
return compositeListType{simpleType: typ}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return typ
|
||||
}
|
||||
|
||||
func (b *Builder) resolveFlagTypeBasic(field protoreflect.FieldDescriptor) Type {
|
||||
scalar := proto.GetExtension(field.Options(), cosmos_proto.E_Scalar)
|
||||
if scalar != nil {
|
||||
b.init()
|
||||
if typ, ok := b.scalarFlagTypes[scalar.(string)]; ok {
|
||||
return typ
|
||||
}
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case protoreflect.EnumKind:
|
||||
return enumType{enum: field.Enum()}
|
||||
case protoreflect.MessageKind:
|
||||
b.init()
|
||||
if flagType, ok := b.messageFlagTypes[field.Message().FullName()]; ok {
|
||||
return flagType
|
||||
}
|
||||
|
||||
return jsonMessageFlagType{
|
||||
messageDesc: field.Message(),
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type simpleValueBinder struct {
|
||||
SimpleValue
|
||||
}
|
||||
|
||||
func (s simpleValueBinder) Bind(message protoreflect.Message, field protoreflect.FieldDescriptor) {
|
||||
val := s.Get()
|
||||
if val.IsValid() {
|
||||
message.Set(field, val)
|
||||
} else {
|
||||
message.Clear(field)
|
||||
}
|
||||
}
|
||||
|
||||
type listValueBinder struct {
|
||||
ListValue
|
||||
}
|
||||
|
||||
func (s listValueBinder) Bind(message protoreflect.Message, field protoreflect.FieldDescriptor) {
|
||||
s.AppendTo(message.NewField(field).List())
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
func bindSimpleListFlag(flagSet *pflag.FlagSet, kind protoreflect.Kind, name, shorthand, usage string) ListValue {
|
||||
switch kind {
|
||||
case protoreflect.StringKind:
|
||||
val := flagSet.StringSliceP(name, shorthand, nil, usage)
|
||||
return listValue(func(list protoreflect.List) {
|
||||
for _, x := range *val {
|
||||
list.Append(protoreflect.ValueOfString(x))
|
||||
}
|
||||
})
|
||||
case protoreflect.BytesKind:
|
||||
// TODO
|
||||
return nil
|
||||
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
|
||||
protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
|
||||
val := flagSet.UintSliceP(name, shorthand, nil, usage)
|
||||
return listValue(func(list protoreflect.List) {
|
||||
for _, x := range *val {
|
||||
list.Append(protoreflect.ValueOfUint64(uint64(x)))
|
||||
}
|
||||
})
|
||||
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
|
||||
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
|
||||
val := flagSet.IntSliceP(name, shorthand, nil, usage)
|
||||
return listValue(func(list protoreflect.List) {
|
||||
for _, x := range *val {
|
||||
list.Append(protoreflect.ValueOfInt64(int64(x)))
|
||||
}
|
||||
})
|
||||
case protoreflect.BoolKind:
|
||||
val := flagSet.BoolSliceP(name, shorthand, nil, usage)
|
||||
return listValue(func(list protoreflect.List) {
|
||||
for _, x := range *val {
|
||||
list.Append(protoreflect.ValueOfBool(x))
|
||||
}
|
||||
})
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type listValue func(protoreflect.List)
|
||||
|
||||
func (f listValue) AppendTo(list protoreflect.List) {
|
||||
f(list)
|
||||
}
|
||||
|
||||
type compositeListType struct {
|
||||
simpleType Type
|
||||
}
|
||||
|
||||
func (t compositeListType) NewValue(ctx context.Context, opts *Builder) pflag.Value {
|
||||
return &compositeListValue{
|
||||
simpleType: t.simpleType,
|
||||
values: nil,
|
||||
ctx: ctx,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (t compositeListType) DefaultValue() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type compositeListValue struct {
|
||||
simpleType Type
|
||||
values []protoreflect.Value
|
||||
ctx context.Context
|
||||
opts *Builder
|
||||
}
|
||||
|
||||
func (c compositeListValue) AppendTo(list protoreflect.List) {
|
||||
for _, value := range c.values {
|
||||
list.Append(value)
|
||||
}
|
||||
}
|
||||
|
||||
func (c compositeListValue) String() string {
|
||||
if len(c.values) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%+v", c.values)
|
||||
}
|
||||
|
||||
func (c *compositeListValue) Set(val string) error {
|
||||
simpleVal := c.simpleType.NewValue(c.ctx, c.opts)
|
||||
err := simpleVal.Set(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.values = append(c.values, simpleVal.(SimpleValue).Get())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c compositeListValue) Type() string {
|
||||
return fmt.Sprintf("%s (repeated)", c.simpleType.NewValue(c.ctx, c.opts).Type())
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/v2/internal/util"
|
||||
)
|
||||
|
||||
type jsonMessageFlagType struct {
|
||||
messageDesc protoreflect.MessageDescriptor
|
||||
}
|
||||
|
||||
func (j jsonMessageFlagType) NewValue(_ context.Context, builder *Builder) pflag.Value {
|
||||
return &jsonMessageFlagValue{
|
||||
messageType: util.ResolveMessageType(builder.TypeResolver, j.messageDesc),
|
||||
jsonMarshalOptions: protojson.MarshalOptions{Resolver: builder.TypeResolver},
|
||||
jsonUnmarshalOptions: protojson.UnmarshalOptions{Resolver: builder.TypeResolver},
|
||||
}
|
||||
}
|
||||
|
||||
func (j jsonMessageFlagType) DefaultValue() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type jsonMessageFlagValue struct {
|
||||
jsonMarshalOptions protojson.MarshalOptions
|
||||
jsonUnmarshalOptions protojson.UnmarshalOptions
|
||||
messageType protoreflect.MessageType
|
||||
message proto.Message
|
||||
}
|
||||
|
||||
func (j jsonMessageFlagValue) Get() protoreflect.Value {
|
||||
if j.message == nil {
|
||||
return protoreflect.Value{}
|
||||
}
|
||||
return protoreflect.ValueOfMessage(j.message.ProtoReflect())
|
||||
}
|
||||
|
||||
func (j jsonMessageFlagValue) String() string {
|
||||
if j.message == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
bz, err := j.jsonMarshalOptions.Marshal(j.message)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return string(bz)
|
||||
}
|
||||
|
||||
func (j *jsonMessageFlagValue) Set(s string) error {
|
||||
j.message = j.messageType.New().Interface()
|
||||
return j.jsonUnmarshalOptions.Unmarshal([]byte(s), j.message)
|
||||
}
|
||||
|
||||
func (j jsonMessageFlagValue) Type() string {
|
||||
return fmt.Sprintf("%s (json)", j.messageType.Descriptor().FullName())
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/v2/internal/util"
|
||||
)
|
||||
|
||||
func (b *Builder) bindPageRequest(ctx context.Context, flagSet *pflag.FlagSet, field protoreflect.FieldDescriptor) FieldValueBinder {
|
||||
handler := b.AddMessageFlags(
|
||||
ctx,
|
||||
flagSet,
|
||||
util.ResolveMessageType(b.TypeResolver, field.Message()),
|
||||
Options{Prefix: "page-"},
|
||||
)
|
||||
return simpleValueBinder{handler}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
// AddMessageFlags adds flags for each field in the message to the flag set.
|
||||
func (b *Builder) AddMessageFlags(ctx context.Context, set *pflag.FlagSet, messageType protoreflect.MessageType, options Options) *MessageBinder {
|
||||
fields := messageType.Descriptor().Fields()
|
||||
numFields := fields.Len()
|
||||
handler := &MessageBinder{
|
||||
messageType: messageType,
|
||||
}
|
||||
for i := 0; i < numFields; i++ {
|
||||
field := fields.Get(i)
|
||||
binder := b.AddFieldFlag(ctx, set, field, options)
|
||||
if binder == nil {
|
||||
fmt.Printf("unable to bind field %s to a flag, support will be added soon\n", field)
|
||||
continue
|
||||
}
|
||||
handler.flagFieldPairs = append(handler.flagFieldPairs, struct {
|
||||
binder FieldValueBinder
|
||||
field protoreflect.FieldDescriptor
|
||||
}{binder: binder, field: field})
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// MessageBinder binds multiple flags in a flag set to a protobuf message.
|
||||
type MessageBinder struct {
|
||||
flagFieldPairs []struct {
|
||||
binder FieldValueBinder
|
||||
field protoreflect.FieldDescriptor
|
||||
}
|
||||
messageType protoreflect.MessageType
|
||||
}
|
||||
|
||||
// BuildMessage builds and returns a new message for the bound flags.
|
||||
func (m MessageBinder) BuildMessage() protoreflect.Message {
|
||||
msg := m.messageType.New()
|
||||
m.Bind(msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
// Bind binds the flag values to an existing protobuf message.
|
||||
func (m MessageBinder) Bind(msg protoreflect.Message) {
|
||||
for _, pair := range m.flagFieldPairs {
|
||||
pair.binder.Bind(msg, pair.field)
|
||||
}
|
||||
}
|
||||
|
||||
// Get calls BuildMessage and wraps the result in a protoreflect.Value.
|
||||
func (m MessageBinder) Get() protoreflect.Value {
|
||||
return protoreflect.ValueOfMessage(m.BuildMessage())
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
func bindSimpleFlag(flagSet *pflag.FlagSet, kind protoreflect.Kind, name, shorthand, usage string) SimpleValue {
|
||||
switch kind {
|
||||
case protoreflect.BytesKind:
|
||||
val := flagSet.BytesBase64P(name, shorthand, nil, usage)
|
||||
return simpleValue(func() protoreflect.Value {
|
||||
return protoreflect.ValueOfBytes(*val)
|
||||
})
|
||||
case protoreflect.StringKind:
|
||||
val := flagSet.StringP(name, shorthand, "", usage)
|
||||
return simpleValue(func() protoreflect.Value {
|
||||
return protoreflect.ValueOfString(*val)
|
||||
})
|
||||
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
|
||||
val := flagSet.Uint32P(name, shorthand, 0, usage)
|
||||
return simpleValue(func() protoreflect.Value {
|
||||
return protoreflect.ValueOfUint32(*val)
|
||||
})
|
||||
case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
|
||||
val := flagSet.Uint64P(name, shorthand, 0, usage)
|
||||
return simpleValue(func() protoreflect.Value {
|
||||
return protoreflect.ValueOfUint64(*val)
|
||||
})
|
||||
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
|
||||
val := flagSet.Int32P(name, shorthand, 0, usage)
|
||||
return simpleValue(func() protoreflect.Value {
|
||||
return protoreflect.ValueOfInt32(*val)
|
||||
})
|
||||
case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
|
||||
val := flagSet.Int64P(name, shorthand, 0, usage)
|
||||
return simpleValue(func() protoreflect.Value {
|
||||
return protoreflect.ValueOfInt64(*val)
|
||||
})
|
||||
case protoreflect.BoolKind:
|
||||
val := flagSet.BoolP(name, shorthand, false, usage)
|
||||
return simpleValue(func() protoreflect.Value {
|
||||
return protoreflect.ValueOfBool(*val)
|
||||
})
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type simpleValue func() protoreflect.Value
|
||||
|
||||
func (f simpleValue) Get() protoreflect.Value {
|
||||
return f()
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type timestampType struct{}
|
||||
|
||||
func (t timestampType) NewValue(context.Context, *Builder) pflag.Value {
|
||||
return ×tampValue{}
|
||||
}
|
||||
|
||||
func (t timestampType) DefaultValue() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type timestampValue struct {
|
||||
value *timestamppb.Timestamp
|
||||
}
|
||||
|
||||
func (t timestampValue) Get() protoreflect.Value {
|
||||
if t.value == nil {
|
||||
return protoreflect.Value{}
|
||||
}
|
||||
return protoreflect.ValueOfMessage(t.value.ProtoReflect())
|
||||
}
|
||||
|
||||
func (v timestampValue) String() string {
|
||||
if v.value == nil {
|
||||
return ""
|
||||
}
|
||||
return v.value.AsTime().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (v *timestampValue) Set(s string) error {
|
||||
t, err := time.Parse(time.RFC3339, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.value = timestamppb.New(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v timestampValue) Type() string {
|
||||
return "timestamp (RFC 3339)"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Type specifies a custom flag type.
|
||||
type Type interface {
|
||||
|
||||
// NewValue returns a new pflag.Value which must also implement either
|
||||
// SimpleValue or ListValue.
|
||||
NewValue(context.Context, *Builder) pflag.Value
|
||||
|
||||
// DefaultValue is the default value for this type.
|
||||
DefaultValue() string
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
// SimpleValue wraps a simple (non-list and non-map) protobuf value.
|
||||
type SimpleValue interface {
|
||||
|
||||
// Get returns the value.
|
||||
Get() protoreflect.Value
|
||||
}
|
||||
|
||||
// ListValue wraps a protobuf list/repeating value.
|
||||
type ListValue interface {
|
||||
|
||||
// AppendTo appends the values to the provided list.
|
||||
AppendTo(protoreflect.List)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/v2/cli/flag"
|
||||
"github.com/cosmos/cosmos-sdk/client/v2/internal/util"
|
||||
)
|
||||
|
||||
// AddQueryServiceCommands adds a sub-command to the provided command for each
|
||||
// method in the specified service and returns the command.
|
||||
func (b *Builder) AddQueryServiceCommands(command *cobra.Command, serviceName protoreflect.FullName) *cobra.Command {
|
||||
resolver := b.FileResolver
|
||||
if resolver == nil {
|
||||
resolver = protoregistry.GlobalFiles
|
||||
}
|
||||
descriptor, err := resolver.FindDescriptorByName(serviceName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
service := descriptor.(protoreflect.ServiceDescriptor)
|
||||
methods := service.Methods()
|
||||
n := methods.Len()
|
||||
for i := 0; i < n; i++ {
|
||||
cmd := b.CreateQueryMethodCommand(methods.Get(i))
|
||||
command.AddCommand(cmd)
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// CreateQueryMethodCommand creates a gRPC query command for the given service method.
|
||||
func (b *Builder) CreateQueryMethodCommand(descriptor protoreflect.MethodDescriptor) *cobra.Command {
|
||||
serviceDescriptor := descriptor.Parent().(protoreflect.ServiceDescriptor)
|
||||
docs := util.DescriptorDocs(descriptor)
|
||||
getClientConn := b.GetClientConn
|
||||
methodName := fmt.Sprintf("/%s/%s", serviceDescriptor.FullName(), descriptor.Name())
|
||||
|
||||
inputDesc := descriptor.Input()
|
||||
inputType := util.ResolveMessageType(b.TypeResolver, inputDesc)
|
||||
outputType := util.ResolveMessageType(b.TypeResolver, descriptor.Output())
|
||||
cmd := &cobra.Command{
|
||||
Use: protoNameToCliName(descriptor.Name()),
|
||||
Long: docs,
|
||||
}
|
||||
|
||||
binder := b.AddMessageFlags(cmd.Context(), cmd.Flags(), inputType, flag.Options{})
|
||||
|
||||
jsonMarshalOptions := protojson.MarshalOptions{
|
||||
Indent: " ",
|
||||
UseProtoNames: true,
|
||||
UseEnumNumbers: false,
|
||||
EmitUnpopulated: true,
|
||||
Resolver: b.TypeResolver,
|
||||
}
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
clientConn := getClientConn(ctx)
|
||||
input := binder.BuildMessage()
|
||||
output := outputType.New()
|
||||
err := clientConn.Invoke(ctx, methodName, input.Interface(), output.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bz, err := jsonMarshalOptions.Marshal(output.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(cmd.OutOrStdout(), string(bz))
|
||||
return err
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func protoNameToCliName(name protoreflect.Name) string {
|
||||
return strcase.ToKebab(string(name))
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/v2/internal/testpb"
|
||||
)
|
||||
|
||||
func testExec(t *testing.T, args ...string) *testClientConn {
|
||||
server := grpc.NewServer()
|
||||
testpb.RegisterQueryServer(server, &testEchoServer{})
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
assert.NilError(t, err)
|
||||
go server.Serve(listener)
|
||||
defer server.GracefulStop()
|
||||
clientConn, err := grpc.Dial(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.NilError(t, err)
|
||||
defer clientConn.Close()
|
||||
|
||||
conn := &testClientConn{
|
||||
ClientConn: clientConn,
|
||||
t: t,
|
||||
out: &bytes.Buffer{},
|
||||
}
|
||||
b := &Builder{
|
||||
GetClientConn: func(ctx context.Context) grpc.ClientConnInterface {
|
||||
return conn
|
||||
},
|
||||
}
|
||||
cmd := b.AddQueryServiceCommands(&cobra.Command{Use: "test"}, protoreflect.FullName(testpb.Query_ServiceDesc.ServiceName))
|
||||
cmd.SetArgs(args)
|
||||
cmd.SetOut(conn.out)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
return conn
|
||||
}
|
||||
|
||||
func TestEcho(t *testing.T) {
|
||||
conn := testExec(t,
|
||||
"echo",
|
||||
"--a-bool",
|
||||
"--an-enum", "one",
|
||||
"--a-message", `{"bar":"abc", "baz":-3}`,
|
||||
"--duration", "4h3s",
|
||||
"--u-32", "27",
|
||||
"--u-64", "3267246890",
|
||||
"--i-32", "-253",
|
||||
"--i-64", "-234602347",
|
||||
"--str", "def",
|
||||
"--timestamp", "2019-01-02T00:01:02Z",
|
||||
"--a-coin", `{"denom":"foo","amount":"100000"}`,
|
||||
"--an-address", "cosmossdghdsfoi2134sdgh",
|
||||
"--bz", "c2RncXdlZndkZ3NkZw==",
|
||||
"--page-count-total",
|
||||
"--page-key", "MTIzNTQ4N3NnaGRhcw==",
|
||||
"--page-limit", "1000",
|
||||
"--page-offset", "10",
|
||||
"--page-reverse",
|
||||
"--bools", "true",
|
||||
"--bools", "false,false,true",
|
||||
"--enums", "one",
|
||||
"--enums", "five",
|
||||
"--enums", "two",
|
||||
"--strings", "abc",
|
||||
"--strings", "xyz",
|
||||
"--strings", "xyz,qrs",
|
||||
"--durations", "3s",
|
||||
"--durations", "5s",
|
||||
"--durations", "10h",
|
||||
"--some-messages", "{}",
|
||||
"--some-messages", `{"bar":"baz"}`,
|
||||
"--some-messages", `{"baz":-1}`,
|
||||
"--uints", "1,2,3",
|
||||
"--uints", "4",
|
||||
)
|
||||
assert.DeepEqual(t, conn.lastRequest, conn.lastResponse.(*testpb.EchoResponse).Request, protocmp.Transform())
|
||||
}
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
conn := testExec(t, "echo", "-h")
|
||||
golden.Assert(t, conn.out.String(), "help.golden")
|
||||
}
|
||||
|
||||
type testClientConn struct {
|
||||
*grpc.ClientConn
|
||||
t *testing.T
|
||||
lastRequest interface{}
|
||||
lastResponse interface{}
|
||||
out *bytes.Buffer
|
||||
}
|
||||
|
||||
func (t *testClientConn) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
|
||||
err := t.ClientConn.Invoke(ctx, method, args, reply, opts...)
|
||||
t.lastRequest = args
|
||||
t.lastResponse = reply
|
||||
return err
|
||||
}
|
||||
|
||||
type testEchoServer struct {
|
||||
testpb.UnimplementedQueryServer
|
||||
}
|
||||
|
||||
func (t testEchoServer) Echo(_ context.Context, request *testpb.EchoRequest) (*testpb.EchoResponse, error) {
|
||||
return &testpb.EchoResponse{Request: request}, nil
|
||||
}
|
||||
|
||||
var _ testpb.QueryServer = testEchoServer{}
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
Usage:
|
||||
test echo [flags]
|
||||
|
||||
Flags:
|
||||
--a-bool
|
||||
--a-coin cosmos.base.v1beta1.Coin (json)
|
||||
--a-message testpb.AMessage (json)
|
||||
--an-address bech32 account address key name
|
||||
--an-enum Enum (unspecified | one | two | five | neg-three) (default unspecified)
|
||||
--bools bools (default [])
|
||||
--bz bytesBase64
|
||||
--duration duration
|
||||
--durations duration (repeated)
|
||||
--enums Enum (unspecified | one | two | five | neg-three) (repeated)
|
||||
-h, --help help for echo
|
||||
--i-32 int32
|
||||
--i-64 int
|
||||
--page-count-total
|
||||
--page-key bytesBase64
|
||||
--page-limit uint
|
||||
--page-offset uint
|
||||
--page-reverse
|
||||
--some-messages testpb.AMessage (json) (repeated)
|
||||
--str string
|
||||
--strings strings
|
||||
--timestamp timestamp (RFC 3339)
|
||||
--u-32 uint32
|
||||
--u-64 uint
|
||||
--uints uints (default [])
|
||||
Reference in New Issue
Block a user