cosmos-sdk/x/tx/textual/message.go
2023-02-24 15:02:56 +00:00

317 lines
8.4 KiB
Go

package textual
import (
"context"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
type messageValueRenderer struct {
tr *SignModeHandler
msgDesc protoreflect.MessageDescriptor
fds []protoreflect.FieldDescriptor
}
func NewMessageValueRenderer(t *SignModeHandler, msgDesc protoreflect.MessageDescriptor) ValueRenderer {
fields := msgDesc.Fields()
fds := make([]protoreflect.FieldDescriptor, 0, fields.Len())
for i := 0; i < fields.Len(); i++ {
fds = append(fds, fields.Get(i))
}
sort.Slice(fds, func(i, j int) bool { return fds[i].Number() < fds[j].Number() })
return &messageValueRenderer{tr: t, msgDesc: msgDesc, fds: fds}
}
func (mr *messageValueRenderer) header() string {
return fmt.Sprintf("%s object", mr.msgDesc.Name())
}
func (mr *messageValueRenderer) Format(ctx context.Context, v protoreflect.Value) ([]Screen, error) {
fullName := v.Message().Descriptor().FullName()
wantFullName := mr.msgDesc.FullName()
if fullName != wantFullName {
return nil, fmt.Errorf(`bad message type: want "%s", got "%s"`, wantFullName, fullName)
}
screens := make([]Screen, 1)
screens[0].Content = mr.header()
for _, fd := range mr.fds {
if !v.Message().Has(fd) {
// Skip default values.
continue
}
vr, err := mr.tr.GetFieldValueRenderer(fd)
if err != nil {
return nil, err
}
var subscreens []Screen
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
}
if len(subscreens) == 0 {
return nil, fmt.Errorf("empty rendering for field %s", fd.Name())
}
headerScreen := Screen{
Title: toSentenceCase(string(fd.Name())),
Content: subscreens[0].Content,
Indent: subscreens[0].Indent + 1,
Expert: subscreens[0].Expert,
}
screens = append(screens, headerScreen)
for i := 1; i < len(subscreens); i++ {
extraScreen := Screen{
Title: subscreens[i].Title,
Content: subscreens[i].Content,
Indent: subscreens[i].Indent + 1,
Expert: subscreens[i].Expert,
}
screens = append(screens, extraScreen)
}
}
return screens, nil
}
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].Content = 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>)
Title: fmt.Sprintf("%s (%d/%d)", toSentenceCase(string(fd.Name())), i+1, l.Len()),
// <value rendered 1st line>
Content: subscreens[0].Content,
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{
Title: subscreens[i].Title,
Content: subscreens[i].Content,
Indent: subscreens[i].Indent + 1,
Expert: subscreens[i].Expert,
}
screens = append(screens, extraScreen)
}
}
// End of <field_name>
terminalScreen := Screen{
Content: 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 toSentenceCase(name string) string {
if len(name) == 0 {
return name
}
return strings.ToTitle(name[0:1]) + strings.ReplaceAll(name[1:], "_", " ")
}
var nilValue = protoreflect.Value{}
func (mr *messageValueRenderer) Parse(ctx context.Context, screens []Screen) (protoreflect.Value, error) {
if len(screens) == 0 {
return nilValue, errors.New("expect at least one screen")
}
wantHeader := fmt.Sprintf("%s object", mr.msgDesc.Name())
if screens[0].Content != wantHeader {
return nilValue, fmt.Errorf(`bad header: want "%s", got "%s"`, wantHeader, screens[0].Title)
}
if screens[0].Indent != 0 {
return nilValue, fmt.Errorf("bad message indentation: want 0, got %d", screens[0].Indent)
}
msgType, err := protoregistry.GlobalTypes.FindMessageByName(mr.msgDesc.FullName())
if err != nil {
return nilValue, err
}
msg := msgType.New()
idx := 1
for _, fd := range mr.fds {
if idx >= len(screens) {
// remaining fields are default
break
}
vr, err := mr.tr.GetFieldValueRenderer(fd)
if err != nil {
return nilValue, err
}
if screens[idx].Indent != 1 {
return nilValue, fmt.Errorf("bad message indentation: want 1, got %d", screens[idx].Indent)
}
expectedTitle := toSentenceCase(string(fd.Name()))
if screens[idx].Title != expectedTitle {
// we must have skipped this fd because of a default value
continue
}
// Make a new screen without the prefix
subscreens := make([]Screen, 1)
subscreens[0] = screens[idx]
subscreens[0].Title = screens[idx].Title
subscreens[0].Content = screens[idx].Content
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++
}
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)
// Skip List Terminator
idx++
}
if err != nil {
return nilValue, err
}
msg.Set(fd, nf)
} else {
val, err = vr.Parse(ctx, subscreens)
if err != nil {
return nilValue, err
}
msg.Set(fd, val)
}
}
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].Content, -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].Title, -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].Content = strings.TrimPrefix(screens[idx].Content, 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
}