feat: duration renderer for SIGN_MODE_TEXTUAL (#13312)
* feat: value renderer for durations * feat: dispatch for duration ValueRenderer * docs: update duration rendering spec * feat: error on malformed proto, review feedback, more tests * fix: silence spurious int overflow warnings * fix: supress spurious gosec warning on safe operation
This commit is contained in:
parent
6d03ddede4
commit
5f01f6f5e5
|
@ -218,12 +218,37 @@ as `2006-01-02T15:04:05.7Z`.
|
|||
The timestamp with 1136214245 seconds and zero nanoseconds is rendered
|
||||
as `2006-01-02T15:04:05Z`.
|
||||
|
||||
### `google.protobuf.Duration` (TODO)
|
||||
### `google.protobuf.Duration`
|
||||
|
||||
- rendered in terms of weeks, days, hours, minutes and seconds as these time units can be measured independently of any calendar and duration values are in seconds (so months and years can't be used precisely)
|
||||
- total seconds values included at the end so users have both pieces of information
|
||||
- Ex:
|
||||
- `1483530 seconds` -> `2 weeks, 3 days, 4 hours, 5 minutes, 30 seconds (1483530 seconds total)`
|
||||
The duration proto expresses a raw number of seconds and nanoseconds.
|
||||
This will be rendered as longer time units of days, hours, and minutes,
|
||||
plus any remaining seconds, in that order.
|
||||
Leading and trailing zero-quantity units will be omitted, but all
|
||||
units in between nonzero units will be shown, e.g. ` 3 days, 0 hours, 0 minutes, 5 seconds`.
|
||||
|
||||
Even longer time units such as months or years are imprecise.
|
||||
Weeks are precise, but not commonly used - `91 days` is more immediately
|
||||
legible than `13 weeks`. Although `days` can be problematic,
|
||||
e.g. noon to noon on subsequent days can be 23 or 25 hours depending on
|
||||
daylight savings transitions, there is significant advantage in using
|
||||
strict 24-hour days over using only hours (e.g. `91 days` vs `2184 hours`).
|
||||
|
||||
When nanoseconds are nonzero, they will be shown as fractional seconds,
|
||||
with only the minimum number of digits, e.g `0.5 seconds`.
|
||||
|
||||
A duration of exactly zero is shown as `0 seconds`.
|
||||
|
||||
Units will be given as singular (no trailing `s`) when the quantity is exactly one,
|
||||
and will be shown in plural otherwise.
|
||||
|
||||
Negative durations will be indicated with a leading minus sign (`-`).
|
||||
|
||||
Examples:
|
||||
|
||||
- `1 day`
|
||||
- `30 days`
|
||||
- `-1 day, 12 hours`
|
||||
- `3 hours, 0 minutes, 53.025 seconds`
|
||||
|
||||
### bytes
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
[
|
||||
{"proto": {"seconds": 0, "nanos": 0}, "text": "0 seconds"},
|
||||
{"proto": {"seconds": 1, "nanos": 0}, "text": "1 second"},
|
||||
{"proto": {"seconds": 2, "nanos": 0}, "text": "2 seconds"},
|
||||
{"proto": {"seconds": -1, "nanos": 0}, "text": "-1 second"},
|
||||
{"proto": {"seconds": 0, "nanos": 500000000}, "text": "0.5 seconds"},
|
||||
{"proto": {"seconds": 0, "nanos": -500000000}, "text": "-0.5 seconds"},
|
||||
{"proto": {"seconds": -1, "nanos": -500000000}, "text": "-1.5 seconds"},
|
||||
{"proto": {"seconds": 60, "nanos": 0}, "text": "1 minute"},
|
||||
{"proto": {"seconds": 3600, "nanos": 0}, "text": "1 hour"},
|
||||
{"proto": {"seconds": 86400, "nanos": 0}, "text": "1 day"},
|
||||
{"proto": {"seconds": 604800, "nanos": 0}, "text": "7 days"},
|
||||
{"proto": {"seconds": 1483530, "nanos": 0}, "text": "17 days, 4 hours, 5 minutes, 30 seconds"},
|
||||
{"proto": {"seconds": 1468800, "nanos": 100000000}, "text": "17 days, 0 hours, 0 minutes, 0.1 seconds"},
|
||||
{"proto": {"seconds": 1468860, "nanos": 100000000}, "text": "17 days, 0 hours, 1 minute, 0.1 seconds"},
|
||||
{"proto": {"seconds": 1, "nanos": -1}, "error": true},
|
||||
{"proto": {"seconds": -1, "nanos": 1}, "error": true},
|
||||
{"proto": {"seconds": 1, "nanos": 2000000000}, "error": true}
|
||||
]
|
|
@ -3,6 +3,7 @@ syntax = "proto3";
|
|||
option go_package = "cosmossdk.io/tx/textual/internal/testpb";
|
||||
|
||||
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";
|
||||
|
@ -25,6 +26,7 @@ message A {
|
|||
repeated cosmos.base.v1beta1.Coin COINS = 8;
|
||||
bytes BYTES = 9;
|
||||
google.protobuf.Timestamp TIMESTAMP = 10;
|
||||
google.protobuf.Duration DURATION = 11;
|
||||
|
||||
// Fields that are not handled by SIGN_MODE_TEXTUAL.
|
||||
sint32 SINT32 = 101;
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
protoiface "google.golang.org/protobuf/runtime/protoiface"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
_ "google.golang.org/protobuf/types/descriptorpb"
|
||||
durationpb "google.golang.org/protobuf/types/known/durationpb"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
io "io"
|
||||
math "math"
|
||||
|
@ -172,6 +173,7 @@ var (
|
|||
fd_A_COINS protoreflect.FieldDescriptor
|
||||
fd_A_BYTES protoreflect.FieldDescriptor
|
||||
fd_A_TIMESTAMP protoreflect.FieldDescriptor
|
||||
fd_A_DURATION protoreflect.FieldDescriptor
|
||||
fd_A_SINT32 protoreflect.FieldDescriptor
|
||||
fd_A_SINT64 protoreflect.FieldDescriptor
|
||||
fd_A_SFIXED32 protoreflect.FieldDescriptor
|
||||
|
@ -196,6 +198,7 @@ func init() {
|
|||
fd_A_COINS = md_A.Fields().ByName("COINS")
|
||||
fd_A_BYTES = md_A.Fields().ByName("BYTES")
|
||||
fd_A_TIMESTAMP = md_A.Fields().ByName("TIMESTAMP")
|
||||
fd_A_DURATION = md_A.Fields().ByName("DURATION")
|
||||
fd_A_SINT32 = md_A.Fields().ByName("SINT32")
|
||||
fd_A_SINT64 = md_A.Fields().ByName("SINT64")
|
||||
fd_A_SFIXED32 = md_A.Fields().ByName("SFIXED32")
|
||||
|
@ -332,6 +335,12 @@ func (x *fastReflection_A) Range(f func(protoreflect.FieldDescriptor, protorefle
|
|||
return
|
||||
}
|
||||
}
|
||||
if x.DURATION != nil {
|
||||
value := protoreflect.ValueOfMessage(x.DURATION.ProtoReflect())
|
||||
if !f(fd_A_DURATION, value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if x.SINT32 != int32(0) {
|
||||
value := protoreflect.ValueOfInt32(x.SINT32)
|
||||
if !f(fd_A_SINT32, value) {
|
||||
|
@ -421,6 +430,8 @@ func (x *fastReflection_A) Has(fd protoreflect.FieldDescriptor) bool {
|
|||
return len(x.BYTES) != 0
|
||||
case "A.TIMESTAMP":
|
||||
return x.TIMESTAMP != nil
|
||||
case "A.DURATION":
|
||||
return x.DURATION != nil
|
||||
case "A.SINT32":
|
||||
return x.SINT32 != int32(0)
|
||||
case "A.SINT64":
|
||||
|
@ -475,6 +486,8 @@ func (x *fastReflection_A) Clear(fd protoreflect.FieldDescriptor) {
|
|||
x.BYTES = nil
|
||||
case "A.TIMESTAMP":
|
||||
x.TIMESTAMP = nil
|
||||
case "A.DURATION":
|
||||
x.DURATION = nil
|
||||
case "A.SINT32":
|
||||
x.SINT32 = int32(0)
|
||||
case "A.SINT64":
|
||||
|
@ -542,6 +555,9 @@ func (x *fastReflection_A) Get(descriptor protoreflect.FieldDescriptor) protoref
|
|||
case "A.TIMESTAMP":
|
||||
value := x.TIMESTAMP
|
||||
return protoreflect.ValueOfMessage(value.ProtoReflect())
|
||||
case "A.DURATION":
|
||||
value := x.DURATION
|
||||
return protoreflect.ValueOfMessage(value.ProtoReflect())
|
||||
case "A.SINT32":
|
||||
value := x.SINT32
|
||||
return protoreflect.ValueOfInt32(value)
|
||||
|
@ -614,6 +630,8 @@ func (x *fastReflection_A) Set(fd protoreflect.FieldDescriptor, value protorefle
|
|||
x.BYTES = value.Bytes()
|
||||
case "A.TIMESTAMP":
|
||||
x.TIMESTAMP = value.Message().Interface().(*timestamppb.Timestamp)
|
||||
case "A.DURATION":
|
||||
x.DURATION = value.Message().Interface().(*durationpb.Duration)
|
||||
case "A.SINT32":
|
||||
x.SINT32 = int32(value.Int())
|
||||
case "A.SINT64":
|
||||
|
@ -670,6 +688,11 @@ func (x *fastReflection_A) Mutable(fd protoreflect.FieldDescriptor) protoreflect
|
|||
x.TIMESTAMP = new(timestamppb.Timestamp)
|
||||
}
|
||||
return protoreflect.ValueOfMessage(x.TIMESTAMP.ProtoReflect())
|
||||
case "A.DURATION":
|
||||
if x.DURATION == nil {
|
||||
x.DURATION = new(durationpb.Duration)
|
||||
}
|
||||
return protoreflect.ValueOfMessage(x.DURATION.ProtoReflect())
|
||||
case "A.MAP":
|
||||
if x.MAP == nil {
|
||||
x.MAP = make(map[string]*A)
|
||||
|
@ -742,6 +765,9 @@ func (x *fastReflection_A) NewField(fd protoreflect.FieldDescriptor) protoreflec
|
|||
case "A.TIMESTAMP":
|
||||
m := new(timestamppb.Timestamp)
|
||||
return protoreflect.ValueOfMessage(m.ProtoReflect())
|
||||
case "A.DURATION":
|
||||
m := new(durationpb.Duration)
|
||||
return protoreflect.ValueOfMessage(m.ProtoReflect())
|
||||
case "A.SINT32":
|
||||
return protoreflect.ValueOfInt32(int32(0))
|
||||
case "A.SINT64":
|
||||
|
@ -868,6 +894,10 @@ func (x *fastReflection_A) ProtoMethods() *protoiface.Methods {
|
|||
l = options.Size(x.TIMESTAMP)
|
||||
n += 1 + l + runtime.Sov(uint64(l))
|
||||
}
|
||||
if x.DURATION != nil {
|
||||
l = options.Size(x.DURATION)
|
||||
n += 1 + l + runtime.Sov(uint64(l))
|
||||
}
|
||||
if x.SINT32 != 0 {
|
||||
n += 2 + runtime.Soz(uint64(x.SINT32))
|
||||
}
|
||||
|
@ -1061,6 +1091,20 @@ func (x *fastReflection_A) ProtoMethods() *protoiface.Methods {
|
|||
i--
|
||||
dAtA[i] = 0xa8
|
||||
}
|
||||
if x.DURATION != nil {
|
||||
encoded, err := options.Marshal(x.DURATION)
|
||||
if err != nil {
|
||||
return protoiface.MarshalOutput{
|
||||
NoUnkeyedLiterals: input.NoUnkeyedLiterals,
|
||||
Buf: input.Buf,
|
||||
}, err
|
||||
}
|
||||
i -= len(encoded)
|
||||
copy(dAtA[i:], encoded)
|
||||
i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded)))
|
||||
i--
|
||||
dAtA[i] = 0x5a
|
||||
}
|
||||
if x.TIMESTAMP != nil {
|
||||
encoded, err := options.Marshal(x.TIMESTAMP)
|
||||
if err != nil {
|
||||
|
@ -1475,6 +1519,42 @@ func (x *fastReflection_A) ProtoMethods() *protoiface.Methods {
|
|||
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 11:
|
||||
if wireType != 2 {
|
||||
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field DURATION", wireType)
|
||||
}
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
msglen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if msglen < 0 {
|
||||
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex < 0 {
|
||||
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength
|
||||
}
|
||||
if postIndex > l {
|
||||
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF
|
||||
}
|
||||
if x.DURATION == nil {
|
||||
x.DURATION = &durationpb.Duration{}
|
||||
}
|
||||
if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.DURATION); err != nil {
|
||||
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 101:
|
||||
if wireType != 0 {
|
||||
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field SINT32", wireType)
|
||||
|
@ -1819,6 +1899,7 @@ type A struct {
|
|||
COINS []*v1beta1.Coin `protobuf:"bytes,8,rep,name=COINS,proto3" json:"COINS,omitempty"`
|
||||
BYTES []byte `protobuf:"bytes,9,opt,name=BYTES,proto3" json:"BYTES,omitempty"`
|
||||
TIMESTAMP *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=TIMESTAMP,proto3" json:"TIMESTAMP,omitempty"`
|
||||
DURATION *durationpb.Duration `protobuf:"bytes,11,opt,name=DURATION,proto3" json:"DURATION,omitempty"`
|
||||
// Fields that are not handled by SIGN_MODE_TEXTUAL.
|
||||
SINT32 int32 `protobuf:"zigzag32,101,opt,name=SINT32,proto3" json:"SINT32,omitempty"`
|
||||
SINT64 int64 `protobuf:"zigzag64,102,opt,name=SINT64,proto3" json:"SINT64,omitempty"`
|
||||
|
@ -1921,6 +2002,13 @@ func (x *A) GetTIMESTAMP() *timestamppb.Timestamp {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *A) GetDURATION() *durationpb.Duration {
|
||||
if x != nil {
|
||||
return x.DURATION
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *A) GetSINT32() int32 {
|
||||
if x != nil {
|
||||
return x.SINT32
|
||||
|
@ -1989,13 +2077,15 @@ var File__1_proto protoreflect.FileDescriptor
|
|||
var file__1_proto_rawDesc = []byte{
|
||||
0x0a, 0x07, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72,
|
||||
0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f,
|
||||
0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d,
|
||||
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x63, 0x6f,
|
||||
0x73, 0x6d, 0x6f, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f,
|
||||
0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f,
|
||||
0x62, 0x61, 0x73, 0x65, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x63, 0x6f, 0x69,
|
||||
0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x84, 0x05, 0x0a, 0x01, 0x41, 0x12, 0x16, 0x0a,
|
||||
0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbb, 0x05, 0x0a, 0x01, 0x41, 0x12, 0x16, 0x0a,
|
||||
0x06, 0x55, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x55,
|
||||
0x49, 0x4e, 0x54, 0x33, 0x32, 0x12, 0x16, 0x0a, 0x06, 0x55, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x55, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x12, 0x14, 0x0a,
|
||||
|
@ -2017,31 +2107,35 @@ var file__1_proto_rawDesc = []byte{
|
|||
0x38, 0x0a, 0x09, 0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x18, 0x0a, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09,
|
||||
0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x49, 0x4e,
|
||||
0x54, 0x33, 0x32, 0x18, 0x65, 0x20, 0x01, 0x28, 0x11, 0x52, 0x06, 0x53, 0x49, 0x4e, 0x54, 0x33,
|
||||
0x32, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x18, 0x66, 0x20, 0x01, 0x28,
|
||||
0x12, 0x52, 0x06, 0x53, 0x49, 0x4e, 0x54, 0x36, 0x34, 0x12, 0x1a, 0x0a, 0x08, 0x53, 0x46, 0x49,
|
||||
0x58, 0x45, 0x44, 0x33, 0x32, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0f, 0x52, 0x08, 0x53, 0x46, 0x49,
|
||||
0x58, 0x45, 0x44, 0x33, 0x32, 0x12, 0x18, 0x0a, 0x07, 0x46, 0x49, 0x58, 0x45, 0x44, 0x33, 0x32,
|
||||
0x18, 0x6a, 0x20, 0x01, 0x28, 0x07, 0x52, 0x07, 0x46, 0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x12,
|
||||
0x14, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x18, 0x6b, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05,
|
||||
0x46, 0x4c, 0x4f, 0x41, 0x54, 0x12, 0x1a, 0x0a, 0x08, 0x53, 0x46, 0x49, 0x58, 0x45, 0x44, 0x36,
|
||||
0x34, 0x18, 0x6c, 0x20, 0x01, 0x28, 0x10, 0x52, 0x08, 0x53, 0x46, 0x49, 0x58, 0x45, 0x44, 0x36,
|
||||
0x34, 0x12, 0x18, 0x0a, 0x07, 0x46, 0x49, 0x58, 0x45, 0x44, 0x36, 0x34, 0x18, 0x6d, 0x20, 0x01,
|
||||
0x28, 0x06, 0x52, 0x07, 0x46, 0x49, 0x58, 0x45, 0x44, 0x36, 0x34, 0x12, 0x16, 0x0a, 0x06, 0x44,
|
||||
0x4f, 0x55, 0x42, 0x4c, 0x45, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x44, 0x4f, 0x55,
|
||||
0x42, 0x4c, 0x45, 0x12, 0x1d, 0x0a, 0x03, 0x4d, 0x41, 0x50, 0x18, 0x6f, 0x20, 0x03, 0x28, 0x0b,
|
||||
0x32, 0x0b, 0x2e, 0x41, 0x2e, 0x4d, 0x41, 0x50, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x4d,
|
||||
0x41, 0x50, 0x1a, 0x3a, 0x0a, 0x08, 0x4d, 0x41, 0x50, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
|
||||
0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
|
||||
0x12, 0x18, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x02, 0x2e, 0x41, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x1f,
|
||||
0x0a, 0x0b, 0x45, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x07, 0x0a,
|
||||
0x03, 0x4f, 0x6e, 0x65, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x77, 0x6f, 0x10, 0x01, 0x42,
|
||||
0x33, 0x42, 0x06, 0x31, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x27, 0x63, 0x6f, 0x73,
|
||||
0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x74, 0x78, 0x2f, 0x74, 0x65, 0x78,
|
||||
0x74, 0x75, 0x61, 0x6c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65,
|
||||
0x73, 0x74, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x12, 0x35, 0x0a, 0x08, 0x44, 0x55, 0x52,
|
||||
0x41, 0x54, 0x49, 0x4f, 0x4e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75,
|
||||
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x44, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e,
|
||||
0x12, 0x16, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x18, 0x65, 0x20, 0x01, 0x28, 0x11,
|
||||
0x52, 0x06, 0x53, 0x49, 0x4e, 0x54, 0x33, 0x32, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x54,
|
||||
0x36, 0x34, 0x18, 0x66, 0x20, 0x01, 0x28, 0x12, 0x52, 0x06, 0x53, 0x49, 0x4e, 0x54, 0x36, 0x34,
|
||||
0x12, 0x1a, 0x0a, 0x08, 0x53, 0x46, 0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x18, 0x69, 0x20, 0x01,
|
||||
0x28, 0x0f, 0x52, 0x08, 0x53, 0x46, 0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x12, 0x18, 0x0a, 0x07,
|
||||
0x46, 0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x18, 0x6a, 0x20, 0x01, 0x28, 0x07, 0x52, 0x07, 0x46,
|
||||
0x49, 0x58, 0x45, 0x44, 0x33, 0x32, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x18,
|
||||
0x6b, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x46, 0x4c, 0x4f, 0x41, 0x54, 0x12, 0x1a, 0x0a, 0x08,
|
||||
0x53, 0x46, 0x49, 0x58, 0x45, 0x44, 0x36, 0x34, 0x18, 0x6c, 0x20, 0x01, 0x28, 0x10, 0x52, 0x08,
|
||||
0x53, 0x46, 0x49, 0x58, 0x45, 0x44, 0x36, 0x34, 0x12, 0x18, 0x0a, 0x07, 0x46, 0x49, 0x58, 0x45,
|
||||
0x44, 0x36, 0x34, 0x18, 0x6d, 0x20, 0x01, 0x28, 0x06, 0x52, 0x07, 0x46, 0x49, 0x58, 0x45, 0x44,
|
||||
0x36, 0x34, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x18, 0x6e, 0x20, 0x01,
|
||||
0x28, 0x01, 0x52, 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x12, 0x1d, 0x0a, 0x03, 0x4d, 0x41,
|
||||
0x50, 0x18, 0x6f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x41, 0x2e, 0x4d, 0x41, 0x50, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x4d, 0x41, 0x50, 0x1a, 0x3a, 0x0a, 0x08, 0x4d, 0x41, 0x50,
|
||||
0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x02, 0x2e, 0x41, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x1f, 0x0a, 0x0b, 0x45, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x6e, 0x65, 0x10, 0x00, 0x12, 0x07, 0x0a,
|
||||
0x03, 0x54, 0x77, 0x6f, 0x10, 0x01, 0x42, 0x33, 0x42, 0x06, 0x31, 0x50, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x50, 0x01, 0x5a, 0x27, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f,
|
||||
0x2f, 0x74, 0x78, 0x2f, 0x74, 0x65, 0x78, 0x74, 0x75, 0x61, 0x6c, 0x2f, 0x69, 0x6e, 0x74, 0x65,
|
||||
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -2064,18 +2158,20 @@ var file__1_proto_goTypes = []interface{}{
|
|||
nil, // 2: A.MAPEntry
|
||||
(*v1beta1.Coin)(nil), // 3: cosmos.base.v1beta1.Coin
|
||||
(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
|
||||
(*durationpb.Duration)(nil), // 5: google.protobuf.Duration
|
||||
}
|
||||
var file__1_proto_depIdxs = []int32{
|
||||
3, // 0: A.COIN:type_name -> cosmos.base.v1beta1.Coin
|
||||
3, // 1: A.COINS:type_name -> cosmos.base.v1beta1.Coin
|
||||
4, // 2: A.TIMESTAMP:type_name -> google.protobuf.Timestamp
|
||||
2, // 3: A.MAP:type_name -> A.MAPEntry
|
||||
1, // 4: A.MAPEntry.value:type_name -> A
|
||||
5, // [5:5] is the sub-list for method output_type
|
||||
5, // [5:5] is the sub-list for method input_type
|
||||
5, // [5:5] is the sub-list for extension type_name
|
||||
5, // [5:5] is the sub-list for extension extendee
|
||||
0, // [0:5] is the sub-list for field type_name
|
||||
5, // 3: A.DURATION:type_name -> google.protobuf.Duration
|
||||
2, // 4: A.MAP:type_name -> A.MAPEntry
|
||||
1, // 5: A.MAPEntry.value:type_name -> A
|
||||
6, // [6:6] is the sub-list for method output_type
|
||||
6, // [6:6] is the sub-list for method input_type
|
||||
6, // [6:6] is the sub-list for extension type_name
|
||||
6, // [6:6] is the sub-list for extension extendee
|
||||
0, // [0:6] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file__1_proto_init() }
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
package valuerenderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
dpb "google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
type durationValueRenderer struct{}
|
||||
|
||||
// NewDurationValueRenderer returns a ValueRenderer for protocol buffer Duration messages.
|
||||
// It renders durations by grouping seconds into units of days (86400s), hours (3600s),
|
||||
// and minutes(60s), plus the total seconds elapsed. E.g. a duration of 1483530s is
|
||||
// formatted as "17 days, 4 hours, 5 minutes, 30 seconds".
|
||||
// Note that the days are always 24 hours regardless of daylight savings changes.
|
||||
func NewDurationValueRenderer() ValueRenderer {
|
||||
return durationValueRenderer{}
|
||||
}
|
||||
|
||||
const (
|
||||
min_sec = 60
|
||||
hour_sec = 60 * min_sec
|
||||
day_sec = 24 * hour_sec
|
||||
)
|
||||
|
||||
type factors struct {
|
||||
days, hours, minutes, seconds int64
|
||||
}
|
||||
|
||||
func factorSeconds(x int64) factors {
|
||||
var f factors
|
||||
f.days = x / day_sec
|
||||
x -= f.days * day_sec
|
||||
f.hours = x / hour_sec
|
||||
x -= f.hours * hour_sec
|
||||
f.minutes = x / min_sec
|
||||
x -= f.minutes * min_sec
|
||||
f.seconds = x
|
||||
return f
|
||||
}
|
||||
|
||||
func maybePlural(s string, plural bool) string {
|
||||
if plural {
|
||||
return s + "s"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func formatSeconds(seconds int64, nanos int32) string {
|
||||
var s string
|
||||
if nanos == 0 {
|
||||
s = fmt.Sprintf("%d", seconds)
|
||||
} else {
|
||||
frac := fmt.Sprintf("%09d", nanos)
|
||||
frac = strings.TrimRight(frac, "0")
|
||||
s = fmt.Sprintf("%d.%s", seconds, frac)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Format implements the ValueRenderer interface.
|
||||
func (dr durationValueRenderer) Format(_ context.Context, v protoreflect.Value, w io.Writer) 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
negative := false
|
||||
if duration.Seconds < 0 || duration.Nanos < 0 {
|
||||
negative = true
|
||||
// copy to avoid side-effecting our input
|
||||
d := *duration
|
||||
duration = &d
|
||||
duration.Seconds *= -1
|
||||
duration.Nanos *= -1
|
||||
}
|
||||
factors := factorSeconds(duration.Seconds)
|
||||
components := []string{}
|
||||
|
||||
if factors.days > 0 {
|
||||
components = append(components, fmt.Sprintf("%d %s", factors.days, maybePlural("day", factors.days != 1)))
|
||||
}
|
||||
if factors.hours > 0 || (len(components) > 0 && (factors.minutes > 0 || factors.seconds > 0 || duration.Nanos > 0)) {
|
||||
components = append(components, fmt.Sprintf("%d %s", factors.hours, maybePlural("hour", factors.hours != 1)))
|
||||
}
|
||||
if factors.minutes > 0 || (len(components) > 0 && (factors.seconds > 0 || duration.Nanos > 0)) {
|
||||
components = append(components, fmt.Sprintf("%d %s", factors.minutes, maybePlural("minute", factors.minutes != 1)))
|
||||
}
|
||||
if factors.seconds > 0 || duration.Nanos > 0 {
|
||||
components = append(components, formatSeconds(factors.seconds, duration.Nanos)+" "+maybePlural("second", factors.seconds != 1 || duration.Nanos > 0))
|
||||
}
|
||||
|
||||
s := strings.Join(components, ", ")
|
||||
|
||||
if s == "" {
|
||||
s = "0 seconds"
|
||||
}
|
||||
|
||||
if negative {
|
||||
s = "-" + s
|
||||
}
|
||||
|
||||
_, err := w.Write([]byte(s))
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
parts := durRegexp.FindStringSubmatch(string(bz))
|
||||
if parts == nil {
|
||||
return protoreflect.Value{}, fmt.Errorf("bad duration format: %s", string(bz))
|
||||
}
|
||||
|
||||
negative := parts[1] != ""
|
||||
var days, hours, minutes, seconds, nanos int64
|
||||
|
||||
if parts[2] != "" {
|
||||
days, err = strconv.ParseInt(parts[2], 10, 64)
|
||||
if err != nil {
|
||||
return protoreflect.Value{}, fmt.Errorf(`bad number "%s": %w`, parts[2], err)
|
||||
}
|
||||
}
|
||||
if parts[3] != "" {
|
||||
hours, err = strconv.ParseInt(parts[3], 10, 64)
|
||||
if err != nil {
|
||||
return protoreflect.Value{}, fmt.Errorf(`bad number "%s": %w`, parts[3], err)
|
||||
}
|
||||
}
|
||||
if parts[4] != "" {
|
||||
minutes, err = strconv.ParseInt(parts[4], 10, 64)
|
||||
if err != nil {
|
||||
return protoreflect.Value{}, fmt.Errorf(`bad number "%s": %w`, parts[4], err)
|
||||
}
|
||||
}
|
||||
if parts[5] != "" {
|
||||
seconds, err = strconv.ParseInt(parts[5], 10, 64)
|
||||
if err != nil {
|
||||
return protoreflect.Value{}, fmt.Errorf(`bad number "%s": %w`, parts[5], err)
|
||||
}
|
||||
if parts[6] != "" {
|
||||
if len(parts[6]) > 9 {
|
||||
return protoreflect.Value{}, fmt.Errorf(`too many nanos "%s"`, parts[6])
|
||||
}
|
||||
addZeros := 9 - len(parts[6])
|
||||
text := parts[6] + strings.Repeat("0", addZeros)
|
||||
nanos, err = strconv.ParseInt(text, 10, 32)
|
||||
if err != nil {
|
||||
return protoreflect.Value{}, fmt.Errorf(`bad number "%s": %w`, text, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dur := &dpb.Duration{}
|
||||
dur.Seconds = days*day_sec + hours*hour_sec + minutes*min_sec + seconds
|
||||
// #nosec G701
|
||||
// Since there are 9 digits or fewer, this conversion is safe.
|
||||
dur.Nanos = int32(nanos)
|
||||
|
||||
if negative {
|
||||
dur.Seconds *= -1
|
||||
dur.Nanos *= -1
|
||||
}
|
||||
|
||||
msg := dur.ProtoReflect()
|
||||
return protoreflect.ValueOfMessage(msg), nil
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package valuerenderer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cosmossdk.io/tx/textual/valuerenderer"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
dpb "google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
type durationTest struct {
|
||||
Proto *dpb.Duration
|
||||
Text string
|
||||
Error bool
|
||||
}
|
||||
|
||||
func TestDurationJSON(t *testing.T) {
|
||||
raw, err := os.ReadFile("../internal/testdata/duration.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
var testcases []durationTest
|
||||
err = json.Unmarshal(raw, &testcases)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, tc := range testcases {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
rend := valuerenderer.NewDurationValueRenderer()
|
||||
|
||||
if tc.Proto != nil {
|
||||
wr := new(strings.Builder)
|
||||
err = rend.Format(context.Background(), protoreflect.ValueOf(tc.Proto.ProtoReflect()), wr)
|
||||
if tc.Error {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.Text, wr.String())
|
||||
}
|
||||
|
||||
rd := strings.NewReader(tc.Text)
|
||||
val, err := rend.Parse(context.Background(), rd)
|
||||
if tc.Error {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
msg := val.Message().Interface()
|
||||
require.IsType(t, &dpb.Duration{}, msg)
|
||||
duration := msg.(*dpb.Duration)
|
||||
require.True(t, proto.Equal(duration, tc.Proto), "%v vs %v", duration, tc.Proto)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1"
|
||||
|
@ -99,6 +100,7 @@ func (r *Textual) init() {
|
|||
if r.messages == nil {
|
||||
r.messages = map[protoreflect.FullName]ValueRenderer{}
|
||||
r.messages[(&basev1beta1.Coin{}).ProtoReflect().Descriptor().FullName()] = NewCoinsValueRenderer(r.coinMetadataQuerier)
|
||||
r.messages[(&durationpb.Duration{}).ProtoReflect().Descriptor().FullName()] = NewDurationValueRenderer()
|
||||
r.messages[(×tamppb.Timestamp{}).ProtoReflect().Descriptor().FullName()] = NewTimestampValueRenderer()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ func TestDispatcher(t *testing.T) {
|
|||
{"SDKDEC", false, valuerenderer.NewDecValueRenderer()},
|
||||
{"BYTES", false, valuerenderer.NewBytesValueRenderer()},
|
||||
{"TIMESTAMP", false, valuerenderer.NewTimestampValueRenderer()},
|
||||
{"DURATION", false, valuerenderer.NewDurationValueRenderer()},
|
||||
{"COIN", false, valuerenderer.NewCoinsValueRenderer(nil)},
|
||||
{"COINS", false, valuerenderer.NewCoinsValueRenderer(nil)},
|
||||
{"FLOAT", true, nil},
|
||||
|
|
Loading…
Reference in New Issue