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:
parent
b585d17e72
commit
fe5b0ff4a5
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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
10
tx/textual/internal/testdata/enum.json
vendored
Normal 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" }
|
||||
]
|
||||
@ -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
55
tx/textual/valuerenderer/enum.go
Normal file
55
tx/textual/valuerenderer/enum.go
Normal 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())
|
||||
}
|
||||
69
tx/textual/valuerenderer/enum_test.go
Normal file
69
tx/textual/valuerenderer/enum_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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},
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user