feat: structured screens for SIGN_MODE_TEXTUAL (#13434)
## Description Refs: #11970 Changes target of `SIGN_MODE_TEXTUAL` rendering to be a structured datatype instead of lines of ASCII text. This avoids the complexities of in-band, signaling and allows more capable signing devices not to be hindered by the limitations of those less capable. --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [X] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [X] added `!` to the type prefix if API or client breaking change - [X] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting)) - [X] provided a link to the relevant issue or specification - [X] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/main/docs/building-modules) - [X] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#testing) - [ ] added a changelog entry to `CHANGELOG.md` - [X] included comments for [documenting Go code](https://blog.golang.org/godoc) - [X] updated the relevant documentation or specification - [X] reviewed "Files changed" and left comments if necessary - [x] confirmed all CI checks have passed NOTE: changelog intentionally omitted - we'll add an entry when #11970 is complete. ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable)
This commit is contained in:
@@ -23,17 +23,15 @@ var intValues = []protoreflect.Value{
|
||||
func BenchmarkIntValueRendererFormat(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
ivr := new(intValueRenderer)
|
||||
buf := new(bytes.Buffer)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, value := range intValues {
|
||||
if err := ivr.Format(ctx, value, buf); err != nil {
|
||||
if _, err := ivr.Format(ctx, value); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +50,15 @@ var decimalValues = []protoreflect.Value{
|
||||
func BenchmarkDecimalValueRendererFormat(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
dvr := new(decValueRenderer)
|
||||
buf := new(bytes.Buffer)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, value := range intValues {
|
||||
if err := dvr.Format(ctx, value, buf); err != nil {
|
||||
if _, err := dvr.Format(ctx, value); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,16 +77,14 @@ var byteValues = []protoreflect.Value{
|
||||
func BenchmarkBytesValueRendererFormat(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
bvr := new(bytesValueRenderer)
|
||||
buf := new(bytes.Buffer)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, value := range byteValues {
|
||||
if err := bvr.Format(ctx, value, buf); err != nil {
|
||||
if _, err := bvr.Format(ctx, value); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package valuerenderer
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
@@ -19,16 +19,16 @@ type bytesValueRenderer struct{}
|
||||
|
||||
var _ ValueRenderer = bytesValueRenderer{}
|
||||
|
||||
func (vr bytesValueRenderer) Format(ctx context.Context, v protoreflect.Value, w io.Writer) error {
|
||||
_, err := io.WriteString(w, strings.ToUpper(hex.EncodeToString(v.Bytes())))
|
||||
return err
|
||||
func (vr bytesValueRenderer) Format(ctx context.Context, v protoreflect.Value) ([]Screen, error) {
|
||||
text := strings.ToUpper(hex.EncodeToString(v.Bytes()))
|
||||
return []Screen{{Text: text}}, nil
|
||||
}
|
||||
|
||||
func (vr bytesValueRenderer) Parse(_ context.Context, r io.Reader) (protoreflect.Value, error) {
|
||||
formatted, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return protoreflect.ValueOfBytes([]byte{}), err
|
||||
func (vr bytesValueRenderer) Parse(_ context.Context, screens []Screen) (protoreflect.Value, error) {
|
||||
if len(screens) != 1 {
|
||||
return protoreflect.ValueOfBytes([]byte{}), fmt.Errorf("expected single screen: %v", screens)
|
||||
}
|
||||
formatted := screens[0].Text
|
||||
|
||||
data, err := hex.DecodeString(string(formatted))
|
||||
if err != nil {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cosmossdk.io/tx/textual/valuerenderer"
|
||||
@@ -31,14 +30,13 @@ func TestBytesJsonTestCases(t *testing.T) {
|
||||
valrend, err := textual.GetValueRenderer(fieldDescriptorFromName("BYTES"))
|
||||
require.NoError(t, err)
|
||||
|
||||
b := new(strings.Builder)
|
||||
err = valrend.Format(context.Background(), protoreflect.ValueOfBytes(data), b)
|
||||
screens, err := valrend.Format(context.Background(), protoreflect.ValueOfBytes(data))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.hex, b.String())
|
||||
require.Equal(t, 1, len(screens))
|
||||
require.Equal(t, tc.hex, screens[0].Text)
|
||||
|
||||
// Round trip
|
||||
r := strings.NewReader(tc.hex)
|
||||
val, err := valrend.Parse(context.Background(), r)
|
||||
val, err := valrend.Parse(context.Background(), screens)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.base64, base64.StdEncoding.EncodeToString(val.Bytes()))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -33,13 +32,11 @@ func mockCoinMetadataQuerier(ctx context.Context, denom string) (*bankv1beta1.Me
|
||||
}
|
||||
|
||||
func TestMetadataQuerier(t *testing.T) {
|
||||
b := new(strings.Builder)
|
||||
|
||||
// Errors on nil metadata querier
|
||||
textual := valuerenderer.NewTextual(nil)
|
||||
vr, err := textual.GetValueRenderer(fieldDescriptorFromName("COIN"))
|
||||
require.NoError(t, err)
|
||||
err = vr.Format(context.Background(), protoreflect.ValueOf((&basev1beta1.Coin{}).ProtoReflect()), b)
|
||||
_, err = vr.Format(context.Background(), protoreflect.ValueOf((&basev1beta1.Coin{}).ProtoReflect()))
|
||||
require.Error(t, err)
|
||||
|
||||
// Errors if metadata querier returns an error
|
||||
@@ -49,9 +46,9 @@ func TestMetadataQuerier(t *testing.T) {
|
||||
})
|
||||
vr, err = textual.GetValueRenderer(fieldDescriptorFromName("COIN"))
|
||||
require.NoError(t, err)
|
||||
err = vr.Format(context.Background(), protoreflect.ValueOf((&basev1beta1.Coin{}).ProtoReflect()), b)
|
||||
_, err = vr.Format(context.Background(), protoreflect.ValueOf((&basev1beta1.Coin{}).ProtoReflect()))
|
||||
require.ErrorIs(t, err, expErr)
|
||||
err = vr.Format(context.Background(), protoreflect.ValueOf(NewGenericList([]*basev1beta1.Coin{{}})), b)
|
||||
_, err = vr.Format(context.Background(), protoreflect.ValueOf(NewGenericList([]*basev1beta1.Coin{{}})))
|
||||
require.ErrorIs(t, err, expErr)
|
||||
}
|
||||
|
||||
@@ -70,8 +67,7 @@ func TestCoinJsonTestcases(t *testing.T) {
|
||||
t.Run(tc.Text, func(t *testing.T) {
|
||||
if tc.Proto != nil {
|
||||
ctx := context.WithValue(context.Background(), mockCoinMetadataKey(tc.Proto.Denom), tc.Metadata)
|
||||
b := new(strings.Builder)
|
||||
err = vr.Format(ctx, protoreflect.ValueOf(tc.Proto.ProtoReflect()), b)
|
||||
screens, err := vr.Format(ctx, protoreflect.ValueOf(tc.Proto.ProtoReflect()))
|
||||
|
||||
if tc.Error {
|
||||
require.Error(t, err)
|
||||
@@ -79,7 +75,8 @@ func TestCoinJsonTestcases(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.Text, b.String())
|
||||
require.Equal(t, 1, len(screens))
|
||||
require.Equal(t, tc.Text, screens[0].Text)
|
||||
}
|
||||
|
||||
// TODO Add parsing tests
|
||||
|
||||
@@ -3,7 +3,6 @@ package valuerenderer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -28,9 +27,9 @@ type coinsValueRenderer struct {
|
||||
|
||||
var _ ValueRenderer = coinsValueRenderer{}
|
||||
|
||||
func (vr coinsValueRenderer) Format(ctx context.Context, v protoreflect.Value, w io.Writer) error {
|
||||
func (vr coinsValueRenderer) Format(ctx context.Context, v protoreflect.Value) ([]Screen, error) {
|
||||
if vr.coinMetadataQuerier == nil {
|
||||
return fmt.Errorf("expected non-nil coin metadata querier")
|
||||
return nil, fmt.Errorf("expected non-nil coin metadata querier")
|
||||
}
|
||||
|
||||
// Check whether we have a Coin or some Coins.
|
||||
@@ -45,17 +44,16 @@ func (vr coinsValueRenderer) Format(ctx context.Context, v protoreflect.Value, w
|
||||
coins[i] = coin
|
||||
metadatas[i], err = vr.coinMetadataQuerier(ctx, coin.Denom)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
formatted, err := formatCoins(coins, metadatas)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(formatted))
|
||||
return err
|
||||
return []Screen{{Text: formatted}}, nil
|
||||
}
|
||||
// If it's a single Coin:
|
||||
case protoreflect.Message:
|
||||
@@ -64,23 +62,22 @@ func (vr coinsValueRenderer) Format(ctx context.Context, v protoreflect.Value, w
|
||||
|
||||
metadata, err := vr.coinMetadataQuerier(ctx, coin.Denom)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
formatted, err := formatCoin(coin, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(formatted))
|
||||
return err
|
||||
return []Screen{{Text: formatted}}, nil
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("got invalid type %t for coins", v.Interface())
|
||||
return nil, fmt.Errorf("got invalid type %t for coins", v.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
func (vr coinsValueRenderer) Parse(_ context.Context, r io.Reader) (protoreflect.Value, error) {
|
||||
func (vr coinsValueRenderer) Parse(_ context.Context, screens []Screen) (protoreflect.Value, error) {
|
||||
// ref: https://github.com/cosmos/cosmos-sdk/issues/13153
|
||||
panic("implement me, see #13153")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1"
|
||||
@@ -35,9 +34,8 @@ func TestCoinsJsonTestcases(t *testing.T) {
|
||||
ctx = context.WithValue(ctx, mockCoinMetadataKey(coin.Denom), tc.Metadata[coin.Denom])
|
||||
}
|
||||
|
||||
b := new(strings.Builder)
|
||||
listValue := NewGenericList(tc.Proto)
|
||||
err = vr.Format(ctx, protoreflect.ValueOf(listValue), b)
|
||||
screens, err := vr.Format(ctx, protoreflect.ValueOf(listValue))
|
||||
|
||||
if tc.Error {
|
||||
require.Error(t, err)
|
||||
@@ -45,7 +43,8 @@ func TestCoinsJsonTestcases(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.Text, b.String())
|
||||
require.Equal(t, 1, len(screens))
|
||||
require.Equal(t, tc.Text, screens[0].Text)
|
||||
}
|
||||
|
||||
// TODO Add parsing tests
|
||||
|
||||
@@ -3,7 +3,6 @@ package valuerenderer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
@@ -21,17 +20,15 @@ type decValueRenderer struct{}
|
||||
|
||||
var _ ValueRenderer = decValueRenderer{}
|
||||
|
||||
func (vr decValueRenderer) Format(_ context.Context, v protoreflect.Value, w io.Writer) error {
|
||||
func (vr decValueRenderer) Format(_ context.Context, v protoreflect.Value) ([]Screen, error) {
|
||||
formatted, err := formatDecimal(v.String())
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = io.WriteString(w, formatted)
|
||||
return err
|
||||
return []Screen{{Text: formatted}}, nil
|
||||
}
|
||||
|
||||
func (vr decValueRenderer) Parse(_ context.Context, r io.Reader) (protoreflect.Value, error) {
|
||||
func (vr decValueRenderer) Parse(_ context.Context, screens []Screen) (protoreflect.Value, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package valuerenderer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -65,19 +64,19 @@ func formatSeconds(seconds int64, nanos int32) string {
|
||||
}
|
||||
|
||||
// Format implements the ValueRenderer interface.
|
||||
func (dr durationValueRenderer) Format(_ context.Context, v protoreflect.Value, w io.Writer) error {
|
||||
func (dr durationValueRenderer) Format(_ context.Context, v protoreflect.Value) ([]Screen, error) {
|
||||
// Reify the reflected message as a proto Duration
|
||||
msg := v.Message().Interface()
|
||||
duration, ok := msg.(*dpb.Duration)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected Duration, got %T", msg)
|
||||
return nil, fmt.Errorf("expected Duration, got %T", msg)
|
||||
}
|
||||
|
||||
// Bypass use of time.Duration, as the range is more limited than that of dpb.Duration.
|
||||
// (Too bad the companies that produced both technologies didn't coordinate better!)
|
||||
|
||||
if err := duration.CheckValid(); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
negative := false
|
||||
@@ -115,26 +114,25 @@ func (dr durationValueRenderer) Format(_ context.Context, v protoreflect.Value,
|
||||
s = "-" + s
|
||||
}
|
||||
|
||||
_, err := w.Write([]byte(s))
|
||||
return err
|
||||
return []Screen{{Text: s}}, nil
|
||||
}
|
||||
|
||||
var durRegexp = regexp.MustCompile(`^(-)?(?:([0-9]+) days?)?(?:, )?(?:([0-9]+) hours?)?(?:, )?(?:([0-9]+) minutes?)?(?:, )?(?:([0-9]+)(?:\.([0-9]+))? seconds?)?$`)
|
||||
|
||||
// Parse implements the ValueRenderer interface.
|
||||
func (dr durationValueRenderer) Parse(_ context.Context, r io.Reader) (protoreflect.Value, error) {
|
||||
bz, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return protoreflect.Value{}, err
|
||||
func (dr durationValueRenderer) Parse(_ context.Context, screens []Screen) (protoreflect.Value, error) {
|
||||
if len(screens) != 1 {
|
||||
return protoreflect.Value{}, fmt.Errorf("expected single screen: %v", screens)
|
||||
}
|
||||
|
||||
parts := durRegexp.FindStringSubmatch(string(bz))
|
||||
parts := durRegexp.FindStringSubmatch(screens[0].Text)
|
||||
if parts == nil {
|
||||
return protoreflect.Value{}, fmt.Errorf("bad duration format: %s", string(bz))
|
||||
return protoreflect.Value{}, fmt.Errorf("bad duration format: %s", screens[0].Text)
|
||||
}
|
||||
|
||||
negative := parts[1] != ""
|
||||
var days, hours, minutes, seconds, nanos int64
|
||||
var err error
|
||||
|
||||
if parts[2] != "" {
|
||||
days, err = strconv.ParseInt(parts[2], 10, 64)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cosmossdk.io/tx/textual/valuerenderer"
|
||||
@@ -34,19 +33,19 @@ func TestDurationJSON(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
rend := valuerenderer.NewDurationValueRenderer()
|
||||
|
||||
var screens []valuerenderer.Screen
|
||||
if tc.Proto != nil {
|
||||
wr := new(strings.Builder)
|
||||
err = rend.Format(context.Background(), protoreflect.ValueOf(tc.Proto.ProtoReflect()), wr)
|
||||
screens, err = rend.Format(context.Background(), protoreflect.ValueOf(tc.Proto.ProtoReflect()))
|
||||
if tc.Error {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.Text, wr.String())
|
||||
require.Equal(t, 1, len(screens))
|
||||
require.Equal(t, tc.Text, screens[0].Text)
|
||||
}
|
||||
|
||||
rd := strings.NewReader(tc.Text)
|
||||
val, err := rend.Parse(context.Background(), rd)
|
||||
val, err := rend.Parse(context.Background(), screens)
|
||||
if tc.Error {
|
||||
require.Error(t, err)
|
||||
return
|
||||
|
||||
@@ -3,7 +3,6 @@ package valuerenderer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
@@ -19,17 +18,15 @@ type intValueRenderer struct{}
|
||||
|
||||
var _ ValueRenderer = intValueRenderer{}
|
||||
|
||||
func (vr intValueRenderer) Format(_ context.Context, v protoreflect.Value, w io.Writer) error {
|
||||
func (vr intValueRenderer) Format(_ context.Context, v protoreflect.Value) ([]Screen, error) {
|
||||
formatted, err := formatInteger(v.String())
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = io.WriteString(w, formatted)
|
||||
return err
|
||||
return []Screen{{Text: formatted}}, nil
|
||||
}
|
||||
|
||||
func (vr intValueRenderer) Parse(_ context.Context, r io.Reader) (protoreflect.Value, error) {
|
||||
func (vr intValueRenderer) Parse(_ context.Context, screens []Screen) (protoreflect.Value, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package valuerenderer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
@@ -20,12 +19,12 @@ func NewTimestampValueRenderer() ValueRenderer {
|
||||
}
|
||||
|
||||
// Format implements the ValueRenderer interface.
|
||||
func (vr timestampValueRenderer) Format(_ context.Context, v protoreflect.Value, w io.Writer) error {
|
||||
func (vr timestampValueRenderer) Format(_ context.Context, v protoreflect.Value) ([]Screen, error) {
|
||||
// Reify the reflected message as a proto Timestamp
|
||||
msg := v.Message().Interface()
|
||||
timestamp, ok := msg.(*tspb.Timestamp)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected Timestamp, got %T", msg)
|
||||
return nil, fmt.Errorf("expected Timestamp, got %T", msg)
|
||||
}
|
||||
|
||||
// Convert proto timestamp to a Go Time.
|
||||
@@ -33,18 +32,16 @@ func (vr timestampValueRenderer) Format(_ context.Context, v protoreflect.Value,
|
||||
|
||||
// Format the Go Time as RFC 3339.
|
||||
s := t.Format(time.RFC3339Nano)
|
||||
w.Write([]byte(s))
|
||||
return nil
|
||||
return []Screen{{Text: s}}, nil
|
||||
}
|
||||
|
||||
// Parse implements the ValueRenderer interface.
|
||||
func (vr timestampValueRenderer) Parse(_ context.Context, r io.Reader) (protoreflect.Value, error) {
|
||||
func (vr timestampValueRenderer) Parse(_ context.Context, screens []Screen) (protoreflect.Value, error) {
|
||||
// Parse the RFC 3339 input as a Go Time.
|
||||
bz, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return protoreflect.Value{}, err
|
||||
if len(screens) != 1 {
|
||||
return protoreflect.Value{}, fmt.Errorf("expected single screen: %v", screens)
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339Nano, string(bz))
|
||||
t, err := time.Parse(time.RFC3339Nano, screens[0].Text)
|
||||
if err != nil {
|
||||
return protoreflect.Value{}, err
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -45,19 +43,19 @@ func TestTimestampJsonTestcases(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
rend := valuerenderer.NewTimestampValueRenderer()
|
||||
|
||||
var screens []valuerenderer.Screen
|
||||
if tc.Proto != nil {
|
||||
wr := new(strings.Builder)
|
||||
err = rend.Format(context.Background(), protoreflect.ValueOf(tc.Proto.ProtoReflect()), wr)
|
||||
screens, err = rend.Format(context.Background(), protoreflect.ValueOf(tc.Proto.ProtoReflect()))
|
||||
if tc.Error {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.Text, wr.String())
|
||||
require.Equal(t, 1, len(screens))
|
||||
require.Equal(t, tc.Text, screens[0].Text)
|
||||
}
|
||||
|
||||
rd := strings.NewReader(tc.Text)
|
||||
val, err := rend.Parse(context.Background(), rd)
|
||||
val, err := rend.Parse(context.Background(), screens)
|
||||
if tc.Error {
|
||||
require.Error(t, err)
|
||||
return
|
||||
@@ -73,21 +71,6 @@ func TestTimestampJsonTestcases(t *testing.T) {
|
||||
|
||||
func TestTimestampBadFormat(t *testing.T) {
|
||||
rend := valuerenderer.NewTimestampValueRenderer()
|
||||
wr := new(strings.Builder)
|
||||
err := rend.Format(context.Background(), protoreflect.ValueOf(dur.New(time.Hour).ProtoReflect()), wr)
|
||||
_, err := rend.Format(context.Background(), protoreflect.ValueOf(dur.New(time.Hour).ProtoReflect()))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
type badReader struct{}
|
||||
|
||||
var _ io.Reader = badReader{}
|
||||
|
||||
func (br badReader) Read(p []byte) (int, error) {
|
||||
return 0, fmt.Errorf("reader error")
|
||||
}
|
||||
|
||||
func TestTimestampBadParse_reader(t *testing.T) {
|
||||
rend := valuerenderer.NewTimestampValueRenderer()
|
||||
_, err := rend.Parse(context.Background(), badReader{})
|
||||
require.ErrorContains(t, err, "reader error")
|
||||
}
|
||||
|
||||
@@ -2,11 +2,24 @@ package valuerenderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
// Screen is the abstract unit of Textual rendering.
|
||||
type Screen struct {
|
||||
// Text is the text to display - a sequence of Unicode code points.
|
||||
Text string
|
||||
|
||||
// Indent is the indentation level of the screen.
|
||||
// Zero indicates top-level. Should be less than 16.
|
||||
Indent int
|
||||
|
||||
// Expert indicates that the screen should only be displayed
|
||||
// via an opt-in from the user.
|
||||
Expert bool
|
||||
}
|
||||
|
||||
// ValueRenderer defines an interface to produce formatted output for all
|
||||
// protobuf types as well as parse a string into those protobuf types.
|
||||
//
|
||||
@@ -15,6 +28,9 @@ import (
|
||||
// here, so that optionally more value renderers could be built, for example, a
|
||||
// separate one for a different language.
|
||||
type ValueRenderer interface {
|
||||
Format(context.Context, protoreflect.Value, io.Writer) error
|
||||
Parse(context.Context, io.Reader) (protoreflect.Value, error)
|
||||
// Format should render the value to a text plus annotation.
|
||||
Format(context.Context, protoreflect.Value) ([]Screen, error)
|
||||
|
||||
// Parse should be the inverse of Format.
|
||||
Parse(context.Context, []Screen) (protoreflect.Value, error)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -33,11 +32,10 @@ func TestFormatInteger(t *testing.T) {
|
||||
if err == nil {
|
||||
r, err := textual.GetValueRenderer(fieldDescriptorFromName("UINT64"))
|
||||
require.NoError(t, err)
|
||||
b := new(strings.Builder)
|
||||
err = r.Format(context.Background(), protoreflect.ValueOf(i), b)
|
||||
screens, err := r.Format(context.Background(), protoreflect.ValueOf(i))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc[1], b.String())
|
||||
require.Equal(t, 1, len(screens))
|
||||
require.Equal(t, tc[1], screens[0].Text)
|
||||
}
|
||||
|
||||
// Parse test case strings as protobuf uint32
|
||||
@@ -45,11 +43,10 @@ func TestFormatInteger(t *testing.T) {
|
||||
if err == nil {
|
||||
r, err := textual.GetValueRenderer(fieldDescriptorFromName("UINT32"))
|
||||
require.NoError(t, err)
|
||||
b := new(strings.Builder)
|
||||
err = r.Format(context.Background(), protoreflect.ValueOf(i), b)
|
||||
screens, err := r.Format(context.Background(), protoreflect.ValueOf(i))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc[1], b.String())
|
||||
require.Equal(t, 1, len(screens))
|
||||
require.Equal(t, tc[1], screens[0].Text)
|
||||
}
|
||||
|
||||
// Parse test case strings as sdk.Ints
|
||||
@@ -57,11 +54,10 @@ func TestFormatInteger(t *testing.T) {
|
||||
if ok {
|
||||
r, err := textual.GetValueRenderer(fieldDescriptorFromName("SDKINT"))
|
||||
require.NoError(t, err)
|
||||
b := new(strings.Builder)
|
||||
err = r.Format(context.Background(), protoreflect.ValueOf(tc[0]), b)
|
||||
screens, err := r.Format(context.Background(), protoreflect.ValueOf(tc[0]))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc[1], b.String())
|
||||
require.Equal(t, 1, len(screens))
|
||||
require.Equal(t, tc[1], screens[0].Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,11 +77,10 @@ func TestFormatDecimal(t *testing.T) {
|
||||
t.Run(tc[0], func(t *testing.T) {
|
||||
r, err := textual.GetValueRenderer(fieldDescriptorFromName("SDKDEC"))
|
||||
require.NoError(t, err)
|
||||
b := new(strings.Builder)
|
||||
err = r.Format(context.Background(), protoreflect.ValueOf(tc[0]), b)
|
||||
screens, err := r.Format(context.Background(), protoreflect.ValueOf(tc[0]))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc[1], b.String())
|
||||
require.Equal(t, 1, len(screens))
|
||||
require.Equal(t, tc[1], screens[0].Text)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user