feat(collections): add basic Schema API (#14267)

This commit is contained in:
Aaron Craelius 2022-12-14 20:42:41 -05:00 committed by GitHub
parent b17c2d902e
commit fe34e5df32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 176 additions and 26 deletions

View File

@ -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 {

View File

@ -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) {

View File

@ -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)

View File

@ -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))

View File

@ -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.

View File

@ -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)

52
collections/schema.go Normal file
View File

@ -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 + "$")

View File

@ -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)
})
}