Support for repeated (#13604)
This commit is contained in:
parent
a176eb2646
commit
2cf92caf45
@ -62,20 +62,23 @@ Value Renderers describe how values of different Protobuf types should be encode
|
||||
- A repeated type has the following template:
|
||||
|
||||
```
|
||||
<message_name> has <int> <field_name>
|
||||
<field_name> (<int>/<int>): <value rendered 1st line>
|
||||
<field_name>: <int> <field_kind>
|
||||
<field_name> (<index>/<int>): <value rendered 1st line>
|
||||
<optional value rendered in the next lines>
|
||||
<field_name> (<int>/<int>): <value rendered 1st line>
|
||||
<field_name> (<index>/<int>): <value rendered 1st line>
|
||||
<optional value rendered in the next lines>
|
||||
End of <field_name>.
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
- `message_name` is the name of the Protobuf message which holds the `repeated` field,
|
||||
- `int` is the length of the array,
|
||||
- `field_name` is the Protobuf field name of the repeated field,
|
||||
- add an optional `s` at the end if `<int> > 1` and the `field_name` doesn't already end with `s`.
|
||||
- `field_name` is the Protobuf field name of the repeated field
|
||||
- `field_kind`:
|
||||
- if the type of the repeated field is a message, `field_kind` is the message name
|
||||
- if the type of the repeated field is an enum, `field_kind` is the enum name
|
||||
- in any other case, `field_kind` is the protobuf primitive type (e.g. "string" or "bytes")
|
||||
- `int` is the length of the array
|
||||
- `index` is one based index of the repeated field
|
||||
|
||||
#### Examples
|
||||
|
||||
@ -150,7 +153,7 @@ we get the following encoding for the `Vote` message:
|
||||
```
|
||||
Vote object
|
||||
> Proposal id: 4
|
||||
> Vote: cosmos1abc...def
|
||||
> Voter: cosmos1abc...def
|
||||
> Options: 2 WeightedVoteOptions
|
||||
> Options (1/2): WeightedVoteOption object
|
||||
>> Option: VOTE_OPTION_YES
|
||||
|
||||
161
tx/textual/internal/testdata/repeated.json
vendored
Normal file
161
tx/textual/internal/testdata/repeated.json
vendored
Normal file
@ -0,0 +1,161 @@
|
||||
[
|
||||
{
|
||||
"proto": {},
|
||||
"parses": true,
|
||||
"screens": [
|
||||
{"text": "Qux object", "indent": 0}
|
||||
]
|
||||
},
|
||||
{
|
||||
"proto": {
|
||||
"messages": [
|
||||
{
|
||||
"full_name": "thing one",
|
||||
"nickname": ":thing two"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parses": true,
|
||||
"screens": [
|
||||
{"text": "Qux object", "indent": 0},
|
||||
{"text": "Messages: 1 Foo", "indent": 1},
|
||||
{"text": "Messages (1/1): Foo object", "indent": 2},
|
||||
{"text": "Full name: thing one", "indent": 3},
|
||||
{"text": "Nickname: :thing two", "indent": 3},
|
||||
{"text": "End of Messages", "indent": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"proto": {
|
||||
"messages": [
|
||||
{
|
||||
"full_name": "thing one",
|
||||
"nickname": "thing two"
|
||||
},
|
||||
{
|
||||
"full_name": "thing three",
|
||||
"nickname": "thing four"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parses": true,
|
||||
"screens": [
|
||||
{"text": "Qux object", "indent": 0},
|
||||
{"text": "Messages: 2 Foo", "indent": 1},
|
||||
{"text": "Messages (1/2): Foo object", "indent": 2},
|
||||
{"text": "Full name: thing one", "indent": 3},
|
||||
{"text": "Nickname: thing two", "indent": 3},
|
||||
{"text": "Messages (2/2): Foo object", "indent": 2},
|
||||
{"text": "Full name: thing three", "indent": 3},
|
||||
{"text": "Nickname: thing four", "indent": 3},
|
||||
{"text": "End of Messages", "indent": 1}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"proto": {
|
||||
"string_messages": [
|
||||
"/OmniFlix.onft.v1beta1.MsgTransferONFT"
|
||||
]
|
||||
},
|
||||
"parses": true,
|
||||
"screens": [
|
||||
{"text": "Qux object", "indent": 0},
|
||||
{"text": "String messages: 1 String", "indent": 1},
|
||||
{"text": "String messages (1/1): /OmniFlix.onft.v1beta1.MsgTransferONFT", "indent": 2},
|
||||
{"text": "End of String messages", "indent": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"proto": {
|
||||
"string_messages": [
|
||||
"/OmniFlix.onft.v1beta1.MsgTransferONFT",
|
||||
"/OmniFlix.onft.v1beta1.MsgBurnONFT",
|
||||
"/OmniFlix.marketplace.v1beta1.MsgListNFT"
|
||||
]
|
||||
},
|
||||
"parses": true,
|
||||
"screens": [
|
||||
{"text": "Qux object", "indent": 0},
|
||||
{"text": "String messages: 3 String", "indent": 1},
|
||||
{"text": "String messages (1/3): /OmniFlix.onft.v1beta1.MsgTransferONFT", "indent": 2},
|
||||
{"text": "String messages (2/3): /OmniFlix.onft.v1beta1.MsgBurnONFT", "indent": 2},
|
||||
{"text": "String messages (3/3): /OmniFlix.marketplace.v1beta1.MsgListNFT", "indent": 2},
|
||||
{"text": "End of String messages", "indent": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"proto": {
|
||||
"vote" : {
|
||||
"proposal_id": 4,
|
||||
"voter": "cosmos1abc...def",
|
||||
"options": [
|
||||
{
|
||||
"option": "Yes",
|
||||
"weight": "0.7"
|
||||
},
|
||||
{
|
||||
"option": "No",
|
||||
"weight": "0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"parses": false,
|
||||
"screens": [
|
||||
{"text": "Qux object", "indent": 0},
|
||||
{"text": "Vote: Ballot object", "indent": 1},
|
||||
{"text": "Proposal id: 4", "indent": 2},
|
||||
{"text": "Voter: cosmos1abc...def", "indent": 2},
|
||||
{"text": "Options: 2 WeightedBallotOption", "indent": 2},
|
||||
{"text": "Options (1/2): WeightedBallotOption object", "indent": 3},
|
||||
{"text": "Option: Yes", "indent": 4},
|
||||
{"text": "Weight: 0.7", "indent": 4},
|
||||
{"text": "Options (2/2): WeightedBallotOption object", "indent": 3},
|
||||
{"text": "Option: No", "indent": 4},
|
||||
{"text": "Weight: 0.3", "indent": 4},
|
||||
{"text": "End of Options", "indent": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"proto": {
|
||||
"price": [
|
||||
{ "amount": "1", "denom": "ucosm" },
|
||||
{ "amount": "3", "denom": "ustake" }
|
||||
]
|
||||
},
|
||||
"parses": false,
|
||||
"screens": [
|
||||
{"text": "Qux object", "indent": 0},
|
||||
{"text": "Price: 1 ucosm, 3 ustake", "indent": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"proto": {
|
||||
"expirations": [
|
||||
{"seconds": 0, "nanos": 1},
|
||||
{"seconds": 0, "nanos": 10},
|
||||
{"seconds": 0, "nanos": 100},
|
||||
{"seconds": 0, "nanos": 1000},
|
||||
{"seconds": 0, "nanos": 10000},
|
||||
{"seconds": 0, "nanos": 100000},
|
||||
{"seconds": 0, "nanos": 1000000},
|
||||
{"seconds": 0, "nanos": 10000000}
|
||||
]
|
||||
},
|
||||
"parses": true,
|
||||
"screens": [
|
||||
{"text": "Qux object", "indent": 0},
|
||||
{"text": "Expirations: 8 Timestamp", "indent": 1},
|
||||
{"text": "Expirations (1/8): 1970-01-01T00:00:00.000000001Z", "indent": 2},
|
||||
{"text": "Expirations (2/8): 1970-01-01T00:00:00.00000001Z", "indent": 2},
|
||||
{"text": "Expirations (3/8): 1970-01-01T00:00:00.0000001Z", "indent": 2},
|
||||
{"text": "Expirations (4/8): 1970-01-01T00:00:00.000001Z", "indent": 2},
|
||||
{"text": "Expirations (5/8): 1970-01-01T00:00:00.00001Z", "indent": 2},
|
||||
{"text": "Expirations (6/8): 1970-01-01T00:00:00.0001Z", "indent": 2},
|
||||
{"text": "Expirations (7/8): 1970-01-01T00:00:00.001Z", "indent": 2},
|
||||
{"text": "Expirations (8/8): 1970-01-01T00:00:00.01Z", "indent": 2},
|
||||
{"text": "End of Expirations", "indent": 1}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -1,57 +1,57 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "cosmossdk.io/tx/textual/internal/testpb";
|
||||
|
||||
import "cosmos/base/v1beta1/coin.proto";
|
||||
import "cosmos_proto/cosmos.proto";
|
||||
import "google/protobuf/any.proto";
|
||||
import "google/protobuf/descriptor.proto";
|
||||
import "google/protobuf/duration.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "cosmos_proto/cosmos.proto";
|
||||
import "cosmos/base/v1beta1/coin.proto";
|
||||
|
||||
option go_package = "cosmossdk.io/tx/textual/internal/testpb";
|
||||
|
||||
// A is used for testing value renderers.
|
||||
message A {
|
||||
// Fields that are parseable by SIGN_MODE_TEXTUAL.
|
||||
uint32 UINT32 = 1;
|
||||
uint64 UINT64 = 2;
|
||||
int32 INT32 = 3;
|
||||
int64 INT64 = 4;
|
||||
string SDKINT = 5 [(cosmos_proto.scalar) = "cosmos.Int"];
|
||||
string SDKDEC = 6 [(cosmos_proto.scalar) = "cosmos.Dec"];
|
||||
cosmos.base.v1beta1.Coin COIN = 7;
|
||||
repeated cosmos.base.v1beta1.Coin COINS = 8;
|
||||
bytes BYTES = 9;
|
||||
google.protobuf.Timestamp TIMESTAMP = 10;
|
||||
google.protobuf.Duration DURATION = 11;
|
||||
ExternalEnum ENUM = 12;
|
||||
google.protobuf.Any ANY = 13;
|
||||
uint32 UINT32 = 1;
|
||||
uint64 UINT64 = 2;
|
||||
int32 INT32 = 3;
|
||||
int64 INT64 = 4;
|
||||
string SDKINT = 5 [(cosmos_proto.scalar) = "cosmos.Int"];
|
||||
string SDKDEC = 6 [(cosmos_proto.scalar) = "cosmos.Dec"];
|
||||
cosmos.base.v1beta1.Coin COIN = 7;
|
||||
repeated cosmos.base.v1beta1.Coin COINS = 8;
|
||||
bytes BYTES = 9;
|
||||
google.protobuf.Timestamp TIMESTAMP = 10;
|
||||
google.protobuf.Duration DURATION = 11;
|
||||
ExternalEnum ENUM = 12;
|
||||
google.protobuf.Any ANY = 13;
|
||||
|
||||
// Fields that are not handled by SIGN_MODE_TEXTUAL.
|
||||
sint32 SINT32 = 101;
|
||||
sint64 SINT64 = 102;
|
||||
sfixed32 SFIXED32 = 105;
|
||||
fixed32 FIXED32 = 106;
|
||||
float FLOAT = 107;
|
||||
sfixed64 SFIXED64 = 108;
|
||||
fixed64 FIXED64 = 109;
|
||||
double DOUBLE = 110;
|
||||
map<string, A> MAP = 111;
|
||||
sint32 SINT32 = 101;
|
||||
sint64 SINT64 = 102;
|
||||
sfixed32 SFIXED32 = 105;
|
||||
fixed32 FIXED32 = 106;
|
||||
float FLOAT = 107;
|
||||
sfixed64 SFIXED64 = 108;
|
||||
fixed64 FIXED64 = 109;
|
||||
double DOUBLE = 110;
|
||||
map<string, A> MAP = 111;
|
||||
}
|
||||
|
||||
// Foo is a sample message type used for testing message rendering.
|
||||
message Foo {
|
||||
string full_name = 1;
|
||||
string nickname = 2;
|
||||
google.protobuf.Timestamp mtime = 3;
|
||||
Foo left = 4;
|
||||
Foo right = 5;
|
||||
Bar bar = 8; // skip some field numbers
|
||||
string full_name = 1;
|
||||
string nickname = 2;
|
||||
google.protobuf.Timestamp mtime = 3;
|
||||
Foo left = 4;
|
||||
Foo right = 5;
|
||||
Bar bar = 8; // skip some field numbers
|
||||
}
|
||||
|
||||
// Bar is a sample message type used for testing message rendering.
|
||||
message Bar {
|
||||
string bar_id = 1;
|
||||
bytes data = 2;
|
||||
string bar_id = 1;
|
||||
bytes data = 2;
|
||||
google.protobuf.Any payload = 3;
|
||||
}
|
||||
|
||||
@ -71,13 +71,37 @@ message Baz {
|
||||
ExternalEnum ee = 1;
|
||||
Internal_Enum ie = 2;
|
||||
BallotOption option = 3;
|
||||
|
||||
}
|
||||
|
||||
enum BallotOption {
|
||||
BALLOT_OPTION_UNSPECIFIED = 0;
|
||||
BALLOT_OPTION_YES = 1;
|
||||
BALLOT_OPTION_ABSTAIN = 2;
|
||||
BALLOT_OPTION_NO = 3;
|
||||
BALLOT_OPTION_UNSPECIFIED = 0;
|
||||
BALLOT_OPTION_YES = 1;
|
||||
BALLOT_OPTION_ABSTAIN = 2;
|
||||
BALLOT_OPTION_NO = 3;
|
||||
BALLOT_OPTION_NO_WITH_VETO = 4;
|
||||
}
|
||||
|
||||
// Qux is a sample message type used for testing repeated rendering.
|
||||
message Qux {
|
||||
repeated Foo messages = 1;
|
||||
repeated string string_messages = 2;
|
||||
Ballot vote = 3;
|
||||
repeated cosmos.base.v1beta1.Coin price = 4;
|
||||
repeated google.protobuf.Timestamp expirations = 5;
|
||||
}
|
||||
|
||||
message WeightedBallotOption {
|
||||
// TODO: Enumeration rendering
|
||||
// BallotOption option = 1;
|
||||
string option = 1;
|
||||
string weight = 2 [(cosmos_proto.scalar) = "cosmos.Dec"];
|
||||
}
|
||||
|
||||
message Ballot {
|
||||
uint64 proposal_id = 1;
|
||||
// TODO: cosmos.AddressString rendering
|
||||
// string voter = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
|
||||
string voter = 2;
|
||||
reserved 3;
|
||||
repeated WeightedBallotOption options = 4;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -80,3 +80,12 @@ func (vr coinsValueRenderer) Parse(_ context.Context, screens []Screen) (protore
|
||||
// ref: https://github.com/cosmos/cosmos-sdk/issues/13153
|
||||
panic("implement me, see #13153")
|
||||
}
|
||||
|
||||
func (vr coinsValueRenderer) FormatRepeated(ctx context.Context, v protoreflect.Value) ([]Screen, error) {
|
||||
return vr.Format(ctx, v)
|
||||
}
|
||||
|
||||
func (vr coinsValueRenderer) ParseRepeated(ctx context.Context, screens []Screen, l protoreflect.List) error {
|
||||
// ref: https://github.com/cosmos/cosmos-sdk/issues/13153
|
||||
panic("implement me, see #13153")
|
||||
}
|
||||
|
||||
@ -2,8 +2,11 @@ package valuerenderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
@ -51,7 +54,20 @@ func (mr *messageValueRenderer) Format(ctx context.Context, v protoreflect.Value
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscreens, err := vr.Format(ctx, v.Message().Get(fd))
|
||||
subscreens := make([]Screen, 0)
|
||||
if fd.IsList() {
|
||||
if r, ok := vr.(RepeatedValueRenderer); ok {
|
||||
// If the field is a list, and handles its own repeated rendering
|
||||
subscreens, err = r.FormatRepeated(ctx, v.Message().Get(fd))
|
||||
} else {
|
||||
// If the field is a list, we need to format each element of the list
|
||||
subscreens, err = mr.formatRepeated(ctx, v.Message().Get(fd), fd)
|
||||
}
|
||||
} else {
|
||||
// If the field is not list, we need to format the field
|
||||
subscreens, err = vr.Format(ctx, v.Message().Get(fd))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -60,7 +76,7 @@ func (mr *messageValueRenderer) Format(ctx context.Context, v protoreflect.Value
|
||||
}
|
||||
|
||||
headerScreen := Screen{
|
||||
Text: fmt.Sprintf("%s: %s", formatFieldName(string(fd.Name())), subscreens[0].Text),
|
||||
Text: fmt.Sprintf("%s: %s", toSentenceCase(string(fd.Name())), subscreens[0].Text),
|
||||
Indent: subscreens[0].Indent + 1,
|
||||
Expert: subscreens[0].Expert,
|
||||
}
|
||||
@ -79,9 +95,74 @@ func (mr *messageValueRenderer) Format(ctx context.Context, v protoreflect.Value
|
||||
return screens, nil
|
||||
}
|
||||
|
||||
// formatFieldName formats a field name in sentence case, as specified in:
|
||||
func (mr *messageValueRenderer) formatRepeated(ctx context.Context, v protoreflect.Value, fd protoreflect.FieldDescriptor) ([]Screen, error) {
|
||||
vr, err := mr.tr.GetFieldValueRenderer(fd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := v.List()
|
||||
if l == nil {
|
||||
return nil, fmt.Errorf("got non-List value %T", l)
|
||||
}
|
||||
|
||||
screens := make([]Screen, 1)
|
||||
// <field_name>: <int> <field_kind>
|
||||
screens[0].Text = fmt.Sprintf("%d %s", l.Len(), toSentenceCase(getKind(fd)))
|
||||
|
||||
for i := 0; i < l.Len(); i++ {
|
||||
subscreens, err := vr.Format(ctx, l.Get(i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(subscreens) == 0 {
|
||||
return nil, errors.New("empty rendering")
|
||||
}
|
||||
|
||||
headerScreen := Screen{
|
||||
// <field_name> (<int>/<int>): <value rendered 1st line>
|
||||
Text: fmt.Sprintf("%s (%d/%d): %s", toSentenceCase(string(fd.Name())), i+1, l.Len(), subscreens[0].Text),
|
||||
Indent: subscreens[0].Indent + 1,
|
||||
Expert: subscreens[0].Expert,
|
||||
}
|
||||
screens = append(screens, headerScreen)
|
||||
|
||||
// <optional value rendered in the next lines>
|
||||
for i := 1; i < len(subscreens); i++ {
|
||||
extraScreen := Screen{
|
||||
Text: subscreens[i].Text,
|
||||
Indent: subscreens[i].Indent + 1,
|
||||
Expert: subscreens[i].Expert,
|
||||
}
|
||||
screens = append(screens, extraScreen)
|
||||
}
|
||||
}
|
||||
|
||||
// End of <field_name>
|
||||
terminalScreen := Screen{
|
||||
Text: fmt.Sprintf("End of %s", toSentenceCase(string(fd.Name()))),
|
||||
}
|
||||
screens = append(screens, terminalScreen)
|
||||
return screens, nil
|
||||
}
|
||||
|
||||
// getKind returns the field kind: if the field is a protobuf
|
||||
// message, then we return the message's name. Or else, we
|
||||
// return the protobuf kind.
|
||||
func getKind(fd protoreflect.FieldDescriptor) string {
|
||||
if fd.Kind() == protoreflect.MessageKind {
|
||||
return string(fd.Message().Name())
|
||||
} else if fd.Kind() == protoreflect.EnumKind {
|
||||
return string(fd.Enum().Name())
|
||||
}
|
||||
|
||||
return fd.Kind().String()
|
||||
}
|
||||
|
||||
// toSentenceCase formats a field name in sentence case, as specified in:
|
||||
// https://github.com/cosmos/cosmos-sdk/blob/b6f867d0b674d62e56b27aa4d00f5b6042ebac9e/docs/architecture/adr-050-sign-mode-textual-annex1.md?plain=1#L110
|
||||
func formatFieldName(name string) string {
|
||||
func toSentenceCase(name string) string {
|
||||
if len(name) == 0 {
|
||||
return name
|
||||
}
|
||||
@ -92,7 +173,7 @@ var nilValue = protoreflect.Value{}
|
||||
|
||||
func (mr *messageValueRenderer) Parse(ctx context.Context, screens []Screen) (protoreflect.Value, error) {
|
||||
if len(screens) == 0 {
|
||||
return nilValue, fmt.Errorf("expect at least one screen")
|
||||
return nilValue, errors.New("expect at least one screen")
|
||||
}
|
||||
|
||||
wantHeader := fmt.Sprintf("%s object", mr.msgDesc.Name())
|
||||
@ -125,7 +206,7 @@ func (mr *messageValueRenderer) Parse(ctx context.Context, screens []Screen) (pr
|
||||
return nilValue, fmt.Errorf("bad message indentation: want 1, got %d", screens[idx].Indent)
|
||||
}
|
||||
|
||||
prefix := formatFieldName(string(fd.Name())) + ": "
|
||||
prefix := toSentenceCase(string(fd.Name())) + ": "
|
||||
if !strings.HasPrefix(screens[idx].Text, prefix) {
|
||||
// we must have skipped this fd because of a default value
|
||||
continue
|
||||
@ -146,16 +227,90 @@ func (mr *messageValueRenderer) Parse(ctx context.Context, screens []Screen) (pr
|
||||
idx++
|
||||
}
|
||||
|
||||
val, err := vr.Parse(ctx, subscreens)
|
||||
if err != nil {
|
||||
return nilValue, err
|
||||
var val protoreflect.Value
|
||||
// We have a repeated field...
|
||||
if fd.IsList() {
|
||||
nf := msg.NewField(fd)
|
||||
if r, ok := vr.(RepeatedValueRenderer); ok {
|
||||
err = r.ParseRepeated(ctx, subscreens, nf.List())
|
||||
} else {
|
||||
err = mr.parseRepeated(ctx, subscreens, nf.List(), vr)
|
||||
}
|
||||
if err != nil {
|
||||
return nilValue, err
|
||||
}
|
||||
msg.Set(fd, nf)
|
||||
|
||||
//Skip List Terminator
|
||||
idx++
|
||||
} else {
|
||||
val, err = vr.Parse(ctx, subscreens)
|
||||
if err != nil {
|
||||
return nilValue, err
|
||||
}
|
||||
msg.Set(fd, val)
|
||||
}
|
||||
msg.Set(fd, val)
|
||||
}
|
||||
|
||||
if idx > len(screens) {
|
||||
return nilValue, fmt.Errorf("leftover screens")
|
||||
return nilValue, errors.New("leftover screens")
|
||||
}
|
||||
|
||||
return protoreflect.ValueOfMessage(msg), nil
|
||||
}
|
||||
|
||||
func (mr *messageValueRenderer) parseRepeated(ctx context.Context, screens []Screen, l protoreflect.List, vr ValueRenderer) error {
|
||||
|
||||
// <int> <field_kind>
|
||||
headerRegex := *regexp.MustCompile(`(\d+) .+`)
|
||||
res := headerRegex.FindAllStringSubmatch(screens[0].Text, -1)
|
||||
|
||||
if res == nil {
|
||||
return errors.New("failed to match <int> <field_kind>")
|
||||
}
|
||||
|
||||
lengthStr := res[0][1]
|
||||
length, err := strconv.Atoi(lengthStr)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("malformed length: %q with error: %w", lengthStr, err)
|
||||
}
|
||||
|
||||
idx := 1
|
||||
elementIndex := 1
|
||||
|
||||
// <field_name> (<int>/<int>): <value rendered 1st line>
|
||||
elementRegex := regexp.MustCompile(`(.+) \(\d+\/\d+\): (.+)`)
|
||||
elementRes := elementRegex.FindAllStringSubmatch(screens[idx].Text, -1)
|
||||
if elementRes == nil {
|
||||
return errors.New("element malformed")
|
||||
}
|
||||
fieldName := elementRes[0][1]
|
||||
|
||||
for idx < len(screens) && elementIndex <= length {
|
||||
prefix := fmt.Sprintf("%s (%d/%d): ", fieldName, elementIndex, length)
|
||||
// Make a new screen without the prefix
|
||||
subscreens := make([]Screen, 1)
|
||||
subscreens[0] = screens[idx]
|
||||
subscreens[0].Text = strings.TrimPrefix(screens[idx].Text, prefix)
|
||||
subscreens[0].Indent--
|
||||
idx++
|
||||
|
||||
// Gather nested screens
|
||||
for idx < len(screens) && screens[idx].Indent > 1 {
|
||||
scr := screens[idx]
|
||||
scr.Indent--
|
||||
subscreens = append(subscreens, scr)
|
||||
idx++
|
||||
}
|
||||
|
||||
val, err := vr.Parse(ctx, subscreens)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
elementIndex++
|
||||
l.Append(val)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -26,6 +26,9 @@ func (x *genericList[T]) Len() int {
|
||||
}
|
||||
|
||||
func (x *genericList[T]) Get(i int) protoreflect.Value {
|
||||
if x.Len() == 0 {
|
||||
return protoreflect.Value{}
|
||||
}
|
||||
return protoreflect.ValueOfMessage((*x.list)[i].ProtoReflect())
|
||||
}
|
||||
|
||||
|
||||
58
tx/textual/valuerenderer/repeated_test.go
Normal file
58
tx/textual/valuerenderer/repeated_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package valuerenderer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"cosmossdk.io/tx/textual/valuerenderer"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cosmossdk.io/tx/textual/internal/testpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
type repeatedJsonTest struct {
|
||||
Proto *testpb.Qux
|
||||
Screens []valuerenderer.Screen
|
||||
// TODO Remove once we finished all primitive value renderers parsing
|
||||
// https://github.com/cosmos/cosmos-sdk/pull/13696
|
||||
// https://github.com/cosmos/cosmos-sdk/pull/13853
|
||||
Parses bool
|
||||
}
|
||||
|
||||
func TestRepeatedJsonTestcases(t *testing.T) {
|
||||
raw, err := os.ReadFile("../internal/testdata/repeated.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
var testcases []repeatedJsonTest
|
||||
err = json.Unmarshal(raw, &testcases)
|
||||
require.NoError(t, err)
|
||||
|
||||
tr := valuerenderer.NewTextual(mockCoinMetadataQuerier)
|
||||
for i, tc := range testcases {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
// Create a context.Context containing all coins metadata, to simulate
|
||||
// that they are in state.
|
||||
ctx := context.Background()
|
||||
rend := valuerenderer.NewMessageValueRenderer(&tr, (&testpb.Qux{}).ProtoReflect().Descriptor())
|
||||
require.NoError(t, err)
|
||||
|
||||
screens, err := rend.Format(ctx, protoreflect.ValueOf(tc.Proto.ProtoReflect()))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.Screens, screens)
|
||||
|
||||
if tc.Parses {
|
||||
val, err := rend.Parse(context.Background(), screens)
|
||||
require.NoError(t, err)
|
||||
msg := val.Message().Interface()
|
||||
require.IsType(t, &testpb.Qux{}, msg)
|
||||
baz := msg.(*testpb.Qux)
|
||||
require.True(t, proto.Equal(baz, tc.Proto))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -34,3 +34,15 @@ type ValueRenderer interface {
|
||||
// Parse should be the inverse of Format.
|
||||
Parse(context.Context, []Screen) (protoreflect.Value, error)
|
||||
}
|
||||
|
||||
// RepeatedValueRenderer defines an interface to produce formatted output for
|
||||
// protobuf message fields that are repeated.
|
||||
type RepeatedValueRenderer interface {
|
||||
ValueRenderer
|
||||
|
||||
// FormatRepeated should render the value to a text plus annotation.
|
||||
FormatRepeated(context.Context, protoreflect.Value) ([]Screen, error)
|
||||
|
||||
// ParseRepeated should be the inverse of Format. The list will be populated with the repeated values.
|
||||
ParseRepeated(context.Context, []Screen, protoreflect.List) error
|
||||
}
|
||||
|
||||
@ -50,8 +50,8 @@ func NewTextual(q CoinMetadataQueryFn) Textual {
|
||||
func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueRenderer, error) {
|
||||
switch {
|
||||
// Scalars, such as sdk.Int and sdk.Dec encoded as strings.
|
||||
case fd.Kind() == protoreflect.StringKind && proto.GetExtension(fd.Options(), cosmos_proto.E_Scalar) != "":
|
||||
{
|
||||
case fd.Kind() == protoreflect.StringKind:
|
||||
if proto.GetExtension(fd.Options(), cosmos_proto.E_Scalar) != "" {
|
||||
scalar, ok := proto.GetExtension(fd.Options(), cosmos_proto.E_Scalar).(string)
|
||||
if !ok || scalar == "" {
|
||||
return nil, fmt.Errorf("got extension option %s of type %T", scalar, scalar)
|
||||
@ -64,6 +64,8 @@ func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueR
|
||||
|
||||
return vr, nil
|
||||
}
|
||||
return NewStringValueRenderer(), nil
|
||||
|
||||
case fd.Kind() == protoreflect.BytesKind:
|
||||
return NewBytesValueRenderer(), nil
|
||||
|
||||
@ -72,9 +74,7 @@ func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueR
|
||||
fd.Kind() == protoreflect.Uint64Kind ||
|
||||
fd.Kind() == protoreflect.Int32Kind ||
|
||||
fd.Kind() == protoreflect.Int64Kind:
|
||||
{
|
||||
return NewIntValueRenderer(), nil
|
||||
}
|
||||
return NewIntValueRenderer(), nil
|
||||
|
||||
case fd.Kind() == protoreflect.StringKind:
|
||||
return stringValueRenderer{}, nil
|
||||
@ -93,10 +93,6 @@ func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueR
|
||||
if fd.IsMap() {
|
||||
return nil, fmt.Errorf("value renderers cannot format value of type map")
|
||||
}
|
||||
if fd.IsList() {
|
||||
// This will be implemented in https://github.com/cosmos/cosmos-sdk/issues/12714
|
||||
return nil, fmt.Errorf("repeated field renderer not yet implemented")
|
||||
}
|
||||
return NewMessageValueRenderer(r, md), nil
|
||||
|
||||
default:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user