feat(collections): add basic Schema API (#14267)
This commit is contained in:
parent
b17c2d902e
commit
fe34e5df32
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
52
collections/schema.go
Normal 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 + "$")
|
||||
41
collections/schema_test.go
Normal file
41
collections/schema_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user