-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
- Loading branch information
1 parent
e44a4a9
commit 1c8a2d9
Showing
27 changed files
with
5,069 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
codegen: | ||
@(cd internal; buf generate) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, " | ")) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
Oops, something went wrong.