diff --git a/client/v2/autocli/flag/builder.go b/client/v2/autocli/flag/builder.go index 5bd3d67bb234..a11304281214 100644 --- a/client/v2/autocli/flag/builder.go +++ b/client/v2/autocli/flag/builder.go @@ -165,12 +165,13 @@ func (b *Builder) addMessageFlags(ctx *context.Context, flagSet *pflag.FlagSet, s := strings.Split(arg.ProtoField, ".") if len(s) == 1 { - err := b.addFieldBindingToArgs(ctx, messageBinder, protoreflect.Name(arg.ProtoField), fields) + f, err := b.addFieldBindingToArgs(ctx, messageBinder, protoreflect.Name(arg.ProtoField), fields) if err != nil { return nil, err } + messageBinder.positionalArgs = append(messageBinder.positionalArgs, f) } else { - err := b.addFlattenFieldBindingToArgs(ctx, s, messageType, messageBinder) + err := b.addFlattenFieldBindingToArgs(ctx, arg.ProtoField, s, messageType, messageBinder) if err != nil { return nil, err } @@ -271,22 +272,33 @@ func (b *Builder) addMessageFlags(ctx *context.Context, flagSet *pflag.FlagSet, // It takes a slice of field names representing the path to the target field, where each element is a field name // in the nested message structure. For example, ["foo", "bar", "baz"] would bind the "baz" field inside the "bar" // message which is inside the "foo" message. -func (b *Builder) addFlattenFieldBindingToArgs(ctx *context.Context, s []string, msg protoreflect.MessageType, messageBinder *MessageBinder) error { +func (b *Builder) addFlattenFieldBindingToArgs(ctx *context.Context, path string, s []string, msg protoreflect.MessageType, messageBinder *MessageBinder) error { fields := msg.Descriptor().Fields() if len(s) == 1 { - return b.addFieldBindingToArgs(ctx, messageBinder, protoreflect.Name(s[0]), fields) + f, err := b.addFieldBindingToArgs(ctx, messageBinder, protoreflect.Name(s[0]), fields) + if err != nil { + return err + } + f.path = path + messageBinder.positionalArgs = append(messageBinder.positionalArgs, f) + return nil } - - innerMsg := msg.New().Get(fields.ByName(protoreflect.Name(s[0]))).Message().Type() - return b.addFlattenFieldBindingToArgs(ctx, s[1:], innerMsg, messageBinder) + fd := fields.ByName(protoreflect.Name(s[0])) + var innerMsg protoreflect.MessageType + if fd.IsList() { + innerMsg = msg.New().Get(fd).List().NewElement().Message().Type() + } else { + innerMsg = msg.New().Get(fd).Message().Type() + } + return b.addFlattenFieldBindingToArgs(ctx, path, s[1:], innerMsg, messageBinder) } // addFieldBindingToArgs adds a fieldBinding for a positional argument to the message binder. // The fieldBinding is appended to the positional arguments list in the message binder. -func (b *Builder) addFieldBindingToArgs(ctx *context.Context, messageBinder *MessageBinder, name protoreflect.Name, fields protoreflect.FieldDescriptors) error { +func (b *Builder) addFieldBindingToArgs(ctx *context.Context, messageBinder *MessageBinder, name protoreflect.Name, fields protoreflect.FieldDescriptors) (fieldBinding, error) { field := fields.ByName(name) if field == nil { - return fmt.Errorf("can't find field %s", name) // TODO: it will improve error if msg.FullName() was included.` + return fieldBinding{}, fmt.Errorf("can't find field %s", name) // TODO: it will improve error if msg.FullName() was included.` } _, hasValue, err := b.addFieldFlag( @@ -297,15 +309,13 @@ func (b *Builder) addFieldBindingToArgs(ctx *context.Context, messageBinder *Mes namingOptions{}, ) if err != nil { - return err + return fieldBinding{}, err } - messageBinder.positionalArgs = append(messageBinder.positionalArgs, fieldBinding{ + return fieldBinding{ field: field, hasValue: hasValue, - }) - - return nil + }, nil } // bindPageRequest create a flag for pagination diff --git a/client/v2/autocli/flag/messager_binder.go b/client/v2/autocli/flag/messager_binder.go index 8140a50b3fa2..08a270e792a3 100644 --- a/client/v2/autocli/flag/messager_binder.go +++ b/client/v2/autocli/flag/messager_binder.go @@ -74,7 +74,8 @@ func (m MessageBinder) Bind(msg protoreflect.Message, positionalArgs []string) e return err } } else { - if err := m.bindNestedField(msg, arg); err != nil { + s := strings.Split(arg.path, ".") + if err := m.bindNestedField(msg, arg, s); err != nil { return err } } @@ -92,20 +93,35 @@ func (m MessageBinder) Bind(msg protoreflect.Message, positionalArgs []string) e // bindNestedField binds a field value to a nested message field. It handles cases where the field // belongs to a nested message type by recursively traversing the message structure. -func (m *MessageBinder) bindNestedField(msg protoreflect.Message, arg fieldBinding) error { - name := protoreflect.Name(strings.ToLower(string(arg.field.Parent().Name()))) - innerMsgValue := msg.Get(msg.Descriptor().Fields().ByName(name)) - if !innerMsgValue.Message().IsValid() { - msg.Set(msg.Descriptor().Fields().ByName(name), protoreflect.ValueOfMessage(innerMsgValue.Message().New())) +func (m *MessageBinder) bindNestedField(msg protoreflect.Message, arg fieldBinding, path []string) error { + if len(path) == 1 { + return arg.bind(msg) } - innerMsg := msg.Get(msg.Descriptor().Fields().ByName(name)).Message() - argField := innerMsg.Descriptor().Fields().ByName(arg.field.Name()) - if argField.Kind() == protoreflect.MessageKind { - return m.bindNestedField(innerMsg, arg) + name := protoreflect.Name(path[0]) + fd := msg.Descriptor().Fields().ByName(name) + if fd == nil { + return fmt.Errorf("field %q not found", path[0]) } - return arg.bind(innerMsg) + var innerMsg protoreflect.Message + if fd.IsList() { + if msg.Get(fd).List().Len() == 0 { + l := msg.Mutable(fd).List() + elem := l.NewElement().Message().New() + l.Append(protoreflect.ValueOfMessage(elem)) + msg.Set(msg.Descriptor().Fields().ByName(name), protoreflect.ValueOfList(l)) + } + innerMsg = msg.Get(fd).List().Get(0).Message() + } else { + innerMsgValue := msg.Get(fd) + if !innerMsgValue.Message().IsValid() { + msg.Set(msg.Descriptor().Fields().ByName(name), protoreflect.ValueOfMessage(innerMsgValue.Message().New())) + } + innerMsg = msg.Get(msg.Descriptor().Fields().ByName(name)).Message() + } + + return m.bindNestedField(innerMsg, arg, path[1:]) } // Get calls BuildMessage and wraps the result in a protoreflect.Value. @@ -117,6 +133,7 @@ func (m MessageBinder) Get(protoreflect.Value) (protoreflect.Value, error) { type fieldBinding struct { hasValue HasValue field protoreflect.FieldDescriptor + path string } func (f fieldBinding) bind(msg protoreflect.Message) error { diff --git a/client/v2/autocli/msg_test.go b/client/v2/autocli/msg_test.go index a86fc8ebcca7..49b3feeaeee1 100644 --- a/client/v2/autocli/msg_test.go +++ b/client/v2/autocli/msg_test.go @@ -16,6 +16,7 @@ import ( autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" "cosmossdk.io/client/v2/internal/testpb" + _ "cosmossdk.io/client/v2/internal/testpb" "github.com/cosmos/cosmos-sdk/client" ) @@ -128,6 +129,33 @@ func TestMsg(t *testing.T) { assertNormalizedJSONEqual(t, out.Bytes(), goldenLoad(t, "msg-output.golden")) } +func TestMsgWithFlattenFields(t *testing.T) { + fixture := initFixture(t) + + out, err := runCmd(fixture, buildCustomModuleMsgCommand(&autocliv1.ServiceCommandDescriptor{ + Service: bankv1beta1.Msg_ServiceDesc.ServiceName, + RpcCommandOptions: []*autocliv1.RpcCommandOptions{ + { + RpcMethod: "UpdateParams", + PositionalArgs: []*autocliv1.PositionalArgDescriptor{ + {ProtoField: "authority"}, + {ProtoField: "params.send_enabled.denom"}, + {ProtoField: "params.send_enabled.enabled"}, + {ProtoField: "params.default_send_enabled"}, + }, + }, + }, + EnhanceCustomCommand: true, + }), "update-params", + "cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk", "stake", "true", "true", + "--generate-only", + "--output", "json", + "--chain-id", fixture.chainID, + ) + assert.NilError(t, err) + assertNormalizedJSONEqual(t, out.Bytes(), goldenLoad(t, "flatten-output.golden")) +} + func goldenLoad(t *testing.T, filename string) []byte { t.Helper() content, err := os.ReadFile(filepath.Join("testdata", filename)) diff --git a/client/v2/autocli/testdata/flatten-output.golden b/client/v2/autocli/testdata/flatten-output.golden new file mode 100644 index 000000000000..59b3c668d4c2 --- /dev/null +++ b/client/v2/autocli/testdata/flatten-output.golden @@ -0,0 +1 @@ +{"body":{"messages":[{"@type":"/cosmos.bank.v1beta1.MsgUpdateParams","authority":"cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk","params":{"send_enabled":[{"denom":"stake","enabled":true}],"default_send_enabled":true}}],"memo":"","timeout_height":"0","unordered":false,"timeout_timestamp":"1970-01-01T00:00:00Z","extension_options":[],"non_critical_extension_options":[]},"auth_info":{"signer_infos":[],"fee":{"amount":[],"gas_limit":"200000","payer":"","granter":""},"tip":null},"signatures":[]}