Skip to content

Commit

Permalink
feat: implement autocli customization (#13251)
Browse files Browse the repository at this point in the history
* feat: implement autocli customization

* WIP

* integrate simple options

* skip positional fields

* WIP

* WIP

* WIP

* trying to simplify diff, fixing errors

* WIP

* WIP

* tests passing again

* positional params working

* add TODO

* WIP on tests

* WIP on tests

* working tests

* doc strings

* docs

* tests

* tests

* simplify API

* proper validation

* fix import

* address review comments

* address review comments

Co-authored-by: Marko <[email protected]>
  • Loading branch information
aaronc and tac0turtle authored Oct 13, 2022
1 parent 6fdcbfd commit fa2cb40
Show file tree
Hide file tree
Showing 28 changed files with 1,494 additions and 363 deletions.
9 changes: 5 additions & 4 deletions api/cosmos/auth/v1beta1/query.pulsar.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions api/cosmos/nft/v1beta1/tx.pulsar.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion api/cosmos/query/v1/query.pulsar.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions api/cosmos/tx/v1beta1/service.pulsar.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions client/v2/cli/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package cli

import (
"errors"
"fmt"
"strings"

"github.com/spf13/cobra"
)

// NOTE: this was copied from client/cmd.go to avoid introducing a dependency
// on the v1 client package.

// validateCmd returns unknown command error or Help display if help flag set
func validateCmd(cmd *cobra.Command, args []string) error {
var unknownCmd string
var skipNext bool

for _, arg := range args {
// search for help flag
if arg == "--help" || arg == "-h" {
return cmd.Help()
}

// check if the current arg is a flag
switch {
case len(arg) > 0 && (arg[0] == '-'):
// the next arg should be skipped if the current arg is a
// flag and does not use "=" to assign the flag's value
if !strings.Contains(arg, "=") {
skipNext = true
} else {
skipNext = false
}
case skipNext:
// skip current arg
skipNext = false
case unknownCmd == "":
// unknown command found
// continue searching for help flag
unknownCmd = arg
}
}

// return the help screen if no unknown command is found
if unknownCmd != "" {
err := fmt.Sprintf("unknown command \"%s\" for \"%s\"", unknownCmd, cmd.CalledAs())

// build suggestions for unknown argument
if suggestions := cmd.SuggestionsFor(unknownCmd); len(suggestions) > 0 {
err += "\n\nDid you mean this?\n"
for _, s := range suggestions {
err += fmt.Sprintf("\t%v\n", s)
}
}
return errors.New(err)
}

return cmd.Help()
}
7 changes: 3 additions & 4 deletions client/v2/cli/flag/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ 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 {
func (a addressStringType) NewValue(_ context.Context, _ *Builder) Value {
return &addressValue{}
}

Expand All @@ -21,8 +20,8 @@ type addressValue struct {
value string
}

func (a addressValue) Get() protoreflect.Value {
return protoreflect.ValueOfString(a.value)
func (a addressValue) Get(protoreflect.Value) (protoreflect.Value, error) {
return protoreflect.ValueOfString(a.value), nil
}

func (a addressValue) String() string {
Expand Down
4 changes: 4 additions & 0 deletions client/v2/cli/flag/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Package flag defines functionality for automatically managing command
// line flags as well positional arguments that are based on protobuf message
// fields.
package flag
11 changes: 5 additions & 6 deletions client/v2/cli/flag/duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ 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 {
func (t durationType) NewValue(context.Context, *Builder) Value {
return &durationValue{}
}

Expand All @@ -23,11 +22,11 @@ type durationValue struct {
value *durationpb.Duration
}

func (t durationValue) Get() protoreflect.Value {
if t.value == nil {
return protoreflect.Value{}
func (a durationValue) Get(protoreflect.Value) (protoreflect.Value, error) {
if a.value == nil {
return protoreflect.Value{}, nil
}
return protoreflect.ValueOfMessage(t.value.ProtoReflect())
return protoreflect.ValueOfMessage(a.value.ProtoReflect()), nil
}

func (v durationValue) String() string {
Expand Down
7 changes: 3 additions & 4 deletions client/v2/cli/flag/enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import (
"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 {
func (b enumType) NewValue(context.Context, *Builder) Value {
val := &enumValue{
enum: b.enum,
valMap: map[string]protoreflect.EnumValueDescriptor{},
Expand All @@ -41,8 +40,8 @@ type enumValue struct {
valMap map[string]protoreflect.EnumValueDescriptor
}

func (e enumValue) Get() protoreflect.Value {
return protoreflect.ValueOfEnum(e.value)
func (e enumValue) Get(protoreflect.Value) (protoreflect.Value, error) {
return protoreflect.ValueOfEnum(e.value), nil
}

func enumValueName(enum protoreflect.EnumDescriptor, enumValue protoreflect.EnumValueDescriptor) string {
Expand Down
94 changes: 41 additions & 53 deletions client/v2/cli/flag/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package flag

import (
"context"
"fmt"

autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
cosmos_proto "github.com/cosmos/cosmos-proto"
"github.com/spf13/pflag"
"google.golang.org/protobuf/proto"
Expand All @@ -12,59 +12,68 @@ import (
"cosmossdk.io/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 {
// namingOptions specifies internal naming options for flags.
type namingOptions 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 {
// 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, opts *autocliv1.FlagOptions, options namingOptions) (name string, hasValue HasValue, err error) {
if opts == nil {
opts = &autocliv1.FlagOptions{}
}

if field.Kind() == protoreflect.MessageKind && field.Message().FullName() == "cosmos.base.query.v1beta1.PageRequest" {
return b.bindPageRequest(ctx, flagSet, field)
hasValue, err := b.bindPageRequest(ctx, flagSet, field)
return "", hasValue, err
}

name := options.Prefix + util.DescriptorKebabName(field)
usage := util.DescriptorDocs(field)
shorthand := ""
name = opts.Name
if name == "" {
name = options.Prefix + util.DescriptorKebabName(field)
}

usage := opts.Usage
if usage == "" {
usage = util.DescriptorDocs(field)
}

shorthand := opts.Shorthand
defaultValue := opts.DefaultValue

if typ := b.resolveFlagType(field); typ != nil {
if defaultValue == "" {
defaultValue = typ.DefaultValue()
}

val := typ.NewValue(ctx, b)
flagSet.AddFlag(&pflag.Flag{
Name: name,
Shorthand: shorthand,
Usage: usage,
DefValue: typ.DefaultValue(),
DefValue: 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))
}
return name, val, nil
}

// use the built-in pflag StringP, Int32P, etc. functions
var val HasValue
if field.IsList() {
if value := bindSimpleListFlag(flagSet, field.Kind(), name, shorthand, usage); value != nil {
return listValueBinder{value}
}
return nil
}
val = bindSimpleListFlag(flagSet, field.Kind(), name, shorthand, usage)

if value := bindSimpleFlag(flagSet, field.Kind(), name, shorthand, usage); value != nil {
return simpleValueBinder{value}
} else {
val = bindSimpleFlag(flagSet, field.Kind(), name, shorthand, usage)
}

return nil
// This is a bit of hacking around the pflag API, but the
// defaultValue is set in this way because this is much easier than trying
// to parse the string into the types that StringSliceP, Int32P, etc. expect
if defaultValue != "" {
err = flagSet.Set(name, defaultValue)
}
return name, val, err
}

func (b *Builder) resolveFlagType(field protoreflect.FieldDescriptor) Type {
Expand Down Expand Up @@ -105,24 +114,3 @@ func (b *Builder) resolveFlagTypeBasic(field protoreflect.FieldDescriptor) Type
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 fa2cb40

Please sign in to comment.