From 295c261b18c0332f78be881da0f2b275c0fd0ff1 Mon Sep 17 00:00:00 2001 From: testinginprod <98415576+testinginprod@users.noreply.github.com> Date: Mon, 24 Apr 2023 10:34:42 +0200 Subject: [PATCH] feat(codec): Add collections value codec for interfaces. (#15898) Co-authored-by: unknown unknown --- codec/collections.go | 47 ++++++++++++++++++++++++++++++++++ codec/collections_test.go | 29 ++++++++++++++++----- collections/README.md | 54 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 6 deletions(-) diff --git a/codec/collections.go b/codec/collections.go index f34b0dcb17..2904475c8b 100644 --- a/codec/collections.go +++ b/codec/collections.go @@ -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) +} diff --git a/codec/collections_test.go b/codec/collections_test.go index 6352738933..9e8ef18afa 100644 --- a/codec/collections_test.go +++ b/codec/collections_test.go @@ -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) + }) + }) } diff --git a/collections/README.md b/collections/README.md index 19884abe94..7f8278233c 100644 --- a/collections/README.md +++ b/collections/README.md @@ -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) +} +```