Skip to content

Commit

Permalink
Use reflection + Generics to delete most CLI boilerplate for queries …
Browse files Browse the repository at this point in the history
…(backport #3611) (#3633)

Makes new, far more succinct methods, to build tx and query CLI's

Co-authored-by: Dev Ojha <ValarDragon@users.noreply.github.com>
Co-authored-by: Dev Ojha <dojha@berkeley.edu>
  • Loading branch information
3 people authored Dec 5, 2022
1 parent 90ccdd0 commit fe6d544
Show file tree
Hide file tree
Showing 33 changed files with 1,111 additions and 3,055 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Misc Improvements

* [#3611](https://github.com/osmosis-labs/osmosis/pull/3611) Introduce osmocli, to automate thousands of lines of CLI boilerplate

## v13.0.0

Expand Down
17 changes: 17 additions & 0 deletions osmoutils/generic_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package osmoutils

import "reflect"

// MakeNew makes a new instance of generic T.
// if T is a pointer, makes a new instance of the underlying struct via reflection,
// and then a pointer to it.
func MakeNew[T any]() T {
var v T
if typ := reflect.TypeOf(v); typ.Kind() == reflect.Ptr {
elem := typ.Elem()
//nolint:forcetypeassert
return reflect.New(elem).Interface().(T) // must use reflect
} else {
return *new(T) // v is not ptr, alloc with new
}
}
13 changes: 13 additions & 0 deletions osmoutils/osmocli/flag_advice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package osmocli

type FlagAdvice struct {
HasPagination bool

// Map of FieldName -> FlagName
CustomFlagOverrides map[string]string

// Tx sender value
IsTx bool
TxSenderFieldName string
FromValue string
}
31 changes: 31 additions & 0 deletions osmoutils/osmocli/index_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package osmocli

import (
"fmt"

"github.com/spf13/cobra"
)

// Index command, but short is not set. That is left to caller.
func IndexCmd(moduleName string) *cobra.Command {
return &cobra.Command{
Use: moduleName,
Short: fmt.Sprintf("Querying commands for the %s module", moduleName),
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: indexRunCmd,
}
}

func indexRunCmd(cmd *cobra.Command, args []string) error {
usageTemplate := `Usage:{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command]{{end}}
{{if .HasAvailableSubCommands}}Available Commands:{{range .Commands}}{{if .IsAvailableCommand}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`
cmd.SetUsageTemplate(usageTemplate)
return cmd.Help()
}
279 changes: 279 additions & 0 deletions osmoutils/osmocli/parsers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
package osmocli

import (
"fmt"
"reflect"
"strconv"
"strings"
"time"

"github.com/cosmos/cosmos-sdk/client"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/spf13/pflag"

"github.com/osmosis-labs/osmosis/v13/osmoutils"
)

// Parses arguments 1-1 from args
// makes an exception, where it allows Pagination to come from flags.
func ParseFieldsFromFlagsAndArgs[reqP any](flagAdvice FlagAdvice, flags *pflag.FlagSet, args []string) (reqP, error) {
req := osmoutils.MakeNew[reqP]()
v := reflect.ValueOf(req).Elem()
t := v.Type()

argIndexOffset := 0
// Iterate over the fields in the struct
for i := 0; i < t.NumField(); i++ {
arg := ""
if len(args) > i+argIndexOffset {
arg = args[i+argIndexOffset]
}
usedArg, err := ParseField(v, t, i, arg, flagAdvice, flags)
if err != nil {
return req, err
}
if !usedArg {
argIndexOffset -= 1
}
}
return req, nil
}

func ParseNumFields[reqP any]() int {
req := osmoutils.MakeNew[reqP]()
v := reflect.ValueOf(req).Elem()
t := v.Type()
return t.NumField()
}

func ParseExpectedQueryFnName[reqP any]() string {
req := osmoutils.MakeNew[reqP]()
v := reflect.ValueOf(req).Elem()
s := v.Type().String()
// handle some non-std queries
var prefixTrimmed string
if strings.Contains(s, "Query") {
prefixTrimmed = strings.Split(s, "Query")[1]
} else {
prefixTrimmed = strings.Split(s, ".")[1]
}
suffixTrimmed := strings.TrimSuffix(prefixTrimmed, "Request")
return suffixTrimmed
}

func ParseHasPagination[reqP any]() bool {
req := osmoutils.MakeNew[reqP]()
t := reflect.ValueOf(req).Elem().Type()
for i := 0; i < t.NumField(); i++ {
fType := t.Field(i)
if fType.Type.String() == paginationType {
return true
}
}
return false
}

const paginationType = "*query.PageRequest"

// ParseField parses field #fieldIndex from either an arg or a flag.
// Returns true if it was parsed from an argument.
// Returns error if there was an issue in parsing this field.
func ParseField(v reflect.Value, t reflect.Type, fieldIndex int, arg string, flagAdvice FlagAdvice, flags *pflag.FlagSet) (bool, error) {
fVal := v.Field(fieldIndex)
fType := t.Field(fieldIndex)
// fmt.Printf("Field %d: %s %s %s\n", fieldIndex, fType.Name, fType.Type, fType.Type.Kind())

parsedFromFlag, err := ParseFieldFromFlag(fVal, fType, flagAdvice, flags)
if err != nil {
return false, err
}
if parsedFromFlag {
return false, nil
}
return true, ParseFieldFromArg(fVal, fType, arg)
}

// ParseFieldFromFlag attempts to parses the value of a field in a struct from a flag.
// The field is identified by the provided `reflect.StructField`.
// The flag advice and `pflag.FlagSet` are used to determine the flag to parse the field from.
// If the field corresponds to a value from a flag, true is returned.
// Otherwise, `false` is returned.
// In the true case, the parsed value is set on the provided `reflect.Value`.
// An error is returned if there is an issue parsing the field from the flag.
func ParseFieldFromFlag(fVal reflect.Value, fType reflect.StructField, flagAdvice FlagAdvice, flags *pflag.FlagSet) (bool, error) {
lowercaseFieldNameStr := strings.ToLower(fType.Name)
if flagName, ok := flagAdvice.CustomFlagOverrides[lowercaseFieldNameStr]; ok {
return true, parseFieldFromDirectlySetFlag(fVal, fType, flagAdvice, flagName, flags)
}

kind := fType.Type.Kind()
switch kind {
case reflect.String:
if flagAdvice.IsTx {
// matchesFieldName is true if lowercaseFieldNameStr is the same as TxSenderFieldName,
// or if TxSenderFieldName is left blank, then matches fields named "sender" or "owner"
matchesFieldName := (flagAdvice.TxSenderFieldName == lowercaseFieldNameStr) ||
(flagAdvice.TxSenderFieldName == "" && (lowercaseFieldNameStr == "sender" || lowercaseFieldNameStr == "owner"))
if matchesFieldName {
fVal.SetString(flagAdvice.FromValue)
return true, nil
}
}
case reflect.Ptr:
if flagAdvice.HasPagination {
typeStr := fType.Type.String()
if typeStr == paginationType {
pageReq, err := client.ReadPageRequest(flags)
if err != nil {
return true, err
}
fVal.Set(reflect.ValueOf(pageReq))
return true, nil
}
}
}
return false, nil
}

func parseFieldFromDirectlySetFlag(fVal reflect.Value, fType reflect.StructField, flagAdvice FlagAdvice, flagName string, flags *pflag.FlagSet) error {
// get string. If its a string great, run through arg parser. Otherwise try setting directly
s, err := flags.GetString(flagName)
if err != nil {
flag := flags.Lookup(flagName)
if flag == nil {
return fmt.Errorf("Programmer set the flag name wrong. Flag %s does not exist", flagName)
}
t := flag.Value.Type()
if t == "uint64" {
u, err := flags.GetUint64(flagName)
if err != nil {
return err
}
fVal.SetUint(u)
return nil
}
}
return ParseFieldFromArg(fVal, fType, s)
}

func ParseFieldFromArg(fVal reflect.Value, fType reflect.StructField, arg string) error {
switch fType.Type.Kind() {
// SetUint allows anyof type u8, u16, u32, u64, and uint
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
u, err := ParseUint(arg, fType.Name)
if err != nil {
return err
}
fVal.SetUint(u)
return nil
// SetInt allows anyof type i8,i16,i32,i64 and int
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
typeStr := fType.Type.String()
var i int64
var err error
if typeStr == "time.Duration" {
dur, err2 := time.ParseDuration(arg)
i, err = int64(dur), err2
} else {
i, err = ParseInt(arg, fType.Name)
}
if err != nil {
return err
}
fVal.SetInt(i)
return nil
case reflect.String:
s, err := ParseDenom(arg, fType.Name)
if err != nil {
return err
}
fVal.SetString(s)
return nil
case reflect.Ptr:
case reflect.Slice:
typeStr := fType.Type.String()
if typeStr == "types.Coins" {
coins, err := ParseCoins(arg, fType.Name)
if err != nil {
return err
}
fVal.Set(reflect.ValueOf(coins))
return nil
}
case reflect.Struct:
typeStr := fType.Type.String()
var v any
var err error
if typeStr == "types.Coin" {
v, err = ParseCoin(arg, fType.Name)
} else if typeStr == "types.Int" {
v, err = ParseSdkInt(arg, fType.Name)
} else {
return fmt.Errorf("struct field type not recognized. Got type %v", fType)
}

if err != nil {
return err
}
fVal.Set(reflect.ValueOf(v))
return nil
}
fmt.Println(fType.Type.Kind().String())
return fmt.Errorf("field type not recognized. Got type %v", fType)
}

func ParseUint(arg string, fieldName string) (uint64, error) {
v, err := strconv.ParseUint(arg, 10, 64)
if err != nil {
return 0, fmt.Errorf("could not parse %s as uint for field %s: %w", arg, fieldName, err)
}
return v, nil
}

func ParseInt(arg string, fieldName string) (int64, error) {
v, err := strconv.ParseInt(arg, 10, 64)
if err != nil {
return 0, fmt.Errorf("could not parse %s as int for field %s: %w", arg, fieldName, err)
}
return v, nil
}

func ParseUnixTime(arg string, fieldName string) (time.Time, error) {
timeUnix, err := strconv.ParseInt(arg, 10, 64)
if err != nil {
return time.Time{}, fmt.Errorf("could not parse %s as unix time for field %s: %w", arg, fieldName, err)
}
startTime := time.Unix(timeUnix, 0)
return startTime, nil
}

func ParseDenom(arg string, fieldName string) (string, error) {
return strings.TrimSpace(arg), nil
}

// TODO: Make this able to read from some local alias file for denoms.
func ParseCoin(arg string, fieldName string) (sdk.Coin, error) {
coin, err := sdk.ParseCoinNormalized(arg)
if err != nil {
return sdk.Coin{}, fmt.Errorf("could not parse %s as sdk.Coin for field %s: %w", arg, fieldName, err)
}
return coin, nil
}

// TODO: Make this able to read from some local alias file for denoms.
func ParseCoins(arg string, fieldName string) (sdk.Coins, error) {
coins, err := sdk.ParseCoinsNormalized(arg)
if err != nil {
return sdk.Coins{}, fmt.Errorf("could not parse %s as sdk.Coins for field %s: %w", arg, fieldName, err)
}
return coins, nil
}

// TODO: This really shouldn't be getting used in the CLI, its misdesign on the CLI ux
func ParseSdkInt(arg string, fieldName string) (sdk.Int, error) {
i, ok := sdk.NewIntFromString(arg)
if !ok {
return sdk.Int{}, fmt.Errorf("could not parse %s as sdk.Int for field %s", arg, fieldName)
}
return i, nil
}
Loading

0 comments on commit fe6d544

Please sign in to comment.