Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): dynamically generate query CLI commands #11725

Merged
merged 26 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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