feat(autocli): add simple msg support (#14832)

Co-authored-by: Aaron Craelius <aaron@regen.network>
Co-authored-by: Aaron Craelius <aaronc@users.noreply.github.com>
This commit is contained in:
Jeancarlo Barrios 2023-03-02 16:43:09 -05:00 committed by GitHub
parent 1edc60b871
commit ee458eb6c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 4570 additions and 176 deletions

View File

@ -4,6 +4,7 @@ import (
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
"cosmossdk.io/core/appmodule"
"cosmossdk.io/depinject"
"fmt"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/spf13/cobra"
@ -79,19 +80,41 @@ func (appOptions AppOptions) EnhanceRootCommandWithBuilder(rootCmd *cobra.Comman
}
customQueryCmds := map[string]*cobra.Command{}
customMsgCmds := map[string]*cobra.Command{}
for name, module := range appOptions.Modules {
if module, ok := module.(HasCustomQueryCommand); ok {
cmd := module.GetQueryCmd()
if queryModule, ok := module.(HasCustomQueryCommand); ok {
queryCmd := queryModule.GetQueryCmd()
// filter any nil commands
if cmd != nil {
customQueryCmds[name] = cmd
if queryCmd != nil {
customQueryCmds[name] = queryCmd
}
}
if msgModule, ok := module.(HasCustomTxCommand); ok {
msgCmd := msgModule.GetTxCmd()
// filter any nil commands
if msgCmd != nil {
customMsgCmds[name] = msgCmd
}
}
}
// if we have an existing query command, enhance it or build a custom one
enhanceQuery := func(cmd *cobra.Command, modOpts *autocliv1.ModuleOptions, moduleName string) error {
queryCmdDesc := modOpts.Query
if queryCmdDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Querying commands for the %s module", moduleName))
err := builder.AddQueryServiceCommands(cmd, queryCmdDesc)
if err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
if queryCmd := findSubCommand(rootCmd, "query"); queryCmd != nil {
if err := builder.EnhanceQueryCommand(queryCmd, moduleOptions, customQueryCmds); err != nil {
if err := builder.enhanceCommandCommon(queryCmd, moduleOptions, customQueryCmds, enhanceQuery); err != nil {
return err
}
} else {
@ -103,5 +126,32 @@ func (appOptions AppOptions) EnhanceRootCommandWithBuilder(rootCmd *cobra.Comman
rootCmd.AddCommand(queryCmd)
}
enhanceMsg := func(cmd *cobra.Command, modOpts *autocliv1.ModuleOptions, moduleName string) error {
txCmdDesc := modOpts.Tx
if txCmdDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Transations commands for the %s module", moduleName))
err := builder.AddQueryServiceCommands(cmd, txCmdDesc)
if err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
if msgCmd := findSubCommand(rootCmd, "tx"); msgCmd != nil {
if err := builder.enhanceCommandCommon(msgCmd, moduleOptions, customQueryCmds, enhanceMsg); err != nil {
return err
}
} else {
subCmd, err := builder.BuildMsgCommand(moduleOptions, customQueryCmds)
if err != nil {
return err
}
rootCmd.AddCommand(subCmd)
}
return nil
}

110
client/v2/autocli/common.go Normal file
View File

@ -0,0 +1,110 @@
package autocli
import (
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
"github.com/spf13/cobra"
"google.golang.org/protobuf/reflect/protoreflect"
"cosmossdk.io/client/v2/internal/util"
)
func (b *Builder) buildMethodCommandCommon(descriptor protoreflect.MethodDescriptor, options *autocliv1.RpcCommandOptions, exec func(cmd *cobra.Command, input protoreflect.Message) error) (*cobra.Command, error) {
if options == nil {
// use the defaults
options = &autocliv1.RpcCommandOptions{}
}
if options.Skip {
return nil, nil
}
long := options.Long
if long == "" {
long = util.DescriptorDocs(descriptor)
}
inputDesc := descriptor.Input()
inputType := util.ResolveMessageType(b.TypeResolver, inputDesc)
use := options.Use
if use == "" {
use = protoNameToCliName(descriptor.Name())
}
cmd := &cobra.Command{
Use: use,
Long: long,
Short: options.Short,
Example: options.Example,
Aliases: options.Alias,
SuggestFor: options.SuggestFor,
Deprecated: options.Deprecated,
Version: options.Version,
}
binder, err := b.AddMessageFlags(cmd.Context(), cmd.Flags(), inputType, options)
if err != nil {
return nil, err
}
cmd.Args = binder.CobraArgs
cmd.RunE = func(cmd *cobra.Command, args []string) error {
input, err := binder.BuildMessage(args)
if err != nil {
return err
}
return exec(cmd, input)
}
if b.AddQueryConnFlags != nil {
b.AddQueryConnFlags(cmd)
}
return cmd, nil
}
// enhanceCommandCommon enhances the provided query or msg command with either generated commands based on the provided module
// options or the provided custom commands for each module. If the provided query command already contains a command
// for a module, that command is not over-written by this method. This allows a graceful addition of autocli to
// automatically fill in missing commands.
func (b *Builder) enhanceCommandCommon(cmd *cobra.Command, moduleOptions map[string]*autocliv1.ModuleOptions, customCmds map[string]*cobra.Command, buildModuleCommand func(*cobra.Command, *autocliv1.ModuleOptions, string) error) error {
allModuleNames := map[string]bool{}
for moduleName := range moduleOptions {
allModuleNames[moduleName] = true
}
for moduleName := range customCmds {
allModuleNames[moduleName] = true
}
for moduleName := range allModuleNames {
// if we have an existing command skip adding one here
if cmd.HasSubCommands() {
if _, _, err := cmd.Find([]string{moduleName}); err == nil {
// command already exists, skip
continue
}
}
// if we have a custom command use that instead of generating one
if custom := customCmds[moduleName]; custom != nil {
// custom commands get added lower down
cmd.AddCommand(custom)
continue
}
// check for autocli options
modOpts := moduleOptions[moduleName]
if modOpts == nil {
continue
}
err := buildModuleCommand(cmd, modOpts, moduleName)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,56 @@
package autocli
import (
"bytes"
"cosmossdk.io/client/v2/internal/testpb"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"gotest.tools/v3/assert"
"google.golang.org/grpc/credentials/insecure"
"net"
"testing"
)
func testExecCommon(t *testing.T, buildModuleCommand func(string, *Builder) (*cobra.Command, error), args ...string) *testClientConn {
server := grpc.NewServer()
testpb.RegisterQueryServer(server, &testEchoServer{})
listener, err := net.Listen("tcp", "127.0.0.1:0")
assert.NilError(t, err)
go func() {
err := server.Serve(listener)
if err != nil {
panic(err)
}
}()
clientConn, err := grpc.Dial(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
assert.NilError(t, err)
defer func() {
err := clientConn.Close()
if err != nil {
panic(err)
}
}()
conn := &testClientConn{
ClientConn: clientConn,
t: t,
out: &bytes.Buffer{},
errorOut: &bytes.Buffer{},
}
b := &Builder{
GetClientConn: func(*cobra.Command) (grpc.ClientConnInterface, error) {
return conn, nil
},
}
cmd, err := buildModuleCommand("test", b)
assert.NilError(t, err)
assert.NilError(t, err)
cmd.SetArgs(args)
cmd.SetOut(conn.out)
cmd.SetErr(conn.errorOut)
cmd.Execute()
return conn
}

116
client/v2/autocli/msg.go Normal file
View File

@ -0,0 +1,116 @@
package autocli
import (
"fmt"
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
"github.com/cockroachdb/errors"
"github.com/spf13/cobra"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
// BuildMsgCommand builds the msg commands for all the provided modules. If a custom command is provided for a
// module, this is used instead of any automatically generated CLI commands. This allows apps to a fully dynamic client
// with a more customized experience if a binary with custom commands is downloaded.
func (b *Builder) BuildMsgCommand(moduleOptions map[string]*autocliv1.ModuleOptions, customCmds map[string]*cobra.Command) (*cobra.Command, error) {
msgCmd := topLevelCmd("tx", "Transaction subcommands")
enhanceMsg := func(cmd *cobra.Command, modOpts *autocliv1.ModuleOptions, moduleName string) error {
txCmdDesc := modOpts.Tx
if txCmdDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Transations commands for the %s module", moduleName))
err := b.AddMsgServiceCommands(cmd, txCmdDesc)
if err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
if err := b.enhanceCommandCommon(msgCmd, moduleOptions, customCmds, enhanceMsg); err != nil {
return nil, err
}
return msgCmd, nil
}
// AddMsgServiceCommands adds a sub-command to the provided command for each
// method in the specified service and returns the command. This can be used in
// order to add auto-generated commands to an existing command.
func (b *Builder) AddMsgServiceCommands(cmd *cobra.Command, cmdDescriptor *autocliv1.ServiceCommandDescriptor) error {
for cmdName, subCmdDescriptor := range cmdDescriptor.SubCommands {
subCmd := topLevelCmd(cmdName, fmt.Sprintf("Tx commands for the %s service", subCmdDescriptor.Service))
// Add recursive sub-commands if there are any. This is used for nested services.
err := b.AddMsgServiceCommands(subCmd, subCmdDescriptor)
if err != nil {
return err
}
cmd.AddCommand(subCmd)
}
if cmdDescriptor.Service == "" {
// skip empty command descriptor
return nil
}
resolver := b.FileResolver
if b.FileResolver == nil {
resolver = protoregistry.GlobalFiles
}
descriptor, err := resolver.FindDescriptorByName(protoreflect.FullName(cmdDescriptor.Service))
if err != nil {
return errors.Errorf("can't find service %s: %v", cmdDescriptor.Service, err)
}
service := descriptor.(protoreflect.ServiceDescriptor)
methods := service.Methods()
rpcOptMap := map[protoreflect.Name]*autocliv1.RpcCommandOptions{}
for _, option := range cmdDescriptor.RpcCommandOptions {
methodName := protoreflect.Name(option.RpcMethod)
// validate that methods exist
if m := methods.ByName(methodName); m == nil {
return fmt.Errorf("rpc method %q not found for service %q", methodName, service.FullName())
}
rpcOptMap[methodName] = option
}
methodsLength := methods.Len()
for i := 0; i < methodsLength; i++ {
methodDescriptor := methods.Get(i)
methodOpts := rpcOptMap[methodDescriptor.Name()]
methodCmd, err := b.BuildMsgMethodCommand(methodDescriptor, methodOpts)
if err != nil {
return err
}
if methodCmd != nil {
cmd.AddCommand(methodCmd)
}
}
return nil
}
func (b *Builder) BuildMsgMethodCommand(descriptor protoreflect.MethodDescriptor, options *autocliv1.RpcCommandOptions) (*cobra.Command, error) {
jsonMarshalOptions := protojson.MarshalOptions{
Indent: " ",
UseProtoNames: true,
UseEnumNumbers: false,
EmitUnpopulated: true,
Resolver: b.TypeResolver,
}
return b.buildMethodCommandCommon(descriptor, options, func(cmd *cobra.Command, input protoreflect.Message) error {
bz, err := jsonMarshalOptions.Marshal(input.Interface())
if err != nil {
return err
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), string(bz))
return err
})
}

View File

@ -0,0 +1,359 @@
package autocli
import (
"fmt"
"google.golang.org/protobuf/encoding/protojson"
"strings"
"testing"
"gotest.tools/v3/golden"
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
"cosmossdk.io/client/v2/internal/testpb"
)
var buildModuleMsgCommand = func(moduleName string, b *Builder) (*cobra.Command, error) {
cmd := topLevelCmd(moduleName, fmt.Sprintf("Transations commands for the %s module", moduleName))
err := b.AddMsgServiceCommands(cmd, testCmdMsgDesc)
return cmd, err
}
var testCmdMsgDesc = &autocliv1.ServiceCommandDescriptor{
Service: testpb.Msg_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{
RpcMethod: "Send",
Use: "send [pos1] [pos2] [pos3...]",
Version: "1.0",
Alias: []string{"s"},
SuggestFor: []string{"send"},
Example: "send 1 abc {}",
Short: "send msg the value provided by the user",
Long: "send msg the value provided by the user as a proto JSON object with populated with the provided fields and positional arguments",
PositionalArgs: []*autocliv1.PositionalArgDescriptor{
{
ProtoField: "positional1",
},
{
ProtoField: "positional2",
},
{
ProtoField: "positional3_varargs",
Varargs: true,
},
},
FlagOptions: map[string]*autocliv1.FlagOptions{
"u32": {
Name: "uint32",
Shorthand: "u",
Usage: "some random uint32",
},
"i32": {
Usage: "some random int32",
DefaultValue: "3",
},
"u64": {
Usage: "some random uint64",
DefaultValue: "5",
},
"deprecated_field": {
Deprecated: "don't use this",
},
"shorthand_deprecated_field": {
Shorthand: "d",
Deprecated: "bad idea",
},
"hidden_bool": {
Hidden: true,
},
},
},
},
SubCommands: map[string]*autocliv1.ServiceCommandDescriptor{
// we test the sub-command functionality using the same service with different options
"deprecatedmsg": {
Service: testpb.Msg_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{
RpcMethod: "Send",
Deprecated: "dont use this",
Short: "deprecated subcommand",
},
},
},
"skipmsg": {
Service: testpb.Msg_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{
RpcMethod: "Send",
Skip: true,
Short: "skip subcommand",
},
},
},
},
}
func TestMsgOptions(t *testing.T) {
conn := testExecCommon(t,
buildModuleMsgCommand,
"send", "5", "6", `{"denom":"foo","amount":"1"}`,
"--uint32", "7",
"--u64", "8",
)
response := conn.out.String()
var output testpb.MsgRequest
err := protojson.Unmarshal([]byte(response), &output)
assert.NilError(t, err)
assert.Equal(t, output.GetU32(), uint32(7))
assert.Equal(t, output.GetPositional1(), int32(5))
assert.Equal(t, output.GetPositional2(), "6")
}
func TestMsgOptionsError(t *testing.T) {
conn := testExecCommon(t, buildModuleMsgCommand,
"send", "5",
"--uint32", "7",
"--u64", "8",
)
assert.Assert(t, strings.Contains(conn.errorOut.String(), "requires at least 3 arg"))
conn = testExecCommon(t, buildModuleMsgCommand,
"send", "5", "6", `{"denom":"foo","amount":"1"}`,
"--uint32", "7",
"--u64", "abc",
)
assert.Assert(t, strings.Contains(conn.errorOut.String(), "invalid argument "))
}
func TestDeprecatedMsg(t *testing.T) {
conn := testExecCommon(t, buildModuleMsgCommand, "send",
"1", "abc", `{"denom":"foo","amount":"1"}`,
"--deprecated-field", "foo")
assert.Assert(t, strings.Contains(conn.out.String(), "--deprecated-field has been deprecated"))
conn = testExecCommon(t, buildModuleMsgCommand, "send",
"1", "abc", `{"denom":"foo","amount":"1"}`,
"-d", "foo")
assert.Assert(t, strings.Contains(conn.out.String(), "--shorthand-deprecated-field has been deprecated"))
}
func TestEverythingMsg(t *testing.T) {
conn := testExecCommon(t, buildModuleMsgCommand,
"send",
"1",
"abc",
`{"denom":"foo","amount":"1234"}`,
`{"denom":"bar","amount":"4321"}`,
"--a-bool",
"--an-enum", "two",
"--a-message", `{"bar":"abc", "baz":-3}`,
"--duration", "4h3s",
"--uint32", "27",
"--u64", "3267246890",
"--i32", "-253",
"--i64", "-234602347",
"--str", "def",
"--timestamp", "2019-01-02T00:01:02Z",
"--a-coin", `{"denom":"foo","amount":"100000"}`,
"--an-address", "cosmossdghdsfoi2134sdgh",
"--bz", "c2RncXdlZndkZ3NkZw==",
"--page-count-total",
"--page-key", "MTIzNTQ4N3NnaGRhcw==",
"--page-limit", "1000",
"--page-offset", "10",
"--page-reverse",
"--bools", "true",
"--bools", "false,false,true",
"--enums", "one",
"--enums", "five",
"--enums", "two",
"--strings", "abc",
"--strings", "xyz",
"--strings", "xyz,qrs",
"--durations", "3s",
"--durations", "5s",
"--durations", "10h",
"--some-messages", "{}",
"--some-messages", `{"bar":"baz"}`,
"--some-messages", `{"baz":-1}`,
"--uints", "1,2,3",
"--uints", "4",
)
response := conn.out.String()
var output testpb.MsgRequest
err := protojson.Unmarshal([]byte(response), &output)
assert.NilError(t, err)
assert.Equal(t, output.GetU32(), uint32(27))
assert.Equal(t, output.GetU64(), uint64(3267246890))
assert.Equal(t, output.GetPositional1(), int32(1))
assert.Equal(t, output.GetPositional2(), "abc")
assert.Equal(t, output.GetABool(), true)
assert.Equal(t, output.GetAnEnum(), testpb.Enum_ENUM_TWO)
}
func TestHelpMsg(t *testing.T) {
conn := testExecCommon(t, buildModuleMsgCommand, "-h")
golden.Assert(t, conn.out.String(), "help-toplevel-msg.golden")
conn = testExecCommon(t, buildModuleMsgCommand, "send", "-h")
golden.Assert(t, conn.out.String(), "help-echo-msg.golden")
conn = testExecCommon(t, buildModuleMsgCommand, "deprecatedmsg", "send", "-h")
golden.Assert(t, conn.out.String(), "help-deprecated-msg.golden")
}
func TestBuildCustomMsgCommand(t *testing.T) {
b := &Builder{}
customCommandCalled := false
cmd, err := b.BuildMsgCommand(map[string]*autocliv1.ModuleOptions{
"test": {
Tx: testCmdMsgDesc,
},
}, map[string]*cobra.Command{
"test": {Use: "test", Run: func(cmd *cobra.Command, args []string) {
customCommandCalled = true
}},
})
assert.NilError(t, err)
cmd.SetArgs([]string{"test", "tx"})
assert.NilError(t, cmd.Execute())
assert.Assert(t, customCommandCalled)
}
func TestErrorBuildMsgCommand(t *testing.T) {
b := &Builder{}
commandDescriptor := &autocliv1.ServiceCommandDescriptor{
Service: testpb.Msg_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{
RpcMethod: "Send",
PositionalArgs: []*autocliv1.PositionalArgDescriptor{
{
ProtoField: "un-existent-proto-field",
},
},
},
},
}
opts := map[string]*autocliv1.ModuleOptions{
"test": {
Tx: commandDescriptor,
},
}
_, err := b.BuildMsgCommand(opts, nil)
assert.ErrorContains(t, err, "can't find field un-existent-proto-field")
nonExistentService := &autocliv1.ServiceCommandDescriptor{Service: "un-existent-service"}
opts = map[string]*autocliv1.ModuleOptions{
"test": {
Tx: nonExistentService,
},
}
_, err = b.BuildMsgCommand(opts, nil)
assert.ErrorContains(t, err, "can't find service un-existent-service")
}
func TestNotFoundErrorsMsg(t *testing.T) {
b := &Builder{}
buildModuleMsgCommand := func(moduleName string, cmdDescriptor *autocliv1.ServiceCommandDescriptor) (*cobra.Command, error) {
cmd := topLevelCmd(moduleName, fmt.Sprintf("Transations commands for the %s module", moduleName))
err := b.AddMsgServiceCommands(cmd, cmdDescriptor)
return cmd, err
}
// Query non existent service
_, err := buildModuleMsgCommand("test", &autocliv1.ServiceCommandDescriptor{Service: "un-existent-service"})
assert.ErrorContains(t, err, "can't find service un-existent-service")
_, err = buildModuleMsgCommand("test", &autocliv1.ServiceCommandDescriptor{
Service: testpb.Query_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{{RpcMethod: "un-existent-method"}},
})
assert.ErrorContains(t, err, "rpc method \"un-existent-method\" not found")
_, err = buildModuleMsgCommand("test", &autocliv1.ServiceCommandDescriptor{
Service: testpb.Msg_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{
RpcMethod: "Send",
PositionalArgs: []*autocliv1.PositionalArgDescriptor{
{
ProtoField: "un-existent-proto-field",
},
},
},
},
})
assert.ErrorContains(t, err, "can't find field un-existent-proto-field")
_, err = buildModuleMsgCommand("test", &autocliv1.ServiceCommandDescriptor{
Service: testpb.Msg_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{
RpcMethod: "Send",
FlagOptions: map[string]*autocliv1.FlagOptions{
"un-existent-flag": {},
},
},
},
})
assert.ErrorContains(t, err, "can't find field un-existent-flag")
}
func TestEnhanceMessageCommand(t *testing.T) {
b := &Builder{}
enhanceMsg := func(cmd *cobra.Command, modOpts *autocliv1.ModuleOptions, moduleName string) error {
txCmdDesc := modOpts.Tx
if txCmdDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Transations commands for the %s module", moduleName))
err := b.AddMsgServiceCommands(cmd, txCmdDesc)
if err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
// Test that the command has a subcommand
cmd := &cobra.Command{Use: "test"}
cmd.AddCommand(&cobra.Command{Use: "test"})
options := map[string]*autocliv1.ModuleOptions{
"test": {},
}
err := b.enhanceCommandCommon(cmd, options, map[string]*cobra.Command{}, enhanceMsg)
assert.NilError(t, err)
cmd = &cobra.Command{Use: "test"}
options = map[string]*autocliv1.ModuleOptions{}
customCommands := map[string]*cobra.Command{
"test2": {Use: "test"},
}
err = b.enhanceCommandCommon(cmd, options, customCommands, enhanceMsg)
assert.NilError(t, err)
cmd = &cobra.Command{Use: "test"}
options = map[string]*autocliv1.ModuleOptions{
"test": {Tx: nil},
}
customCommands = map[string]*cobra.Command{}
err = b.enhanceCommandCommon(cmd, options, customCommands, enhanceMsg)
assert.NilError(t, err)
}
type testMessageServer struct {
testpb.UnimplementedMsgServer
}

View File

@ -10,7 +10,6 @@ import (
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"cosmossdk.io/client/v2/internal/strcase"
"cosmossdk.io/client/v2/internal/util"
)
@ -20,66 +19,25 @@ import (
func (b *Builder) BuildQueryCommand(moduleOptions map[string]*autocliv1.ModuleOptions, customCmds map[string]*cobra.Command) (*cobra.Command, error) {
queryCmd := topLevelCmd("query", "Querying subcommands")
queryCmd.Aliases = []string{"q"}
if err := b.EnhanceQueryCommand(queryCmd, moduleOptions, customCmds); err != nil {
return nil, err
}
return queryCmd, nil
}
// EnhanceQueryCommand enhances the provided query command with either generated commands based on the provided module
// options or the provided custom commands for each module. If the provided query command already contains a command
// for a module, that command is not over-written by this method. This allows a graceful addition of autocli to
// automatically fill in missing commands.
func (b *Builder) EnhanceQueryCommand(queryCmd *cobra.Command, moduleOptions map[string]*autocliv1.ModuleOptions, customCmds map[string]*cobra.Command) error {
allModuleNames := map[string]bool{}
for moduleName := range moduleOptions {
allModuleNames[moduleName] = true
}
for moduleName := range customCmds {
allModuleNames[moduleName] = true
}
for moduleName := range allModuleNames {
// if we have an existing command skip adding one here
if existing := findSubCommand(queryCmd, moduleName); existing != nil {
continue
}
// if we have a custom command use that instead of generating one
if custom := customCmds[moduleName]; custom != nil {
// custom commands get added lower down
queryCmd.AddCommand(custom)
continue
}
// check for autocli options
modOpts := moduleOptions[moduleName]
if modOpts == nil {
continue
}
queryCmdDesc := modOpts.Query
if queryCmdDesc != nil {
cmd, err := b.BuildModuleQueryCommand(moduleName, queryCmdDesc)
enhanceMsg := func(cmd *cobra.Command, modOpts *autocliv1.ModuleOptions, moduleName string) error {
txQueryDesc := modOpts.Query
if txQueryDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Querying commands for the %s module", moduleName))
err := b.AddQueryServiceCommands(subCmd, txQueryDesc)
if err != nil {
return err
}
queryCmd.AddCommand(cmd)
cmd.AddCommand(subCmd)
}
return nil
}
if err := b.enhanceCommandCommon(queryCmd, moduleOptions, customCmds, enhanceMsg); err != nil {
return nil, err
}
return nil
}
// BuildModuleQueryCommand builds the query command for a single module.
func (b *Builder) BuildModuleQueryCommand(moduleName string, cmdDescriptor *autocliv1.ServiceCommandDescriptor) (*cobra.Command, error) {
cmd := topLevelCmd(moduleName, fmt.Sprintf("Querying commands for the %s module", moduleName))
err := b.AddQueryServiceCommands(cmd, cmdDescriptor)
return cmd, err
return queryCmd, nil
}
// AddQueryServiceCommands adds a sub-command to the provided command for each
@ -143,52 +101,10 @@ func (b *Builder) AddQueryServiceCommands(cmd *cobra.Command, cmdDescriptor *aut
// BuildQueryMethodCommand creates a gRPC query command for the given service method. This can be used to auto-generate
// just a single command for a single service rpc method.
func (b *Builder) BuildQueryMethodCommand(descriptor protoreflect.MethodDescriptor, options *autocliv1.RpcCommandOptions) (*cobra.Command, error) {
if options == nil {
// use the defaults
options = &autocliv1.RpcCommandOptions{}
}
if options.Skip {
return nil, nil
}
serviceDescriptor := descriptor.Parent().(protoreflect.ServiceDescriptor)
long := options.Long
if long == "" {
long = util.DescriptorDocs(descriptor)
}
getClientConn := b.GetClientConn
serviceDescriptor := descriptor.Parent().(protoreflect.ServiceDescriptor)
methodName := fmt.Sprintf("/%s/%s", serviceDescriptor.FullName(), descriptor.Name())
inputDesc := descriptor.Input()
inputType := util.ResolveMessageType(b.TypeResolver, inputDesc)
outputType := util.ResolveMessageType(b.TypeResolver, descriptor.Output())
use := options.Use
if use == "" {
use = protoNameToCliName(descriptor.Name())
}
cmd := &cobra.Command{
Use: use,
Long: long,
Short: options.Short,
Example: options.Example,
Aliases: options.Alias,
SuggestFor: options.SuggestFor,
Deprecated: options.Deprecated,
Version: options.Version,
}
binder, err := b.AddMessageFlags(cmd.Context(), cmd.Flags(), inputType, options)
if err != nil {
return nil, err
}
cmd.Args = binder.CobraArgs
jsonMarshalOptions := protojson.MarshalOptions{
Indent: " ",
UseProtoNames: true,
@ -197,17 +113,12 @@ func (b *Builder) BuildQueryMethodCommand(descriptor protoreflect.MethodDescript
Resolver: b.TypeResolver,
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
cmd, err := b.buildMethodCommandCommon(descriptor, options, func(cmd *cobra.Command, input protoreflect.Message) error {
clientConn, err := getClientConn(cmd)
if err != nil {
return err
}
input, err := binder.BuildMessage(args)
if err != nil {
return err
}
output := outputType.New()
ctx := cmd.Context()
err = clientConn.Invoke(ctx, methodName, input.Interface(), output.Interface())
@ -222,6 +133,9 @@ func (b *Builder) BuildQueryMethodCommand(descriptor protoreflect.MethodDescript
_, err = fmt.Fprintln(cmd.OutOrStdout(), string(bz))
return err
})
if err != nil {
return nil, err
}
if b.AddQueryConnFlags != nil {
@ -230,17 +144,3 @@ func (b *Builder) BuildQueryMethodCommand(descriptor protoreflect.MethodDescript
return cmd, nil
}
func protoNameToCliName(name protoreflect.Name) string {
return strcase.ToKebab(string(name))
}
func topLevelCmd(use, short string) *cobra.Command {
return &cobra.Command{
Use: use,
Short: short,
DisableFlagParsing: false,
SuggestionsMinimumDistance: 2,
RunE: validateCmd,
}
}

View File

@ -3,14 +3,13 @@ package autocli
import (
"bytes"
"context"
"net"
"fmt"
"strings"
"testing"
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/testing/protocmp"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
@ -18,6 +17,14 @@ import (
"cosmossdk.io/client/v2/internal/testpb"
)
var buildModuleQueryCommand = func(moduleName string, b *Builder) (*cobra.Command, error) {
cmd := topLevelCmd(moduleName, fmt.Sprintf("Querying commands for the %s module", moduleName))
err := b.AddQueryServiceCommands(cmd, testCmdDesc)
return cmd, err
}
var testCmdDesc = &autocliv1.ServiceCommandDescriptor{
Service: testpb.Query_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
@ -92,47 +99,8 @@ var testCmdDesc = &autocliv1.ServiceCommandDescriptor{
},
}
func testExec(t *testing.T, args ...string) *testClientConn {
server := grpc.NewServer()
testpb.RegisterQueryServer(server, &testEchoServer{})
listener, err := net.Listen("tcp", "127.0.0.1:0")
assert.NilError(t, err)
go func() {
err := server.Serve(listener)
if err != nil {
panic(err)
}
}()
defer server.GracefulStop()
clientConn, err := grpc.Dial(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
assert.NilError(t, err)
defer func() {
err := clientConn.Close()
if err != nil {
panic(err)
}
}()
conn := &testClientConn{
ClientConn: clientConn,
t: t,
out: &bytes.Buffer{},
}
b := &Builder{
GetClientConn: func(*cobra.Command) (grpc.ClientConnInterface, error) {
return conn, nil
},
}
cmd, err := b.BuildModuleQueryCommand("test", testCmdDesc)
assert.NilError(t, err)
cmd.SetArgs(args)
cmd.SetOut(conn.out)
assert.NilError(t, cmd.Execute())
return conn
}
func TestEverything(t *testing.T) {
conn := testExec(t,
conn := testExecCommon(t, buildModuleQueryCommand,
"echo",
"1",
"abc",
@ -177,11 +145,11 @@ func TestEverything(t *testing.T) {
}
func TestOptions(t *testing.T) {
conn := testExec(t,
conn := testExecCommon(t, buildModuleQueryCommand,
"echo",
"1", "abc", `{"denom":"foo","amount":"1"}`,
"-u", "27", // shorthand
"--u64", // no opt default value
"--u64", "5", // no opt default value
)
lastReq := conn.lastRequest.(*testpb.EchoRequest)
assert.Equal(t, uint32(27), lastReq.U32) // shorthand got set
@ -190,26 +158,26 @@ func TestOptions(t *testing.T) {
}
func TestHelp(t *testing.T) {
conn := testExec(t, "-h")
conn := testExecCommon(t, buildModuleQueryCommand, "-h")
golden.Assert(t, conn.out.String(), "help-toplevel.golden")
conn = testExec(t, "echo", "-h")
conn = testExecCommon(t, buildModuleQueryCommand, "echo", "-h")
golden.Assert(t, conn.out.String(), "help-echo.golden")
conn = testExec(t, "deprecatedecho", "echo", "-h")
conn = testExecCommon(t, buildModuleQueryCommand, "deprecatedecho", "echo", "-h")
golden.Assert(t, conn.out.String(), "help-deprecated.golden")
conn = testExec(t, "skipecho", "-h")
conn = testExecCommon(t, buildModuleQueryCommand, "skipecho", "-h")
golden.Assert(t, conn.out.String(), "help-skip.golden")
}
func TestDeprecated(t *testing.T) {
conn := testExec(t, "echo",
conn := testExecCommon(t, buildModuleQueryCommand, "echo",
"1", "abc", `{}`,
"--deprecated-field", "foo")
assert.Assert(t, strings.Contains(conn.out.String(), "--deprecated-field has been deprecated"))
conn = testExec(t, "echo",
conn = testExecCommon(t, buildModuleQueryCommand, "echo",
"1", "abc", `{}`,
"-s", "foo")
assert.Assert(t, strings.Contains(conn.out.String(), "--shorthand-deprecated-field has been deprecated"))
@ -236,19 +204,26 @@ func TestBuildCustomQueryCommand(t *testing.T) {
func TestNotFoundErrors(t *testing.T) {
b := &Builder{}
buildModuleQueryCommand := func(moduleName string, cmdDescriptor *autocliv1.ServiceCommandDescriptor) (*cobra.Command, error) {
cmd := topLevelCmd("query", "Querying subcommands")
err := b.AddMsgServiceCommands(cmd, cmdDescriptor)
return cmd, err
}
// bad service
_, err := b.BuildModuleQueryCommand("test", &autocliv1.ServiceCommandDescriptor{Service: "foo"})
_, err := buildModuleQueryCommand("test", &autocliv1.ServiceCommandDescriptor{Service: "foo"})
assert.ErrorContains(t, err, "can't find service foo")
// bad method
_, err = b.BuildModuleQueryCommand("test", &autocliv1.ServiceCommandDescriptor{
_, err = buildModuleQueryCommand("test", &autocliv1.ServiceCommandDescriptor{
Service: testpb.Query_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{{RpcMethod: "bar"}},
})
assert.ErrorContains(t, err, "rpc method \"bar\" not found")
// bad positional field
_, err = b.BuildModuleQueryCommand("test", &autocliv1.ServiceCommandDescriptor{
_, err = buildModuleQueryCommand("test", &autocliv1.ServiceCommandDescriptor{
Service: testpb.Query_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{
@ -264,7 +239,7 @@ func TestNotFoundErrors(t *testing.T) {
assert.ErrorContains(t, err, "can't find field foo")
// bad flag field
_, err = b.BuildModuleQueryCommand("test", &autocliv1.ServiceCommandDescriptor{
_, err = buildModuleQueryCommand("test", &autocliv1.ServiceCommandDescriptor{
Service: testpb.Query_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{
@ -284,6 +259,7 @@ type testClientConn struct {
lastRequest interface{}
lastResponse interface{}
out *bytes.Buffer
errorOut *bytes.Buffer
}
func (t *testClientConn) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {

View File

@ -0,0 +1,38 @@
Command "send" is deprecated, dont use this
deprecated subcommand
Usage:
test deprecatedmsg send [flags]
Flags:
--a-bool
--a-coin cosmos.base.v1beta1.Coin (json)
--a-message testpb.AMessage (json)
--an-address bech32 account address key name
--an-enum Enum (unspecified | one | two | five | neg-three) (default unspecified)
--bools bools (default [])
--bz bytesBase64
--deprecated-field string
--duration duration
--durations duration (repeated)
--enums Enum (unspecified | one | two | five | neg-three) (repeated)
-h, --help help for send
--hidden-bool
--i32 int32
--i64 int
--page-count-total
--page-key bytesBase64
--page-limit uint
--page-offset uint
--page-reverse
--positional1 int32
--positional2 string
--positional3-varargs cosmos.base.v1beta1.Coin (json) (repeated)
--shorthand-deprecated-field string
--some-messages testpb.AMessage (json) (repeated)
--str string
--strings strings
--timestamp timestamp (RFC 3339)
--u32 uint32
--u64 uint
--uints uints (default [])

View File

@ -0,0 +1,40 @@
send msg the value provided by the user as a proto JSON object with populated with the provided fields and positional arguments
Usage:
test send [pos1] [pos2] [pos3...] [flags]
Aliases:
send, s
Examples:
send 1 abc {}
Flags:
--a-bool
--a-coin cosmos.base.v1beta1.Coin (json)
--a-message testpb.AMessage (json)
--an-address bech32 account address key name
--an-enum Enum (unspecified | one | two | five | neg-three) (default unspecified)
--bools bools (default [])
--bz bytesBase64
--deprecated-field string (DEPRECATED: don't use this)
--duration duration
--durations duration (repeated)
--enums Enum (unspecified | one | two | five | neg-three) (repeated)
-h, --help help for send
--i32 int32 some random int32
--i64 int
--page-count-total
--page-key bytesBase64
--page-limit uint
--page-offset uint
--page-reverse
-d, --shorthand-deprecated-field string (DEPRECATED: bad idea)
--some-messages testpb.AMessage (json) (repeated)
--str string
--strings strings
--timestamp timestamp (RFC 3339)
--u64 uint some random uint64
-u, --uint32 uint32 some random uint32
--uints uints (default [])
-v, --version version for send

View File

@ -34,7 +34,7 @@ Flags:
--str string
--strings strings
--timestamp timestamp (RFC 3339)
--u64 uint[=5] some random uint64
--u64 uint some random uint64
-u, --uint32 uint32 some random uint32
--uints uints (default [])
-v, --version version for echo

View File

@ -0,0 +1,17 @@
Transations commands for the test module
Usage:
test [flags]
test [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
deprecatedmsg Tx commands for the testpb.Msg service
help Help about any command
send send msg the value provided by the user
skipmsg Tx commands for the testpb.Msg service
Flags:
-h, --help help for test
Use "test [command] --help" for more information about a command.

View File

@ -1,8 +1,10 @@
package autocli
import (
"google.golang.org/protobuf/reflect/protoreflect"
"strings"
"cosmossdk.io/client/v2/internal/strcase"
"github.com/spf13/cobra"
)
@ -17,3 +19,20 @@ func findSubCommand(cmd *cobra.Command, subCmdName string) *cobra.Command {
}
return nil
}
// topLevelCmd creates a new top-level command with the provided name and
// description. The command will have DisableFlagParsing set to false and
// SuggestionsMinimumDistance set to 2.
func topLevelCmd(use, short string) *cobra.Command {
return &cobra.Command{
Use: use,
Short: short,
DisableFlagParsing: false,
SuggestionsMinimumDistance: 2,
RunE: validateCmd,
}
}
func protoNameToCliName(name protoreflect.Name) string {
return strcase.ToKebab(string(name))
}

View File

@ -0,0 +1,53 @@
syntax = "proto3";
package testpb;
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "cosmos_proto/cosmos.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "cosmos/base/v1beta1/coin.proto";
import "testpb/query.proto";
service Msg {
// Send a request and returns the request as a response.
rpc Send(MsgRequest) returns (MsgResponse);
}
message MsgRequest {
// u32 is an uint32
uint32 u32 = 1;
uint64 u64 = 2;
string str = 3;
bytes bz = 4;
google.protobuf.Timestamp timestamp = 5;
google.protobuf.Duration duration = 6;
int32 i32 = 7;
int64 i64 = 10;
bool a_bool = 15;
testpb.Enum an_enum = 16;
testpb.AMessage a_message = 17;
cosmos.base.v1beta1.Coin a_coin = 18;
string an_address = 19 [(cosmos_proto.scalar) = "cosmos.AddressString"];
cosmos.base.query.v1beta1.PageRequest page = 20;
repeated bool bools = 21;
repeated uint32 uints = 22;
repeated string strings = 23;
repeated testpb.Enum enums = 24;
repeated google.protobuf.Duration durations = 25;
repeated testpb.AMessage some_messages = 26;
int32 positional1 = 27;
string positional2 = 28;
repeated cosmos.base.v1beta1.Coin positional3_varargs = 29;
string deprecated_field = 30;
string shorthand_deprecated_field = 31;
bool hidden_bool = 32;
}
message MsgResponse {
MsgRequest request = 1;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,107 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc (unknown)
// source: testpb/msg.proto
package testpb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// MsgClient is the client API for Msg service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type MsgClient interface {
// Send a request and returns the request as a response.
Send(ctx context.Context, in *MsgRequest, opts ...grpc.CallOption) (*MsgResponse, error)
}
type msgClient struct {
cc grpc.ClientConnInterface
}
func NewMsgClient(cc grpc.ClientConnInterface) MsgClient {
return &msgClient{cc}
}
func (c *msgClient) Send(ctx context.Context, in *MsgRequest, opts ...grpc.CallOption) (*MsgResponse, error) {
out := new(MsgResponse)
err := c.cc.Invoke(ctx, "/testpb.Msg/Send", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// MsgServer is the server API for Msg service.
// All implementations must embed UnimplementedMsgServer
// for forward compatibility
type MsgServer interface {
// Send a request and returns the request as a response.
Send(context.Context, *MsgRequest) (*MsgResponse, error)
mustEmbedUnimplementedMsgServer()
}
// UnimplementedMsgServer must be embedded to have forward compatible implementations.
type UnimplementedMsgServer struct {
}
func (UnimplementedMsgServer) Send(context.Context, *MsgRequest) (*MsgResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Send not implemented")
}
func (UnimplementedMsgServer) mustEmbedUnimplementedMsgServer() {}
// UnsafeMsgServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to MsgServer will
// result in compilation errors.
type UnsafeMsgServer interface {
mustEmbedUnimplementedMsgServer()
}
func RegisterMsgServer(s grpc.ServiceRegistrar, srv MsgServer) {
s.RegisterService(&Msg_ServiceDesc, srv)
}
func _Msg_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MsgRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MsgServer).Send(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/testpb.Msg/Send",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MsgServer).Send(ctx, req.(*MsgRequest))
}
return interceptor(ctx, in, info, handler)
}
// Msg_ServiceDesc is the grpc.ServiceDesc for Msg service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Msg_ServiceDesc = grpc.ServiceDesc{
ServiceName: "testpb.Msg",
HandlerType: (*MsgServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Send",
Handler: _Msg_Send_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "testpb/msg.proto",
}