Skip to content

Commit

Permalink
feat(collections): IndexedMap (#14397)
Browse files Browse the repository at this point in the history
Co-authored-by: testinginprod <[email protected]>
Co-authored-by: Marko <[email protected]>
Co-authored-by: Likhita Polavarapu <[email protected]>
  • Loading branch information
4 people authored Jan 27, 2023
1 parent ed17f2d commit 519630e
Show file tree
Hide file tree
Showing 21 changed files with 891 additions and 51 deletions.
3 changes: 2 additions & 1 deletion collections/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [#14351](https://github.com/cosmos/cosmos-sdk/pull/14351) Add keyset
* [#14364](https://github.com/cosmos/cosmos-sdk/pull/14364) Add sequence
* [#14468](https://github.com/cosmos/cosmos-sdk/pull/14468) Add Map.IterateRaw API.
* [#14310](https://github.com/cosmos/cosmos-sdk/pull/14310) Add Pair keys
* [#14310](https://github.com/cosmos/cosmos-sdk/pull/14310) Add Pair keys
* [#14397](https://github.com/cosmos/cosmos-sdk/pull/14397) Add IndexedMap
2 changes: 2 additions & 0 deletions collections/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ var (
ErrNotFound = errors.New("collections: not found")
// ErrEncoding is returned when something fails during key or value encoding/decoding.
ErrEncoding = errors.New("collections: encoding error")
// ErrConflict is returned when there are conflicts, for example in UniqueIndex.
ErrConflict = errors.New("collections: conflict")
)

// collection is the interface that all collections support. It will eventually
Expand Down
1 change: 0 additions & 1 deletion collections/collections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ func (t testStore) Has(key []byte) (bool, error) {

func (t testStore) Set(key, value []byte) error {
return t.db.Set(key, value)

}

func (t testStore) Delete(key []byte) error {
Expand Down
106 changes: 105 additions & 1 deletion collections/colltest/codec.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package colltest

import (
"encoding/json"
"fmt"
"reflect"
"testing"

"cosmossdk.io/collections"

"github.com/stretchr/testify/require"
"testing"
)

// TestKeyCodec asserts the correct behaviour of a KeyCodec over the type T.
Expand All @@ -21,6 +26,7 @@ func TestKeyCodec[T any](t *testing.T, keyCodec collections.KeyCodec[T], key T)
pairKey := collections.Join(key, "TEST")
buffer = make([]byte, pairCodec.Size(pairKey))
written, err = pairCodec.Encode(buffer, pairKey)
require.Equal(t, len(buffer), written, "the pair buffer should have been fully written")
require.NoError(t, err)
read, decodedPairKey, err := pairCodec.Decode(buffer)
require.NoError(t, err)
Expand Down Expand Up @@ -53,3 +59,101 @@ func TestValueCodec[T any](t *testing.T, encoder collections.ValueCodec[T], valu

_ = encoder.Stringify(value)
}

// MockValueCodec returns a mock of collections.ValueCodec for type T, it
// can be used for collections Values testing. It also supports interfaces.
// For the interfaces cases, in order for an interface to be decoded it must
// have been encoded first. Not concurrency safe.
// EG:
// Let's say the value is interface Animal
// if I want to decode Dog which implements Animal, then I need to first encode
// it in order to make the type known by the MockValueCodec.
func MockValueCodec[T any]() collections.ValueCodec[T] {
typ := reflect.ValueOf(new(T)).Elem().Type()
isInterface := false
if typ.Kind() == reflect.Interface {
isInterface = true
}
return &mockValueCodec[T]{
isInterface: isInterface,
seenTypes: map[string]reflect.Type{},
valueType: fmt.Sprintf("%s.%s", typ.PkgPath(), typ.Name()),
}
}

type mockValueJSON struct {
TypeName string `json:"type_name"`
Value json.RawMessage `json:"value"`
}

type mockValueCodec[T any] struct {
isInterface bool
seenTypes map[string]reflect.Type
valueType string
}

func (m mockValueCodec[T]) Encode(value T) ([]byte, error) {
typeName := m.getTypeName(value)
valueBytes, err := json.Marshal(value)
if err != nil {
return nil, err
}

return json.Marshal(mockValueJSON{
TypeName: typeName,
Value: valueBytes,
})
}

func (m mockValueCodec[T]) Decode(b []byte) (t T, err error) {
wrappedValue := mockValueJSON{}
err = json.Unmarshal(b, &wrappedValue)
if err != nil {
return
}
if !m.isInterface {
err = json.Unmarshal(wrappedValue.Value, &t)
return t, err
}

typ, exists := m.seenTypes[wrappedValue.TypeName]
if !exists {
return t, fmt.Errorf("uknown type %s, you're dealing with interfaces... in order to make the interface types known for the MockValueCodec, you need to first encode them", wrappedValue.TypeName)
}

newT := reflect.New(typ).Interface()
err = json.Unmarshal(wrappedValue.Value, newT)
if err != nil {
return t, err
}

iface := new(T)
reflect.ValueOf(iface).Elem().Set(reflect.ValueOf(newT).Elem())
return *iface, nil
}

func (m mockValueCodec[T]) EncodeJSON(value T) ([]byte, error) {
return m.Encode(value)
}

func (m mockValueCodec[T]) DecodeJSON(b []byte) (T, error) {
return m.Decode(b)
}

func (m mockValueCodec[T]) Stringify(value T) string {
return fmt.Sprintf("%#v", value)
}

func (m mockValueCodec[T]) ValueType() string {
return m.valueType
}

func (m mockValueCodec[T]) getTypeName(value T) string {
if !m.isInterface {
return m.valueType
}
typ := reflect.TypeOf(value)
name := fmt.Sprintf("%s.%s", typ.PkgPath(), typ.Name())
m.seenTypes[name] = typ
return name
}
48 changes: 48 additions & 0 deletions collections/colltest/codec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package colltest

import "testing"

type animal interface {
name() string
}

type dog struct {
Name string `json:"name"`
BarksLoudly bool `json:"barks_loudly"`
}

type cat struct {
Name string `json:"name"`
Scratches bool `json:"scratches"`
}

func (d *cat) name() string { return d.Name }

func (d dog) name() string { return d.Name }

func TestMockValueCodec(t *testing.T) {
t.Run("primitive type", func(t *testing.T) {
x := MockValueCodec[string]()
TestValueCodec(t, x, "hello")
})

t.Run("struct type", func(t *testing.T) {
x := MockValueCodec[dog]()
TestValueCodec(t, x, dog{
Name: "kernel",
BarksLoudly: true,
})
})

t.Run("interface type", func(t *testing.T) {
x := MockValueCodec[animal]()
TestValueCodec[animal](t, x, dog{
Name: "kernel",
BarksLoudly: true,
})
TestValueCodec[animal](t, x, &cat{
Name: "echo",
Scratches: true,
})
})
}
49 changes: 49 additions & 0 deletions collections/colltest/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package colltest

import (
"context"

"cosmossdk.io/core/store"
db "github.com/cosmos/cosmos-db"
)

// MockStore returns a mock store.KVStoreService and a mock context.Context.
// They can be used to test collections.
func MockStore() (store.KVStoreService, context.Context) {
kv := db.NewMemDB()
return &testStore{kv}, context.Background()
}

type testStore struct {
db db.DB
}

func (t testStore) OpenKVStore(ctx context.Context) store.KVStore {
return t
}

func (t testStore) Get(key []byte) ([]byte, error) {
return t.db.Get(key)
}

func (t testStore) Has(key []byte) (bool, error) {
return t.db.Has(key)
}

func (t testStore) Set(key, value []byte) error {
return t.db.Set(key, value)
}

func (t testStore) Delete(key []byte) error {
return t.db.Delete(key)
}

func (t testStore) Iterator(start, end []byte) (store.Iterator, error) {
return t.db.Iterator(start, end)
}

func (t testStore) ReverseIterator(start, end []byte) (store.Iterator, error) {
return t.db.ReverseIterator(start, end)
}

var _ store.KVStore = testStore{}
3 changes: 2 additions & 1 deletion collections/correctness_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package collections_test

import (
"testing"

"cosmossdk.io/collections"
"cosmossdk.io/collections/colltest"
"testing"
)

func TestKeyCorrectness(t *testing.T) {
Expand Down
12 changes: 6 additions & 6 deletions collections/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ type jsonMapEntry struct {
}

func (m Map[K, V]) validateGenesis(reader io.Reader) error {
return m.doDecodeJson(reader, func(key K, value V) error {
return m.doDecodeJSON(reader, func(key K, value V) error {
return nil
})
}

func (m Map[K, V]) importGenesis(ctx context.Context, reader io.Reader) error {
return m.doDecodeJson(reader, func(key K, value V) error {
return m.doDecodeJSON(reader, func(key K, value V) error {
return m.Set(ctx, key, value)
})
}
Expand Down Expand Up @@ -95,7 +95,7 @@ func (m Map[K, V]) exportGenesis(ctx context.Context, writer io.Writer) error {
return err
}

func (m Map[K, V]) doDecodeJson(reader io.Reader, onEntry func(key K, value V) error) error {
func (m Map[K, V]) doDecodeJSON(reader io.Reader, onEntry func(key K, value V) error) error {
decoder := json.NewDecoder(reader)
token, err := decoder.Token()
if err != nil {
Expand All @@ -107,14 +107,14 @@ func (m Map[K, V]) doDecodeJson(reader io.Reader, onEntry func(key K, value V) e
}

for decoder.More() {
var rawJson json.RawMessage
err := decoder.Decode(&rawJson)
var rawJSON json.RawMessage
err := decoder.Decode(&rawJSON)
if err != nil {
return err
}

var mapEntry jsonMapEntry
err = json.Unmarshal(rawJson, &mapEntry)
err = json.Unmarshal(rawJSON, &mapEntry)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit 519630e

Please sign in to comment.