feat(codec): Add collections value codec for interfaces. (#15898)

Co-authored-by: unknown unknown <unknown@unknown>
This commit is contained in:
testinginprod 2023-04-24 10:34:42 +02:00 committed by GitHub
parent 294027cf2d
commit 295c261b18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 124 additions and 6 deletions

View File

@ -1,6 +1,9 @@
package codec
import (
"fmt"
"reflect"
gogotypes "github.com/cosmos/gogoproto/types"
"cosmossdk.io/collections"
@ -79,3 +82,47 @@ func (c collValue[T, PT]) Stringify(value T) string {
func (c collValue[T, PT]) ValueType() string {
return "gogoproto/" + proto.MessageName(PT(new(T)))
}
// CollInterfaceValue instantiates a new collections.ValueCodec for a generic
// interface value. The codec must be able to marshal and unmarshal the
// interface.
func CollInterfaceValue[T proto.Message](codec BinaryCodec) collcodec.ValueCodec[T] {
var x T // assertion
if reflect.TypeOf(&x).Elem().Kind() != reflect.Interface {
panic("CollInterfaceValue can only be used with interface types")
}
return collInterfaceValue[T]{codec.(Codec)}
}
type collInterfaceValue[T proto.Message] struct {
codec Codec
}
func (c collInterfaceValue[T]) Encode(value T) ([]byte, error) {
return c.codec.MarshalInterface(value)
}
func (c collInterfaceValue[T]) Decode(b []byte) (T, error) {
var value T
err := c.codec.UnmarshalInterface(b, &value)
return value, err
}
func (c collInterfaceValue[T]) EncodeJSON(value T) ([]byte, error) {
return c.codec.MarshalInterfaceJSON(value)
}
func (c collInterfaceValue[T]) DecodeJSON(b []byte) (T, error) {
var value T
err := c.codec.UnmarshalInterfaceJSON(b, &value)
return value, err
}
func (c collInterfaceValue[T]) Stringify(value T) string {
return value.String()
}
func (c collInterfaceValue[T]) ValueType() string {
var t T
return fmt.Sprintf("%T", t)
}

View File

@ -1,8 +1,11 @@
package codec
package codec_test
import (
"testing"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
"github.com/stretchr/testify/require"
"cosmossdk.io/collections/colltest"
@ -11,22 +14,22 @@ import (
)
func TestCollectionsCorrectness(t *testing.T) {
cdc := NewProtoCodec(codectypes.NewInterfaceRegistry())
t.Run("CollValue", func(t *testing.T) {
colltest.TestValueCodec(t, CollValue[gogotypes.UInt64Value](cdc), gogotypes.UInt64Value{
cdc := codec.NewProtoCodec(codectypes.NewInterfaceRegistry())
colltest.TestValueCodec(t, codec.CollValue[gogotypes.UInt64Value](cdc), gogotypes.UInt64Value{
Value: 500,
})
})
t.Run("BoolValue", func(t *testing.T) {
colltest.TestValueCodec(t, BoolValue, true)
colltest.TestValueCodec(t, BoolValue, false)
colltest.TestValueCodec(t, codec.BoolValue, true)
colltest.TestValueCodec(t, codec.BoolValue, false)
// asserts produced bytes are equal
valueAssert := func(b bool) {
wantBytes, err := (&gogotypes.BoolValue{Value: b}).Marshal()
require.NoError(t, err)
gotBytes, err := BoolValue.Encode(b)
gotBytes, err := codec.BoolValue.Encode(b)
require.NoError(t, err)
require.Equal(t, wantBytes, gotBytes)
}
@ -34,4 +37,18 @@ func TestCollectionsCorrectness(t *testing.T) {
valueAssert(true)
valueAssert(false)
})
t.Run("CollInterfaceValue", func(t *testing.T) {
cdc := codec.NewProtoCodec(codectypes.NewInterfaceRegistry())
cdc.InterfaceRegistry().RegisterInterface("animal", (*testdata.Animal)(nil), &testdata.Dog{}, &testdata.Cat{})
valueCodec := codec.CollInterfaceValue[testdata.Animal](cdc)
colltest.TestValueCodec[testdata.Animal](t, valueCodec, &testdata.Dog{Name: "Doggo"})
colltest.TestValueCodec[testdata.Animal](t, valueCodec, &testdata.Cat{Moniker: "Kitty"})
// assert if used with a non interface type it yields a panic.
require.Panics(t, func() {
codec.CollInterfaceValue[*testdata.Dog](cdc)
})
})
}

View File

@ -1063,3 +1063,57 @@ func (k Keeper) getNextAccountNumber() uint64 {
return 0
}
```
## Collections with interfaces as values
Although cosmos-sdk is shifting away from the usage of interface registry, there are still some places where it is used.
In order to support old code, we have to support collections with interface values.
The generic `codec.CollValue` is not able to handle interface values, so we need to use a special type `codec.CollValueInterface`.
`codec.CollValueInterface` takes a `codec.BinaryCodec` as an argument, and uses it to marshal and unmarshal values as interfaces.
The `codec.CollValueInterface` lives in the `codec` package, whose import path is `github.com/cosmos/cosmos-sdk/codec`.
### Instantiating Collections with interface values
In order to instantiate a collection with interface values, we need to use `codec.CollValueInterface` instead of `codec.CollValue`.
```go
package example
import (
"cosmossdk.io/collections"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)
var AccountsPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
Accounts *collections.Map[sdk.AccAddress, sdk.AccountI]
}
func NewKeeper(cdc codec.BinaryCodec, storeKey *storetypes.KVStoreKey) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Accounts: collections.NewMap(
sb, AccountsPrefix, "accounts",
sdk.AccAddressKey, codec.CollInterfaceValue[sdk.AccountI](cdc),
),
}
}
func (k Keeper) SaveBaseAccount(ctx sdk.Context, account authtypes.BaseAccount) error {
return k.Accounts.Set(ctx, account.GetAddress(), account)
}
func (k Keeper) SaveModuleAccount(ctx sdk.Context, account authtypes.ModuleAccount) error {
return k.Accounts.Set(ctx, account.GetAddress(), account)
}
func (k Keeper) GetAccount(ctx sdk.context, addr sdk.AccAddress) (sdk.AccountI, error) {
return k.Accounts.Get(ctx, addr)
}
```