-
Notifications
You must be signed in to change notification settings - Fork 610
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use reflection + Generics to delete most CLI boilerplate for queries …
…(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
1 parent
90ccdd0
commit fe6d544
Showing
33 changed files
with
1,111 additions
and
3,055 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
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,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 | ||
} | ||
} |
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,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 | ||
} |
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,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() | ||
} |
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,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 | ||
} |
Oops, something went wrong.