From fe34e5df3257e458dcfb3a1d634d812437fed553 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 14 Dec 2022 20:42:41 -0500 Subject: [PATCH] feat(collections): add basic Schema API (#14267) --- collections/collections.go | 12 +++++++++ collections/item.go | 29 ++++++++++++++++----- collections/item_test.go | 3 ++- collections/iter_test.go | 12 ++++++--- collections/map.go | 50 ++++++++++++++++++++++++++---------- collections/map_test.go | 3 ++- collections/schema.go | 52 ++++++++++++++++++++++++++++++++++++++ collections/schema_test.go | 41 ++++++++++++++++++++++++++++++ 8 files changed, 176 insertions(+), 26 deletions(-) create mode 100644 collections/schema.go create mode 100644 collections/schema_test.go diff --git a/collections/collections.go b/collections/collections.go index cd52b26ded..793e46bc38 100644 --- a/collections/collections.go +++ b/collections/collections.go @@ -23,6 +23,18 @@ type StorageProvider interface { KVStore(key storetypes.StoreKey) storetypes.KVStore } +// collection is the interface that all collections support. It will eventually +// include methods for importing/exporting genesis data and schema +// reflection for clients. +type collection interface { + // getName is the unique name of the collection within a schema. It must + // match format specified by NameRegex. + getName() string + + // getPrefix is the unique prefix of the collection within a schema. + getPrefix() []byte +} + // Prefix defines a segregation namespace // for specific collections objects. type Prefix struct { diff --git a/collections/item.go b/collections/item.go index 5a6e159100..4fc0ecaf40 100644 --- a/collections/item.go +++ b/collections/item.go @@ -2,19 +2,34 @@ package collections import ( "context" - - storetypes "github.com/cosmos/cosmos-sdk/store/types" ) -// NewItem instantiates a new Item instance, given the value encoder of the item V. -func NewItem[V any](sk storetypes.StoreKey, prefix Prefix, valueCodec ValueCodec[V]) Item[V] { - return (Item[V])(NewMap[noKey, V](sk, prefix, noKey{}, valueCodec)) -} - // Item is a type declaration based on Map // with a non-existent key. type Item[V any] Map[noKey, V] +// NewItem instantiates a new Item instance, given the value encoder of the item V. +// Name and prefix must be unique within the schema and name must match the format specified by NameRegex, or +// else this method will panic. +func NewItem[V any]( + schema Schema, + prefix Prefix, + name string, + valueCodec ValueCodec[V], +) Item[V] { + item := (Item[V])(newMap[noKey, V](schema, prefix, name, noKey{}, valueCodec)) + schema.addCollection(item) + return item +} + +func (i Item[V]) getName() string { + return i.name +} + +func (i Item[V]) getPrefix() []byte { + return i.prefix +} + // Get gets the item, if it is not set it returns an ErrNotFound error. // If value decoding fails then an ErrEncoding is returned. func (i Item[V]) Get(ctx context.Context) (V, error) { diff --git a/collections/item_test.go b/collections/item_test.go index 684136aa5d..8d439f3de8 100644 --- a/collections/item_test.go +++ b/collections/item_test.go @@ -8,7 +8,8 @@ import ( func TestItem(t *testing.T) { sk, ctx := deps() - item := NewItem(sk, NewPrefix("item"), Uint64Value) + schema := NewSchema(sk) + item := NewItem(schema, NewPrefix("item"), "item", Uint64Value) // set err := item.Set(ctx, 1000) require.NoError(t, err) diff --git a/collections/iter_test.go b/collections/iter_test.go index 9dfc3e3f0c..3434090a5d 100644 --- a/collections/iter_test.go +++ b/collections/iter_test.go @@ -8,7 +8,8 @@ import ( func TestIteratorBasic(t *testing.T) { sk, ctx := deps() - m := NewMap(sk, NewPrefix("some super amazing prefix"), StringKey, Uint64Value) + schema := NewSchema(sk) + m := NewMap(schema, NewPrefix("some super amazing prefix"), "m", StringKey, Uint64Value) for i := uint64(1); i <= 2; i++ { require.NoError(t, m.Set(ctx, fmt.Sprintf("%d", i), i)) @@ -54,7 +55,8 @@ func TestIteratorBasic(t *testing.T) { func TestIteratorKeyValues(t *testing.T) { sk, ctx := deps() - m := NewMap(sk, NewPrefix("some super amazing prefix"), StringKey, Uint64Value) + schema := NewSchema(sk) + m := NewMap(schema, NewPrefix("some super amazing prefix"), "m", StringKey, Uint64Value) for i := uint64(0); i <= 5; i++ { require.NoError(t, m.Set(ctx, fmt.Sprintf("%d", i), i)) @@ -100,7 +102,8 @@ func TestIteratorKeyValues(t *testing.T) { func TestIteratorPrefixing(t *testing.T) { sk, ctx := deps() - m := NewMap(sk, NewPrefix("cool"), StringKey, Uint64Value) + schema := NewSchema(sk) + m := NewMap(schema, NewPrefix("cool"), "cool", StringKey, Uint64Value) require.NoError(t, m.Set(ctx, "A1", 11)) require.NoError(t, m.Set(ctx, "A2", 12)) @@ -115,7 +118,8 @@ func TestIteratorPrefixing(t *testing.T) { func TestIteratorRanging(t *testing.T) { sk, ctx := deps() - m := NewMap(sk, NewPrefix("cool"), Uint64Key, Uint64Value) + schema := NewSchema(sk) + m := NewMap(schema, NewPrefix("cool"), "cool", Uint64Key, Uint64Value) for i := uint64(0); i <= 7; i++ { require.NoError(t, m.Set(ctx, i, i)) diff --git a/collections/map.go b/collections/map.go index 22280c0fe3..9aac57c304 100644 --- a/collections/map.go +++ b/collections/map.go @@ -7,19 +7,6 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" ) -// NewMap returns a Map given a StoreKey, a Prefix and the relative value and key encoders. -func NewMap[K, V any]( - sk storetypes.StoreKey, prefix Prefix, - keyCodec KeyCodec[K], valueCodec ValueCodec[V], -) Map[K, V] { - return Map[K, V]{ - kc: keyCodec, - vc: valueCodec, - sk: sk, - prefix: prefix.Bytes(), - } -} - // Map represents the basic collections object. // It is used to map arbitrary keys to arbitrary // objects. @@ -29,6 +16,43 @@ type Map[K, V any] struct { sk storetypes.StoreKey prefix []byte + name string +} + +// NewMap returns a Map given a StoreKey, a Prefix, human-readable name and the relative value and key encoders. +// Name and prefix must be unique within the schema and name must match the format specified by NameRegex, or +// else this method will panic. +func NewMap[K, V any]( + schema Schema, + prefix Prefix, + name string, + keyCodec KeyCodec[K], + valueCodec ValueCodec[V], +) Map[K, V] { + m := newMap(schema, prefix, name, keyCodec, valueCodec) + schema.addCollection(m) + return m +} + +func newMap[K, V any]( + schema Schema, prefix Prefix, name string, + keyCodec KeyCodec[K], valueCodec ValueCodec[V], +) Map[K, V] { + return Map[K, V]{ + kc: keyCodec, + vc: valueCodec, + sk: schema.storeKey, + prefix: prefix.Bytes(), + name: name, + } +} + +func (m Map[K, V]) getName() string { + return m.name +} + +func (m Map[K, V]) getPrefix() []byte { + return m.prefix } // Set maps the provided value to the provided key in the store. diff --git a/collections/map_test.go b/collections/map_test.go index 3002f11c5a..2db6e71555 100644 --- a/collections/map_test.go +++ b/collections/map_test.go @@ -8,7 +8,8 @@ import ( func TestMap(t *testing.T) { sk, ctx := deps() - m := NewMap(sk, NewPrefix("hi"), Uint64Key, Uint64Value) + schema := NewSchema(sk) + m := NewMap(schema, NewPrefix("hi"), "m", Uint64Key, Uint64Value) // test not has has, err := m.Has(ctx, 1) diff --git a/collections/schema.go b/collections/schema.go new file mode 100644 index 0000000000..b8aca2c04b --- /dev/null +++ b/collections/schema.go @@ -0,0 +1,52 @@ +package collections + +import ( + "fmt" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "regexp" +) + +// Schema specifies a group of collections stored within the storage specified +// by a single store key. All the collections within the schema must have a +// unique binary prefix and human-readable name. Schema will eventually include +// methods for importing/exporting genesis data and for schema reflection for +// clients. +type Schema struct { + storeKey storetypes.StoreKey + collectionsByPrefix map[string]collection + collectionsByName map[string]collection +} + +// NewSchema creates a new schema from the provided store key. +func NewSchema(storeKey storetypes.StoreKey) Schema { + return Schema{ + storeKey: storeKey, + collectionsByName: map[string]collection{}, + collectionsByPrefix: map[string]collection{}, + } +} + +func (s Schema) addCollection(collection collection) { + prefix := collection.getPrefix() + name := collection.getName() + + if _, ok := s.collectionsByPrefix[string(prefix)]; ok { + panic(fmt.Errorf("prefix %v already taken within schema", prefix)) + } + + if _, ok := s.collectionsByName[name]; ok { + panic(fmt.Errorf("name %s already taken within schema", name)) + } + + if !nameRegex.MatchString(name) { + panic(fmt.Errorf("name must match regex %s, got %s", NameRegex, name)) + } + + s.collectionsByPrefix[string(prefix)] = collection + s.collectionsByName[name] = collection +} + +// NameRegex is the regular expression that all valid collection names must match. +const NameRegex = "[A-Za-z][A-Za-z0-9_]*" + +var nameRegex = regexp.MustCompile("^" + NameRegex + "$") diff --git a/collections/schema_test.go b/collections/schema_test.go new file mode 100644 index 0000000000..5831df7890 --- /dev/null +++ b/collections/schema_test.go @@ -0,0 +1,41 @@ +package collections + +import ( + storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/stretchr/testify/require" + "testing" +) + +func TestNameRegex(t *testing.T) { + require.Regexp(t, nameRegex, "a") + require.Regexp(t, nameRegex, "ABC") + require.Regexp(t, nameRegex, "foo1_xyz") + require.NotRegexp(t, nameRegex, "1foo") + require.NotRegexp(t, nameRegex, "_bar") + require.NotRegexp(t, nameRegex, "abc-xyz") +} + +func TestAddCollection(t *testing.T) { + require.NotPanics(t, func() { + schema := NewSchema(storetypes.NewKVStoreKey("test")) + NewMap(schema, NewPrefix(1), "abc", Uint64Key, Uint64Value) + NewMap(schema, NewPrefix(2), "def", Uint64Key, Uint64Value) + }) + + require.PanicsWithError(t, "name must match regex [A-Za-z][A-Za-z0-9_]*, got 123", func() { + schema := NewSchema(storetypes.NewKVStoreKey("test")) + NewMap(schema, NewPrefix(1), "123", Uint64Key, Uint64Value) + }) + + require.PanicsWithError(t, "prefix [1] already taken within schema", func() { + schema := NewSchema(storetypes.NewKVStoreKey("test")) + NewMap(schema, NewPrefix(1), "abc", Uint64Key, Uint64Value) + NewMap(schema, NewPrefix(1), "def", Uint64Key, Uint64Value) + }) + + require.PanicsWithError(t, "name abc already taken within schema", func() { + schema := NewSchema(storetypes.NewKVStoreKey("test")) + NewMap(schema, NewPrefix(1), "abc", Uint64Key, Uint64Value) + NewMap(schema, NewPrefix(2), "abc", Uint64Key, Uint64Value) + }) +}