feat(textual): Add Enum value renderer (#13853)

* feat(textual): Add Enum value renderer

* parse

* Update ADR

* Fix test

* Update tx/textual/internal/testpb/1.proto

Co-authored-by: Likhita Polavarapu <78951027+likhita-809@users.noreply.github.com>

Co-authored-by: Likhita Polavarapu <78951027+likhita-809@users.noreply.github.com>
This commit is contained in:
Amaury 2022-11-23 12:01:33 +01:00 committed by GitHub
parent b585d17e72
commit fe5b0ff4a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1013 additions and 102 deletions

View File

@ -153,23 +153,17 @@ Vote object
> Vote: cosmos1abc...def
> Options: 2 WeightedVoteOptions
> Options (1/2): WeightedVoteOption object
>> Option: Yes
>> Option: VOTE_OPTION_YES
>> Weight: 0.7
> Options (2/2): WeightedVoteOption object
>> Option: No
>> Option: VOTE_OPTION_NO
>> Weight: 0.3
> End of Options
```
### Enums
- String case convention: snake case to sentence case
- Allow optional annotation for textual name (TBD)
- Algorithm:
- convert enum name (`VoteOption`) to snake_case (`VOTE_OPTION`)
- truncate that prefix + `_` from the enum name if it exists (`VOTE_OPTION_` gets stripped from `VOTE_OPTION_YES` -> `YES`)
- convert rest to sentence case: `YES` -> `Yes`
- in summary: `VOTE_OPTION_YES` -> `Yes`
- Show the enum variant name as string.
#### Examples

View File

@ -402,7 +402,7 @@ Tip: 200 ibc/CDC4587874B85BEA4FCEC3CEA5A1195139799A1FEE711A07D972537E18FDA39D
*This transaction has 1 other signer:
*Signer (1/2):
*Public Key: iQ...==
*Sign mode: Direct Aux
*Sign mode: SIGN_MODE_DIRECT_AUX
*Sequence: 42
*End of other signers
*Hash of raw bytes: <hex_string>
@ -549,7 +549,7 @@ Fee: 0.002 atom
*This transaction has 1 other signer:
*Signer (2/2):
*Public Key: iR...==
*Sign mode: Direct
*Sign mode: SIGN_MODE_DIRECT
*Sequence: 42
*End of other signers
*Hash of raw bytes: <hex_string>

View File

@ -7,6 +7,7 @@ require (
cosmossdk.io/core v0.3.2
cosmossdk.io/math v1.0.0-beta.3
github.com/cosmos/cosmos-proto v1.0.0-alpha8
github.com/google/go-cmp v0.5.9
github.com/stretchr/testify v1.8.1
google.golang.org/protobuf v1.28.1
)

View File

@ -12,6 +12,7 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

10
tx/textual/internal/testdata/enum.json vendored Normal file
View File

@ -0,0 +1,10 @@
[
{ "proto": { "ee": 0 }, "text": "One" },
{ "proto": { "ee": 1 }, "text": "Two" },
{ "proto": { "ee": 127 }, "text": "EXTERNAL_ENUM_THREE" },
{ "proto": { "ie": 0 }, "text": "Baz.Four" },
{ "proto": { "ie": 1 }, "text": "Baz.Five" },
{ "proto": { "option": 0 }, "text": "BALLOT_OPTION_UNSPECIFIED" },
{ "proto": { "option": 1 }, "text": "BALLOT_OPTION_YES" },
{ "proto": { "option": 4 }, "text": "BALLOT_OPTION_NO_WITH_VETO" }
]

View File

@ -8,11 +8,6 @@ import "google/protobuf/timestamp.proto";
import "cosmos_proto/cosmos.proto";
import "cosmos/base/v1beta1/coin.proto";
enum Enumeration {
One = 0;
Two = 1;
}
// A is used for testing value renderers.
message A {
// Fields that are parseable by SIGN_MODE_TEXTUAL.
@ -27,6 +22,7 @@ message A {
bytes BYTES = 9;
google.protobuf.Timestamp TIMESTAMP = 10;
google.protobuf.Duration DURATION = 11;
ExternalEnum ENUM = 12;
// Fields that are not handled by SIGN_MODE_TEXTUAL.
sint32 SINT32 = 101;
@ -55,3 +51,30 @@ message Bar {
string bar_id = 1;
bytes data = 2;
}
enum ExternalEnum {
One = 0;
Two = 1;
EXTERNAL_ENUM_THREE = 127;
}
// Baz is a sample message type used for testing enum rendering.
message Baz {
enum Internal_Enum {
Four = 0;
Five = 1;
}
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_NO_WITH_VETO = 4;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
package valuerenderer
import (
"context"
"fmt"
"google.golang.org/protobuf/reflect/protoreflect"
)
type enumValueRenderer struct {
ed protoreflect.EnumDescriptor
}
func NewEnumValueRenderer(fd protoreflect.FieldDescriptor) ValueRenderer {
ed := fd.Enum()
if ed == nil {
panic(fmt.Errorf("expected enum field, got %s", fd.Kind()))
}
return enumValueRenderer{ed: ed}
}
var _ ValueRenderer = (*enumValueRenderer)(nil)
func (er enumValueRenderer) Format(_ context.Context, v protoreflect.Value) ([]Screen, error) {
// Get the full name of the enum variant.
evd := er.ed.Values().ByNumber(v.Enum())
if evd == nil {
return nil, fmt.Errorf("cannot get enum %s variant of number %d", er.ed.FullName(), v.Enum())
}
return []Screen{{Text: string(evd.FullName())}}, nil
}
func (er enumValueRenderer) Parse(_ context.Context, screens []Screen) (protoreflect.Value, error) {
if len(screens) != 1 {
return nilValue, fmt.Errorf("expected single screen: %v", screens)
}
formatted := screens[0].Text
// Loop through all enum variants until we find the one matching the
// formatted screen's one.
values := er.ed.Values()
for i := 0; i < values.Len(); i++ {
evd := values.Get(i)
if string(evd.FullName()) == formatted {
return protoreflect.ValueOfEnum(evd.Number()), nil
}
}
return nilValue, fmt.Errorf("cannot parse %s as enum on field %s", formatted, er.ed.FullName())
}

View File

@ -0,0 +1,69 @@
package valuerenderer_test
import (
"context"
"encoding/json"
"os"
"strings"
"testing"
"cosmossdk.io/tx/textual/internal/testpb"
"cosmossdk.io/tx/textual/valuerenderer"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/testing/protocmp"
)
type enumTest struct {
Proto json.RawMessage
Text string
}
func TestEnumJsonTestcases(t *testing.T) {
var testcases []enumTest
raw, err := os.ReadFile("../internal/testdata/enum.json")
require.NoError(t, err)
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
textual := valuerenderer.NewTextual(nil)
for _, tc := range testcases {
t.Run(tc.Text, func(t *testing.T) {
m := &testpb.Baz{}
err := protojson.Unmarshal(tc.Proto, m)
require.NoError(t, err)
fd := getFd(tc.Proto, m)
valrend, err := textual.GetValueRenderer(fd)
require.NoError(t, err)
val := m.ProtoReflect().Get(fd)
screens, err := valrend.Format(context.Background(), val)
require.NoError(t, err)
require.Equal(t, 1, len(screens))
require.Equal(t, tc.Text, screens[0].Text)
// Round trip
parsedVal, err := valrend.Parse(context.Background(), screens)
require.NoError(t, err)
diff := cmp.Diff(val.Interface(), parsedVal.Interface(), protocmp.Transform())
require.Empty(t, diff)
})
}
}
// getFd returns the field descriptor on Baz whose value is set. Since golang
// treats empty and default values as the same, we actually parse the protojson
// encoded string to retrieve which field is set.
func getFd(proto json.RawMessage, m *testpb.Baz) protoreflect.FieldDescriptor {
if strings.Contains(string(proto), `"ee"`) {
return m.ProtoReflect().Descriptor().Fields().ByNumber(1)
} else if strings.Contains(string(proto), `"ie"`) {
return m.ProtoReflect().Descriptor().Fields().ByNumber(2)
} else {
return m.ProtoReflect().Descriptor().Fields().ByNumber(3)
}
}

View File

@ -78,6 +78,9 @@ func (r Textual) GetValueRenderer(fd protoreflect.FieldDescriptor) (ValueRendere
case fd.Kind() == protoreflect.StringKind:
return stringValueRenderer{}, nil
case fd.Kind() == protoreflect.EnumKind:
return NewEnumValueRenderer(fd), nil
case fd.Kind() == protoreflect.MessageKind:
md := fd.Message()
fullName := md.FullName()

View File

@ -26,6 +26,7 @@ func TestDispatcher(t *testing.T) {
{"DURATION", false, valuerenderer.NewDurationValueRenderer()},
{"COIN", false, valuerenderer.NewCoinsValueRenderer(nil)},
{"COINS", false, valuerenderer.NewCoinsValueRenderer(nil)},
{"ENUM", false, valuerenderer.NewEnumValueRenderer(fieldDescriptorFromName("ENUM"))},
{"FLOAT", true, nil},
}