Skip to content

Commit

Permalink
feat(cli): dynamically generate query CLI commands (#11725)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Apr 27, 2022
1 parent e44a4a9 commit 1c8a2d9
Show file tree
Hide file tree
Showing 27 changed files with 5,069 additions and 0 deletions.
2 changes: 2 additions & 0 deletions client/v2/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
codegen:
@(cd internal; buf generate)
20 changes: 20 additions & 0 deletions client/v2/cli/builder.go
Original file line number Diff line number Diff line change
@@ -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
}
40 changes: 40 additions & 0 deletions client/v2/cli/flag/address.go
Original file line number Diff line number Diff line change
@@ -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"
}
47 changes: 47 additions & 0 deletions client/v2/cli/flag/builder.go
Original file line number Diff line number Diff line change
@@ -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
}
52 changes: 52 additions & 0 deletions client/v2/cli/flag/duration.go
Original file line number Diff line number Diff line change
@@ -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"
}
74 changes: 74 additions & 0 deletions client/v2/cli/flag/enum.go
Original file line number Diff line number Diff line change
@@ -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, " | "))
}
129 changes: 129 additions & 0 deletions client/v2/cli/flag/field.go
Original file line number Diff line number Diff line change
@@ -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())
}
Loading

0 comments on commit 1c8a2d9

Please sign in to comment.