-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(collections): genesis support (#14331)
Co-authored-by: testinginprod <testinginprod@somewhere.idk> Co-authored-by: testinginprod <98415576+testinginprod@users.noreply.github.com> Co-authored-by: Marko <marbar3778@yahoo.com> Co-authored-by: Julien Robert <julien@rbrt.fr> Co-authored-by: Aleksandr Bezobchuk <alexanderbez@users.noreply.github.com> Co-authored-by: Rafael Tenfen <rafaeltenfen.rt@gmail.com> Co-authored-by: Facundo Medica <14063057+facundomedica@users.noreply.github.com> Co-authored-by: atheeshp <59333759+atheeshp@users.noreply.github.com> Co-authored-by: Likhita Polavarapu <78951027+likhita-809@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Matt Kocubinski <mkocubinski@gmail.com> Co-authored-by: Jacob Gadikian <faddat@users.noreply.github.com> Co-authored-by: samricotta <37125168+samricotta@users.noreply.github.com> Co-authored-by: Noel Ukwa <noeluchechukwu@gmail.com> Co-authored-by: JayT106 <JayT106@users.noreply.github.com> Co-authored-by: Julián Toledano <JulianToledano@users.noreply.github.com> Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com> Co-authored-by: Denver <aeharvlee@gmail.com> Co-authored-by: Hyunwoo Lee <denver@Hyunwoos-MacBook-Pro-2.local> Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> Co-authored-by: MalteHerrmann <42640438+MalteHerrmann@users.noreply.github.com> Co-authored-by: Vladislav Varadinov <vladislav.varadinov@gmail.com> Co-authored-by: cipherZ <dev@cipherz.com>
- Loading branch information
1 parent
afdbc51
commit b3c750c
Showing
17 changed files
with
689 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
package collections | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
) | ||
|
||
type genesisHandler interface { | ||
validateGenesis(r io.Reader) error | ||
importGenesis(ctx context.Context, r io.Reader) error | ||
exportGenesis(ctx context.Context, w io.Writer) error | ||
defaultGenesis(w io.Writer) error | ||
} | ||
|
||
type jsonMapEntry struct { | ||
Key json.RawMessage `json:"key"` | ||
Value json.RawMessage `json:"value,omitempty"` | ||
} | ||
|
||
func (m Map[K, V]) validateGenesis(reader io.Reader) 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.Set(ctx, key, value) | ||
}) | ||
} | ||
|
||
func (m Map[K, V]) exportGenesis(ctx context.Context, writer io.Writer) error { | ||
_, err := writer.Write([]byte("[")) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
it, err := m.Iterate(ctx, nil) | ||
if err != nil { | ||
return err | ||
} | ||
defer it.Close() | ||
|
||
first := true | ||
for ; it.Valid(); it.Next() { | ||
// add a comma before encoding the object | ||
// for all objects besides the first one. | ||
if !first { | ||
_, err = writer.Write([]byte(",")) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
first = false | ||
|
||
key, err := it.Key() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
keyBz, err := m.kc.EncodeJSON(key) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
value, err := it.Value() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
valueBz, err := m.vc.EncodeJSON(value) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
entry := jsonMapEntry{ | ||
Key: keyBz, | ||
Value: valueBz, | ||
} | ||
|
||
bz, err := json.Marshal(entry) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
_, err = writer.Write(bz) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
_, err = writer.Write([]byte("]")) | ||
return err | ||
} | ||
|
||
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 { | ||
return err | ||
} | ||
|
||
if token != json.Delim('[') { | ||
return fmt.Errorf("expected [ got %s", token) | ||
} | ||
|
||
for decoder.More() { | ||
var rawJson json.RawMessage | ||
err := decoder.Decode(&rawJson) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var mapEntry jsonMapEntry | ||
err = json.Unmarshal(rawJson, &mapEntry) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
key, err := m.kc.DecodeJSON(mapEntry.Key) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
value, err := m.vc.DecodeJSON(mapEntry.Value) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = onEntry(key, value) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
token, err = decoder.Token() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if token != json.Delim(']') { | ||
return fmt.Errorf("expected ] got %s", token) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (m Map[K, V]) defaultGenesis(writer io.Writer) error { | ||
_, err := writer.Write([]byte(`[]`)) | ||
return err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
package collections | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"io" | ||
"testing" | ||
|
||
"cosmossdk.io/core/appmodule" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestDefaultGenesis(t *testing.T) { | ||
f := initFixture(t) | ||
var writers []*bufCloser | ||
require.NoError(t, f.schema.DefaultGenesis(func(field string) (io.WriteCloser, error) { | ||
w := newBufCloser(t, "") | ||
writers = append(writers, w) | ||
return w, nil | ||
})) | ||
require.Len(t, writers, 4) | ||
require.Equal(t, `[]`, writers[0].Buffer.String()) | ||
require.Equal(t, `[]`, writers[1].Buffer.String()) | ||
require.Equal(t, `[]`, writers[2].Buffer.String()) | ||
require.Equal(t, `[]`, writers[3].Buffer.String()) | ||
} | ||
|
||
func TestValidateGenesis(t *testing.T) { | ||
f := initFixture(t) | ||
require.NoError(t, f.schema.ValidateGenesis(createTestGenesisSource(t))) | ||
} | ||
|
||
func TestImportGenesis(t *testing.T) { | ||
f := initFixture(t) | ||
require.NoError(t, f.schema.InitGenesis(f.ctx, createTestGenesisSource(t))) | ||
// assert map correct genesis | ||
mapIt, err := f.m.Iterate(f.ctx, nil) | ||
require.NoError(t, err) | ||
defer mapIt.Close() | ||
|
||
kvs, err := mapIt.KeyValues() | ||
require.NoError(t, err) | ||
require.Equal(t, KeyValue[string, uint64]{Key: "abc", Value: 1}, kvs[0]) | ||
require.Equal(t, KeyValue[string, uint64]{Key: "def", Value: 2}, kvs[1]) | ||
|
||
// assert item correct genesis | ||
x, err := f.i.Get(f.ctx) | ||
require.NoError(t, err) | ||
require.Equal(t, "superCoolItem", x) | ||
|
||
// assert keyset correct genesis | ||
ksIt, err := f.ks.Iterate(f.ctx, nil) | ||
require.NoError(t, err) | ||
defer ksIt.Close() | ||
|
||
keys, err := ksIt.Keys() | ||
require.NoError(t, err) | ||
require.Equal(t, []string{"0", "1", "2"}, keys) | ||
|
||
// assert sequence correct genesis | ||
seq, err := f.s.Peek(f.ctx) | ||
require.NoError(t, err) | ||
require.Equal(t, uint64(1000), seq) | ||
} | ||
|
||
func TestExportGenesis(t *testing.T) { | ||
f := initFixture(t) | ||
require.NoError(t, f.schema.InitGenesis(f.ctx, createTestGenesisSource(t))) | ||
|
||
var writers []*bufCloser | ||
require.NoError(t, f.schema.ExportGenesis(f.ctx, func(field string) (io.WriteCloser, error) { | ||
w := newBufCloser(t, "") | ||
writers = append(writers, w) | ||
return w, nil | ||
})) | ||
require.Len(t, writers, 4) | ||
require.Equal(t, expectedItemGenesis, writers[0].Buffer.String()) | ||
require.Equal(t, expectedKeySetGenesis, writers[1].Buffer.String()) | ||
require.Equal(t, expectedMapGenesis, writers[2].Buffer.String()) | ||
require.Equal(t, expectedSequenceGenesis, writers[3].Buffer.String()) | ||
} | ||
|
||
type testFixture struct { | ||
schema Schema | ||
ctx context.Context | ||
m Map[string, uint64] | ||
i Item[string] | ||
s Sequence | ||
ks KeySet[string] | ||
} | ||
|
||
func initFixture(t *testing.T) *testFixture { | ||
sk, ctx := deps() | ||
schemaBuilder := NewSchemaBuilder(sk) | ||
m := NewMap(schemaBuilder, NewPrefix(1), "map", StringKey, Uint64Value) | ||
i := NewItem(schemaBuilder, NewPrefix(2), "item", StringValue) | ||
s := NewSequence(schemaBuilder, NewPrefix(3), "sequence") | ||
ks := NewKeySet(schemaBuilder, NewPrefix(4), "key_set", StringKey) | ||
schema, err := schemaBuilder.Build() | ||
require.NoError(t, err) | ||
return &testFixture{ | ||
schema: schema, | ||
ctx: ctx, | ||
m: m, | ||
i: i, | ||
s: s, | ||
ks: ks, | ||
} | ||
} | ||
|
||
func createTestGenesisSource(t *testing.T) appmodule.GenesisSource { | ||
expectedOrder := []string{"item", "key_set", "map", "sequence"} | ||
currentIndex := 0 | ||
return func(field string) (io.ReadCloser, error) { | ||
require.Equal(t, expectedOrder[currentIndex], field, "unordered genesis") | ||
currentIndex++ | ||
|
||
switch field { | ||
case "map": | ||
return newBufCloser(t, expectedMapGenesis), nil | ||
case "item": | ||
return newBufCloser(t, expectedItemGenesis), nil | ||
case "key_set": | ||
return newBufCloser(t, expectedKeySetGenesis), nil | ||
case "sequence": | ||
return newBufCloser(t, expectedSequenceGenesis), nil | ||
default: | ||
return nil, nil | ||
} | ||
} | ||
} | ||
|
||
const ( | ||
expectedMapGenesis = `[{"key":"abc","value":"1"},{"key":"def","value":"2"}]` | ||
expectedItemGenesis = `[{"key":"item","value":"superCoolItem"}]` | ||
expectedKeySetGenesis = `[{"key":"0"},{"key":"1"},{"key":"2"}]` | ||
expectedSequenceGenesis = `[{"key":"item","value":"1000"}]` | ||
) | ||
|
||
type bufCloser struct { | ||
*bytes.Buffer | ||
closed bool | ||
} | ||
|
||
func (b *bufCloser) Close() error { | ||
b.closed = true | ||
return nil | ||
} | ||
|
||
func newBufCloser(t *testing.T, str string) *bufCloser { | ||
b := &bufCloser{ | ||
Buffer: bytes.NewBufferString(str), | ||
closed: false, | ||
} | ||
// this ensures Close was called by the implementation | ||
t.Cleanup(func() { | ||
require.True(t, b.closed) | ||
}) | ||
return b | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.