Support for repeated (#13604)

This commit is contained in:
Joe Abbey 2022-11-29 11:37:29 -05:00 committed by GitHub
parent a176eb2646
commit 2cf92caf45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2839 additions and 118 deletions

View File

@ -62,20 +62,23 @@ Value Renderers describe how values of different Protobuf types should be encode
- A repeated type has the following template:
```
<message_name> has <int> <field_name>
<field_name> (<int>/<int>): <value rendered 1st line>
<field_name>: <int> <field_kind>
<field_name> (<index>/<int>): <value rendered 1st line>
<optional value rendered in the next lines>
<field_name> (<int>/<int>): <value rendered 1st line>
<field_name> (<index>/<int>): <value rendered 1st line>
<optional value rendered in the next lines>
End of <field_name>.
```
where:
- `message_name` is the name of the Protobuf message which holds the `repeated` field,
- `int` is the length of the array,
- `field_name` is the Protobuf field name of the repeated field,
- add an optional `s` at the end if `<int> > 1` and the `field_name` doesn't already end with `s`.
- `field_name` is the Protobuf field name of the repeated field
- `field_kind`:
- if the type of the repeated field is a message, `field_kind` is the message name
- if the type of the repeated field is an enum, `field_kind` is the enum name
- in any other case, `field_kind` is the protobuf primitive type (e.g. "string" or "bytes")
- `int` is the length of the array
- `index` is one based index of the repeated field
#### Examples
@ -150,7 +153,7 @@ we get the following encoding for the `Vote` message:
```
Vote object
> Proposal id: 4
> Vote: cosmos1abc...def
> Voter: cosmos1abc...def
> Options: 2 WeightedVoteOptions
> Options (1/2): WeightedVoteOption object
>> Option: VOTE_OPTION_YES

View File

@ -0,0 +1,161 @@
[
{
"proto": {},
"parses": true,
"screens": [
{"text": "Qux object", "indent": 0}
]
},
{
"proto": {
"messages": [
{
"full_name": "thing one",
"nickname": ":thing two"
}
]
},
"parses": true,
"screens": [
{"text": "Qux object", "indent": 0},
{"text": "Messages: 1 Foo", "indent": 1},
{"text": "Messages (1/1): Foo object", "indent": 2},
{"text": "Full name: thing one", "indent": 3},
{"text": "Nickname: :thing two", "indent": 3},
{"text": "End of Messages", "indent": 1}
]
},
{
"proto": {
"messages": [
{
"full_name": "thing one",
"nickname": "thing two"
},
{
"full_name": "thing three",
"nickname": "thing four"
}
]
},
"parses": true,
"screens": [
{"text": "Qux object", "indent": 0},
{"text": "Messages: 2 Foo", "indent": 1},
{"text": "Messages (1/2): Foo object", "indent": 2},
{"text": "Full name: thing one", "indent": 3},
{"text": "Nickname: thing two", "indent": 3},
{"text": "Messages (2/2): Foo object", "indent": 2},
{"text": "Full name: thing three", "indent": 3},
{"text": "Nickname: thing four", "indent": 3},
{"text": "End of Messages", "indent": 1}
]
},
{
"proto": {
"string_messages": [
"/OmniFlix.onft.v1beta1.MsgTransferONFT"
]
},
"parses": true,
"screens": [
{"text": "Qux object", "indent": 0},
{"text": "String messages: 1 String", "indent": 1},
{"text": "String messages (1/1): /OmniFlix.onft.v1beta1.MsgTransferONFT", "indent": 2},
{"text": "End of String messages", "indent": 1}
]
},
{
"proto": {
"string_messages": [
"/OmniFlix.onft.v1beta1.MsgTransferONFT",
"/OmniFlix.onft.v1beta1.MsgBurnONFT",
"/OmniFlix.marketplace.v1beta1.MsgListNFT"
]
},
"parses": true,
"screens": [
{"text": "Qux object", "indent": 0},
{"text": "String messages: 3 String", "indent": 1},
{"text": "String messages (1/3): /OmniFlix.onft.v1beta1.MsgTransferONFT", "indent": 2},
{"text": "String messages (2/3): /OmniFlix.onft.v1beta1.MsgBurnONFT", "indent": 2},
{"text": "String messages (3/3): /OmniFlix.marketplace.v1beta1.MsgListNFT", "indent": 2},
{"text": "End of String messages", "indent": 1}
]
},
{
"proto": {
"vote" : {
"proposal_id": 4,
"voter": "cosmos1abc...def",
"options": [
{
"option": "Yes",
"weight": "0.7"
},
{
"option": "No",
"weight": "0.3"
}
]
}
},
"parses": false,
"screens": [
{"text": "Qux object", "indent": 0},
{"text": "Vote: Ballot object", "indent": 1},
{"text": "Proposal id: 4", "indent": 2},
{"text": "Voter: cosmos1abc...def", "indent": 2},
{"text": "Options: 2 WeightedBallotOption", "indent": 2},
{"text": "Options (1/2): WeightedBallotOption object", "indent": 3},
{"text": "Option: Yes", "indent": 4},
{"text": "Weight: 0.7", "indent": 4},
{"text": "Options (2/2): WeightedBallotOption object", "indent": 3},
{"text": "Option: No", "indent": 4},
{"text": "Weight: 0.3", "indent": 4},
{"text": "End of Options", "indent": 2}
]
},
{
"proto": {
"price": [
{ "amount": "1", "denom": "ucosm" },
{ "amount": "3", "denom": "ustake" }
]
},
"parses": false,
"screens": [
{"text": "Qux object", "indent": 0},
{"text": "Price: 1 ucosm, 3 ustake", "indent": 1}
]
},
{
"proto": {
"expirations": [
{"seconds": 0, "nanos": 1},
{"seconds": 0, "nanos": 10},
{"seconds": 0, "nanos": 100},
{"seconds": 0, "nanos": 1000},
{"seconds": 0, "nanos": 10000},
{"seconds": 0, "nanos": 100000},
{"seconds": 0, "nanos": 1000000},
{"seconds": 0, "nanos": 10000000}
]
},
"parses": true,
"screens": [
{"text": "Qux object", "indent": 0},
{"text": "Expirations: 8 Timestamp", "indent": 1},
{"text": "Expirations (1/8): 1970-01-01T00:00:00.000000001Z", "indent": 2},
{"text": "Expirations (2/8): 1970-01-01T00:00:00.00000001Z", "indent": 2},
{"text": "Expirations (3/8): 1970-01-01T00:00:00.0000001Z", "indent": 2},
{"text": "Expirations (4/8): 1970-01-01T00:00:00.000001Z", "indent": 2},
{"text": "Expirations (5/8): 1970-01-01T00:00:00.00001Z", "indent": 2},
{"text": "Expirations (6/8): 1970-01-01T00:00:00.0001Z", "indent": 2},
{"text": "Expirations (7/8): 1970-01-01T00:00:00.001Z", "indent": 2},
{"text": "Expirations (8/8): 1970-01-01T00:00:00.01Z", "indent": 2},
{"text": "End of Expirations", "indent": 1}
]
}
]

View File

@ -1,57 +1,57 @@
syntax = "proto3";
option go_package = "cosmossdk.io/tx/textual/internal/testpb";
import "cosmos/base/v1beta1/coin.proto";
import "cosmos_proto/cosmos.proto";
import "google/protobuf/any.proto";
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";
option go_package = "cosmossdk.io/tx/textual/internal/testpb";
// A is used for testing value renderers.
message A {
// Fields that are parseable by SIGN_MODE_TEXTUAL.
uint32 UINT32 = 1;
uint64 UINT64 = 2;
int32 INT32 = 3;
int64 INT64 = 4;
string SDKINT = 5 [(cosmos_proto.scalar) = "cosmos.Int"];
string SDKDEC = 6 [(cosmos_proto.scalar) = "cosmos.Dec"];
cosmos.base.v1beta1.Coin COIN = 7;
repeated cosmos.base.v1beta1.Coin COINS = 8;
bytes BYTES = 9;
google.protobuf.Timestamp TIMESTAMP = 10;
google.protobuf.Duration DURATION = 11;
ExternalEnum ENUM = 12;
google.protobuf.Any ANY = 13;
uint32 UINT32 = 1;
uint64 UINT64 = 2;
int32 INT32 = 3;
int64 INT64 = 4;
string SDKINT = 5 [(cosmos_proto.scalar) = "cosmos.Int"];
string SDKDEC = 6 [(cosmos_proto.scalar) = "cosmos.Dec"];
cosmos.base.v1beta1.Coin COIN = 7;
repeated cosmos.base.v1beta1.Coin COINS = 8;
bytes BYTES = 9;
google.protobuf.Timestamp TIMESTAMP = 10;
google.protobuf.Duration DURATION = 11;
ExternalEnum ENUM = 12;
google.protobuf.Any ANY = 13;
// Fields that are not handled by SIGN_MODE_TEXTUAL.
sint32 SINT32 = 101;
sint64 SINT64 = 102;
sfixed32 SFIXED32 = 105;
fixed32 FIXED32 = 106;
float FLOAT = 107;
sfixed64 SFIXED64 = 108;
fixed64 FIXED64 = 109;
double DOUBLE = 110;
map<string, A> MAP = 111;
sint32 SINT32 = 101;
sint64 SINT64 = 102;
sfixed32 SFIXED32 = 105;
fixed32 FIXED32 = 106;
float FLOAT = 107;
sfixed64 SFIXED64 = 108;
fixed64 FIXED64 = 109;
double DOUBLE = 110;
map<string, A> MAP = 111;
}
// Foo is a sample message type used for testing message rendering.
message Foo {
string full_name = 1;
string nickname = 2;
google.protobuf.Timestamp mtime = 3;
Foo left = 4;
Foo right = 5;
Bar bar = 8; // skip some field numbers
string full_name = 1;
string nickname = 2;
google.protobuf.Timestamp mtime = 3;
Foo left = 4;
Foo right = 5;
Bar bar = 8; // skip some field numbers
}
// Bar is a sample message type used for testing message rendering.
message Bar {
string bar_id = 1;
bytes data = 2;
string bar_id = 1;
bytes data = 2;
google.protobuf.Any payload = 3;
}
@ -71,13 +71,37 @@ message Baz {
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_UNSPECIFIED = 0;
BALLOT_OPTION_YES = 1;
BALLOT_OPTION_ABSTAIN = 2;
BALLOT_OPTION_NO = 3;
BALLOT_OPTION_NO_WITH_VETO = 4;
}
// Qux is a sample message type used for testing repeated rendering.
message Qux {
repeated Foo messages = 1;
repeated string string_messages = 2;
Ballot vote = 3;
repeated cosmos.base.v1beta1.Coin price = 4;
repeated google.protobuf.Timestamp expirations = 5;
}
message WeightedBallotOption {
// TODO: Enumeration rendering
// BallotOption option = 1;
string option = 1;
string weight = 2 [(cosmos_proto.scalar) = "cosmos.Dec"];
}
message Ballot {
uint64 proposal_id = 1;
// TODO: cosmos.AddressString rendering
// string voter = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string voter = 2;
reserved 3;
repeated WeightedBallotOption options = 4;
}

File diff suppressed because it is too large Load Diff

View File

@ -80,3 +80,12 @@ func (vr coinsValueRenderer) Parse(_ context.Context, screens []Screen) (protore
// ref: https://github.com/cosmos/cosmos-sdk/issues/13153
panic("implement me, see #13153")
}
func (vr coinsValueRenderer) FormatRepeated(ctx context.Context, v protoreflect.Value) ([]Screen, error) {
return vr.Format(ctx, v)
}
func (vr coinsValueRenderer) ParseRepeated(ctx context.Context, screens []Screen, l protoreflect.List) error {
// ref: https://github.com/cosmos/cosmos-sdk/issues/13153
panic("implement me, see #13153")
}

View File

@ -2,8 +2,11 @@ package valuerenderer
import (
"context"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"google.golang.org/protobuf/reflect/protoreflect"
@ -51,7 +54,20 @@ func (mr *messageValueRenderer) Format(ctx context.Context, v protoreflect.Value
return nil, err
}
subscreens, err := vr.Format(ctx, v.Message().Get(fd))
subscreens := make([]Screen, 0)
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
}
@ -60,7 +76,7 @@ func (mr *messageValueRenderer) Format(ctx context.Context, v protoreflect.Value
}
headerScreen := Screen{
Text: fmt.Sprintf("%s: %s", formatFieldName(string(fd.Name())), subscreens[0].Text),
Text: fmt.Sprintf("%s: %s", toSentenceCase(string(fd.Name())), subscreens[0].Text),
Indent: subscreens[0].Indent + 1,
Expert: subscreens[0].Expert,
}
@ -79,9 +95,74 @@ func (mr *messageValueRenderer) Format(ctx context.Context, v protoreflect.Value
return screens, nil
}
// formatFieldName formats a field name in sentence case, as specified in:
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].Text = 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>): <value rendered 1st line>
Text: fmt.Sprintf("%s (%d/%d): %s", toSentenceCase(string(fd.Name())), i+1, l.Len(), subscreens[0].Text),
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{
Text: subscreens[i].Text,
Indent: subscreens[i].Indent + 1,
Expert: subscreens[i].Expert,
}
screens = append(screens, extraScreen)
}
}
// End of <field_name>
terminalScreen := Screen{
Text: 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 formatFieldName(name string) string {
func toSentenceCase(name string) string {
if len(name) == 0 {
return name
}
@ -92,7 +173,7 @@ var nilValue = protoreflect.Value{}
func (mr *messageValueRenderer) Parse(ctx context.Context, screens []Screen) (protoreflect.Value, error) {
if len(screens) == 0 {
return nilValue, fmt.Errorf("expect at least one screen")
return nilValue, errors.New("expect at least one screen")
}
wantHeader := fmt.Sprintf("%s object", mr.msgDesc.Name())
@ -125,7 +206,7 @@ func (mr *messageValueRenderer) Parse(ctx context.Context, screens []Screen) (pr
return nilValue, fmt.Errorf("bad message indentation: want 1, got %d", screens[idx].Indent)
}
prefix := formatFieldName(string(fd.Name())) + ": "
prefix := toSentenceCase(string(fd.Name())) + ": "
if !strings.HasPrefix(screens[idx].Text, prefix) {
// we must have skipped this fd because of a default value
continue
@ -146,16 +227,90 @@ func (mr *messageValueRenderer) Parse(ctx context.Context, screens []Screen) (pr
idx++
}
val, err := vr.Parse(ctx, subscreens)
if err != nil {
return nilValue, err
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)
}
if err != nil {
return nilValue, err
}
msg.Set(fd, nf)
//Skip List Terminator
idx++
} else {
val, err = vr.Parse(ctx, subscreens)
if err != nil {
return nilValue, err
}
msg.Set(fd, val)
}
msg.Set(fd, val)
}
if idx > len(screens) {
return nilValue, fmt.Errorf("leftover screens")
return nilValue, errors.New("leftover screens")
}
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].Text, -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].Text, -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].Text = strings.TrimPrefix(screens[idx].Text, 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
}

View File

@ -26,6 +26,9 @@ func (x *genericList[T]) Len() int {
}
func (x *genericList[T]) Get(i int) protoreflect.Value {
if x.Len() == 0 {
return protoreflect.Value{}
}
return protoreflect.ValueOfMessage((*x.list)[i].ProtoReflect())
}

View File

@ -0,0 +1,58 @@
package valuerenderer_test
import (
"context"
"encoding/json"
"fmt"
"os"
"testing"
"cosmossdk.io/tx/textual/valuerenderer"
"github.com/stretchr/testify/require"
"cosmossdk.io/tx/textual/internal/testpb"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
)
type repeatedJsonTest struct {
Proto *testpb.Qux
Screens []valuerenderer.Screen
// TODO Remove once we finished all primitive value renderers parsing
// https://github.com/cosmos/cosmos-sdk/pull/13696
// https://github.com/cosmos/cosmos-sdk/pull/13853
Parses bool
}
func TestRepeatedJsonTestcases(t *testing.T) {
raw, err := os.ReadFile("../internal/testdata/repeated.json")
require.NoError(t, err)
var testcases []repeatedJsonTest
err = json.Unmarshal(raw, &testcases)
require.NoError(t, err)
tr := valuerenderer.NewTextual(mockCoinMetadataQuerier)
for i, tc := range testcases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
// Create a context.Context containing all coins metadata, to simulate
// that they are in state.
ctx := context.Background()
rend := valuerenderer.NewMessageValueRenderer(&tr, (&testpb.Qux{}).ProtoReflect().Descriptor())
require.NoError(t, err)
screens, err := rend.Format(ctx, protoreflect.ValueOf(tc.Proto.ProtoReflect()))
require.NoError(t, err)
require.Equal(t, tc.Screens, screens)
if tc.Parses {
val, err := rend.Parse(context.Background(), screens)
require.NoError(t, err)
msg := val.Message().Interface()
require.IsType(t, &testpb.Qux{}, msg)
baz := msg.(*testpb.Qux)
require.True(t, proto.Equal(baz, tc.Proto))
}
})
}
}

View File

@ -34,3 +34,15 @@ type ValueRenderer interface {
// Parse should be the inverse of Format.
Parse(context.Context, []Screen) (protoreflect.Value, error)
}
// RepeatedValueRenderer defines an interface to produce formatted output for
// protobuf message fields that are repeated.
type RepeatedValueRenderer interface {
ValueRenderer
// FormatRepeated should render the value to a text plus annotation.
FormatRepeated(context.Context, protoreflect.Value) ([]Screen, error)
// ParseRepeated should be the inverse of Format. The list will be populated with the repeated values.
ParseRepeated(context.Context, []Screen, protoreflect.List) error
}

View File

@ -50,8 +50,8 @@ func NewTextual(q CoinMetadataQueryFn) Textual {
func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueRenderer, error) {
switch {
// Scalars, such as sdk.Int and sdk.Dec encoded as strings.
case fd.Kind() == protoreflect.StringKind && proto.GetExtension(fd.Options(), cosmos_proto.E_Scalar) != "":
{
case fd.Kind() == protoreflect.StringKind:
if proto.GetExtension(fd.Options(), cosmos_proto.E_Scalar) != "" {
scalar, ok := proto.GetExtension(fd.Options(), cosmos_proto.E_Scalar).(string)
if !ok || scalar == "" {
return nil, fmt.Errorf("got extension option %s of type %T", scalar, scalar)
@ -64,6 +64,8 @@ func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueR
return vr, nil
}
return NewStringValueRenderer(), nil
case fd.Kind() == protoreflect.BytesKind:
return NewBytesValueRenderer(), nil
@ -72,9 +74,7 @@ func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueR
fd.Kind() == protoreflect.Uint64Kind ||
fd.Kind() == protoreflect.Int32Kind ||
fd.Kind() == protoreflect.Int64Kind:
{
return NewIntValueRenderer(), nil
}
return NewIntValueRenderer(), nil
case fd.Kind() == protoreflect.StringKind:
return stringValueRenderer{}, nil
@ -93,10 +93,6 @@ func (r *Textual) GetFieldValueRenderer(fd protoreflect.FieldDescriptor) (ValueR
if fd.IsMap() {
return nil, fmt.Errorf("value renderers cannot format value of type map")
}
if fd.IsList() {
// This will be implemented in https://github.com/cosmos/cosmos-sdk/issues/12714
return nil, fmt.Errorf("repeated field renderer not yet implemented")
}
return NewMessageValueRenderer(r, md), nil
default: