From b3c750ca27b74dc946447c4de86bcf2da19c579d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 17 Jan 2023 06:25:10 -0500 Subject: [PATCH] feat(collections): genesis support (#14331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: testinginprod Co-authored-by: testinginprod <98415576+testinginprod@users.noreply.github.com> Co-authored-by: Marko Co-authored-by: Julien Robert Co-authored-by: Aleksandr Bezobchuk Co-authored-by: Rafael Tenfen 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 Co-authored-by: Jacob Gadikian Co-authored-by: samricotta <37125168+samricotta@users.noreply.github.com> Co-authored-by: Noel Ukwa Co-authored-by: JayT106 Co-authored-by: Julián Toledano Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com> Co-authored-by: Denver Co-authored-by: Hyunwoo Lee 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 Co-authored-by: cipherZ --- collections/collections.go | 10 ++ collections/collections_test.go | 7 ++ collections/genesis.go | 153 ++++++++++++++++++++++++++++++ collections/genesis_test.go | 160 ++++++++++++++++++++++++++++++++ collections/go.mod | 8 ++ collections/go.sum | 30 +++++- collections/item.go | 22 +++-- collections/iter.go | 3 +- collections/keys.go | 19 ++++ collections/keys_test.go | 2 +- collections/keyset.go | 11 +++ collections/map.go | 13 +-- collections/pair.go | 36 +++++++ collections/pair_test.go | 6 ++ collections/schema.go | 159 ++++++++++++++++++++++++++++++- collections/values.go | 63 ++++++++++++- collections/values_test.go | 10 ++ 17 files changed, 689 insertions(+), 23 deletions(-) create mode 100644 collections/genesis.go create mode 100644 collections/genesis_test.go diff --git a/collections/collections.go b/collections/collections.go index e597722f73..84889a493c 100644 --- a/collections/collections.go +++ b/collections/collections.go @@ -22,6 +22,8 @@ type collection interface { // getPrefix is the unique prefix of the collection within a schema. getPrefix() []byte + + genesisHandler } // Prefix defines a segregation namespace @@ -85,6 +87,10 @@ type KeyCodec[T any] interface { // to return the maximum varint bytes buffer length, at the risk of // over-estimating in order to pick the most performant path. Size(key T) int + // EncodeJSON encodes the value as JSON. + EncodeJSON(value T) ([]byte, error) + // DecodeJSON decodes the provided JSON bytes into an instance of T. + DecodeJSON(b []byte) (T, error) // Stringify returns a string representation of T. Stringify(key T) string // KeyType returns a string identifier for the type of the key. @@ -117,6 +123,10 @@ type ValueCodec[T any] interface { Encode(value T) ([]byte, error) // Decode returns the type T given its binary representation. Decode(b []byte) (T, error) + // EncodeJSON encodes the value as JSON. + EncodeJSON(value T) ([]byte, error) + // DecodeJSON decodes the provided JSON bytes into an instance of T. + DecodeJSON(b []byte) (T, error) // Stringify returns a string representation of T. Stringify(value T) string // ValueType returns the identifier for the type. diff --git a/collections/collections_test.go b/collections/collections_test.go index aa0e567756..03b946a24f 100644 --- a/collections/collections_test.go +++ b/collections/collections_test.go @@ -92,6 +92,13 @@ func checkKeyCodec[T any](t *testing.T, keyCodec KeyCodec[T], key T) { require.NoError(t, err) require.Equal(t, len(buffer), read, "encoded non terminal key and pair key read bytes must have same size") require.Equal(t, pairKey, decodedPairKey, "encoding and decoding produces different keys with non terminal encoding") + + // check JSON + keyJSON, err := keyCodec.EncodeJSON(key) + require.NoError(t, err) + decoded, err := keyCodec.DecodeJSON(keyJSON) + require.NoError(t, err) + require.Equal(t, key, decoded, "json encoding and decoding did not produce the same results") } // checkValueCodec asserts the correct behaviour of a ValueCodec over the type T. diff --git a/collections/genesis.go b/collections/genesis.go new file mode 100644 index 0000000000..e03a766b29 --- /dev/null +++ b/collections/genesis.go @@ -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 +} diff --git a/collections/genesis_test.go b/collections/genesis_test.go new file mode 100644 index 0000000000..9ff95c83c1 --- /dev/null +++ b/collections/genesis_test.go @@ -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 +} diff --git a/collections/go.mod b/collections/go.mod index 6d3e2fb604..94c4e6719c 100644 --- a/collections/go.mod +++ b/collections/go.mod @@ -10,6 +10,8 @@ require ( ) require ( + cosmossdk.io/api v0.2.6 // indirect + cosmossdk.io/depinject v1.0.0-alpha.3 // indirect github.com/DataDog/zstd v1.4.5 // indirect github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect @@ -18,9 +20,11 @@ require ( github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677 // indirect github.com/cockroachdb/redact v1.0.8 // indirect github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -38,5 +42,9 @@ require ( golang.org/x/exp v0.0.0-20221019170559-20944726eadf // indirect golang.org/x/net v0.3.0 // indirect golang.org/x/sys v0.3.0 // indirect + golang.org/x/text v0.5.0 // indirect + google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect + google.golang.org/grpc v1.51.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/collections/go.sum b/collections/go.sum index 432eefd96a..db9434e505 100644 --- a/collections/go.sum +++ b/collections/go.sum @@ -1,6 +1,10 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cosmossdk.io/api v0.2.6 h1:AoNwaLLapcLsphhMK6+o0kZl+D6MMUaHVqSdwinASGU= +cosmossdk.io/api v0.2.6/go.mod h1:u/d+GAxil0nWpl1XnQL8nkziQDIWuBDhv8VnDm/s6dI= cosmossdk.io/core v0.4.1 h1:5icpjPmw8J4uFp6w8OoLJPxsw6X3kRi9nAtTr3qp2Ok= cosmossdk.io/core v0.4.1/go.mod h1:Zqd1GB+krTF3bzhTnPNwzy3NTtXu+MKLX/9sPQXTIDE= +cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw= +cosmossdk.io/depinject v1.0.0-alpha.3/go.mod h1:eRbcdQ7MRpIPEM5YUJh8k97nxHpYbc3sMUnEtt8HPWU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -16,6 +20,7 @@ github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4K github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/participle/v2 v2.0.0-alpha7 h1:cK4vjj0VSgb3lN1nuKA5F7dw+1s1pWBe5bx7nNCnN+c= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -24,6 +29,7 @@ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cb github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/apd/v3 v3.1.0 h1:MK3Ow7LH0W8zkd5GMKA1PvS9qG3bWFI95WaVNfyZJ/w= github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y= @@ -42,8 +48,12 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32 h1:zlCp9n3uwQieELltZWHRmwPmPaZ8+XoL2Sj+A2YJlr8= github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32/go.mod h1:kwMlEC4wWvB48zAShGKVqboJL6w4zCLesaNQ3YLU2BQ= +github.com/cosmos/cosmos-proto v1.0.0-beta.1 h1:iDL5qh++NoXxG8hSy93FdYJut4XfgbShIocllGaXx/0= +github.com/cosmos/cosmos-proto v1.0.0-beta.1/go.mod h1:8k2GNZghi5sDRFw/scPL8gMSowT1vDA+5ouxL8GjaUE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cucumber/common/gherkin/go/v22 v22.0.0 h1:4K8NqptbvdOrjL9DEea6HFjSpbdT9+Q5kgLpmmsHYl0= +github.com/cucumber/common/messages/go/v17 v17.1.1 h1:RNqopvIFyLWnKv0LfATh34SWBhXeoFTJnSrgm9cT/Ts= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -79,6 +89,7 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= @@ -98,6 +109,9 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -109,6 +123,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -177,7 +192,6 @@ github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOA github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -202,6 +216,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/regen-network/gocuke v0.6.2 h1:pHviZ0kKAq2U2hN2q3smKNxct6hS0mGByFMHGnWA97M= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= @@ -329,6 +344,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -360,22 +376,30 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 h1:a2S6M0+660BgMNl++4JPlcAO/CjkqYItDEZwkoDQK7c= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= @@ -389,6 +413,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +pgregory.net/rapid v0.5.3 h1:163N50IHFqr1phZens4FQOdPgfJscR7a562mjQqeo4M= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/collections/item.go b/collections/item.go index d1005b4109..144eb264f8 100644 --- a/collections/item.go +++ b/collections/item.go @@ -1,7 +1,9 @@ package collections import ( + "bytes" "context" + "fmt" ) // Item is a type declaration based on Map @@ -17,8 +19,7 @@ func NewItem[V any]( name string, valueCodec ValueCodec[V], ) Item[V] { - item := (Item[V])(newMap[noKey](schema, prefix, name, noKey{}, valueCodec)) - schema.addCollection(item) + item := (Item[V])(NewMap[noKey](schema, prefix, name, noKey{}, valueCodec)) return item } @@ -55,11 +56,18 @@ func (i Item[V]) Remove(ctx context.Context) error { // noKey defines a KeyCodec which decodes nothing. type noKey struct{} -func (noKey) Stringify(_ noKey) string { return "no_key" } -func (noKey) KeyType() string { return "no_key" } -func (noKey) Size(_ noKey) int { return 0 } -func (noKey) Encode(_ []byte, _ noKey) (int, error) { return 0, nil } -func (noKey) Decode(_ []byte) (int, noKey, error) { return 0, noKey{}, nil } +func (noKey) Stringify(_ noKey) string { return "no_key" } +func (noKey) KeyType() string { return "no_key" } +func (noKey) Size(_ noKey) int { return 0 } +func (noKey) Encode(_ []byte, _ noKey) (int, error) { return 0, nil } +func (noKey) Decode(_ []byte) (int, noKey, error) { return 0, noKey{}, nil } +func (noKey) EncodeJSON(_ noKey) ([]byte, error) { return []byte(`"item"`), nil } +func (noKey) DecodeJSON(b []byte) (noKey, error) { + if !bytes.Equal(b, []byte(`"item"`)) { + return noKey{}, fmt.Errorf("%w: invalid item json key bytes", ErrEncoding) + } + return noKey{}, nil +} func (k noKey) EncodeNonTerminal(_ []byte, _ noKey) (int, error) { panic("must not be called") } func (k noKey) DecodeNonTerminal(_ []byte) (int, noKey, error) { panic("must not be called") } func (k noKey) SizeNonTerminal(_ noKey) int { panic("must not be called") } diff --git a/collections/iter.go b/collections/iter.go index 29b4112d1f..bfde140864 100644 --- a/collections/iter.go +++ b/collections/iter.go @@ -2,9 +2,10 @@ package collections import ( "context" - "cosmossdk.io/core/store" "errors" "fmt" + + "cosmossdk.io/core/store" ) // ErrInvalidIterator is returned when an Iterate call resulted in an invalid iterator. diff --git a/collections/keys.go b/collections/keys.go index 2a47ab126d..3249cc63e7 100644 --- a/collections/keys.go +++ b/collections/keys.go @@ -3,6 +3,7 @@ package collections import ( "bytes" "encoding/binary" + "encoding/json" "errors" "fmt" "strconv" @@ -37,6 +38,14 @@ func (uint64Key) Decode(buffer []byte) (int, uint64, error) { return 8, binary.BigEndian.Uint64(buffer), nil } +func (uint64Key) EncodeJSON(value uint64) ([]byte, error) { + return uint64EncodeJSON(value) +} + +func (uint64Key) DecodeJSON(b []byte) (uint64, error) { + return uint64DecodeJSON(b) +} + func (uint64Key) Size(_ uint64) int { return 8 } func (u uint64Key) EncodeNonTerminal(buffer []byte, key uint64) (int, error) { @@ -69,6 +78,16 @@ func (stringKey) Decode(buffer []byte) (int, string, error) { return len(buffer), string(buffer), nil } +func (stringKey) EncodeJSON(value string) ([]byte, error) { + return json.Marshal(value) +} + +func (stringKey) DecodeJSON(b []byte) (string, error) { + var value string + err := json.Unmarshal(b, &value) + return value, err +} + func (stringKey) Size(key string) int { return len(key) } diff --git a/collections/keys_test.go b/collections/keys_test.go index 9dc804f647..698355490f 100644 --- a/collections/keys_test.go +++ b/collections/keys_test.go @@ -7,7 +7,7 @@ import ( ) func TestUint64Key(t *testing.T) { - t.Run("bijective", func(t *testing.T) { + t.Run("correctness", func(t *testing.T) { checkKeyCodec(t, Uint64Key, 55) }) diff --git a/collections/keyset.go b/collections/keyset.go index 40d096e082..9ca9c21a01 100644 --- a/collections/keyset.go +++ b/collections/keyset.go @@ -61,6 +61,17 @@ const noValueValueType = "no_value" type noValue struct{} +func (n noValue) EncodeJSON(_ noValue) ([]byte, error) { + return nil, nil +} + +func (n noValue) DecodeJSON(b []byte) (noValue, error) { + if b != nil { + return noValue{}, fmt.Errorf("%w: expected nil json bytes, got: %x", ErrEncoding, b) + } + return noValue{}, nil +} + func (noValue) Encode(_ noValue) ([]byte, error) { return []byte{}, nil } diff --git a/collections/map.go b/collections/map.go index 901621be40..6d53e2d2a7 100644 --- a/collections/map.go +++ b/collections/map.go @@ -30,22 +30,15 @@ func NewMap[K, V any]( keyCodec KeyCodec[K], valueCodec ValueCodec[V], ) Map[K, V] { - m := newMap(schemaBuilder, prefix, name, keyCodec, valueCodec) - schemaBuilder.addCollection(m) - return m -} - -func newMap[K, V any]( - schemaBuilder *SchemaBuilder, prefix Prefix, name string, - keyCodec KeyCodec[K], valueCodec ValueCodec[V], -) Map[K, V] { - return Map[K, V]{ + m := Map[K, V]{ kc: keyCodec, vc: valueCodec, sa: schemaBuilder.schema.storeAccessor, prefix: prefix.Bytes(), name: name, } + schemaBuilder.addCollection(m) + return m } func (m Map[K, V]) getName() string { diff --git a/collections/pair.go b/collections/pair.go index 98dfc6bd36..4bcd6637e8 100644 --- a/collections/pair.go +++ b/collections/pair.go @@ -1,6 +1,7 @@ package collections import ( + "encoding/json" "fmt" "strings" ) @@ -174,6 +175,41 @@ func (p pairKeyCodec[K1, K2]) SizeNonTerminal(key Pair[K1, K2]) int { return size } +// GENESIS + +type jsonPairKey [2]json.RawMessage + +func (p pairKeyCodec[K1, K2]) EncodeJSON(v Pair[K1, K2]) ([]byte, error) { + k1Json, err := p.keyCodec1.EncodeJSON(v.K1()) + if err != nil { + return nil, err + } + k2Json, err := p.keyCodec2.EncodeJSON(v.K2()) + if err != nil { + return nil, err + } + return json.Marshal(jsonPairKey{k1Json, k2Json}) +} + +func (p pairKeyCodec[K1, K2]) DecodeJSON(b []byte) (Pair[K1, K2], error) { + pairJSON := jsonPairKey{} + err := json.Unmarshal(b, &pairJSON) + if err != nil { + return Pair[K1, K2]{}, err + } + + k1, err := p.keyCodec1.DecodeJSON(pairJSON[0]) + if err != nil { + return Pair[K1, K2]{}, err + } + k2, err := p.keyCodec2.DecodeJSON(pairJSON[1]) + if err != nil { + return Pair[K1, K2]{}, err + } + + return Join(k1, k2), nil +} + // NewPrefixedPairRange creates a new PairRange which will prefix over all the keys // starting with the provided prefix. func NewPrefixedPairRange[K1, K2 any](prefix K1) *PairRange[K1, K2] { diff --git a/collections/pair_test.go b/collections/pair_test.go index cd53e9df3c..9abc3626ab 100644 --- a/collections/pair_test.go +++ b/collections/pair_test.go @@ -19,6 +19,12 @@ func TestPair(t *testing.T) { s = keyCodec.Stringify(Pair[string, string]{}) require.Equal(t, `(, )`, s) }) + + t.Run("json", func(t *testing.T) { + b, err := keyCodec.EncodeJSON(Join("k1", "k2")) + require.NoError(t, err) + require.Equal(t, []byte(`["k1","k2"]`), b) + }) } func TestPairRange(t *testing.T) { diff --git a/collections/schema.go b/collections/schema.go index 49c9553bce..e52ccb2439 100644 --- a/collections/schema.go +++ b/collections/schema.go @@ -4,8 +4,11 @@ import ( "context" "fmt" "regexp" + "sort" "strings" + "cosmossdk.io/core/appmodule" + "cosmossdk.io/core/store" "github.com/hashicorp/go-multierror" @@ -58,9 +61,17 @@ func (s *SchemaBuilder) Build() (Schema, error) { } } + // compute ordered collections + collectionsOrdered := make([]string, 0, len(s.schema.collectionsByName)) + for name := range s.schema.collectionsByName { + collectionsOrdered = append(collectionsOrdered, name) + } + sort.Strings(collectionsOrdered) + s.schema.collectionsOrdered = collectionsOrdered + if s.schema == nil { // explicit panic to avoid nil pointer dereference - panic("schema is nil") + panic("builder already used to construct a schema") } schema := *s.schema @@ -105,6 +116,7 @@ var nameRegex = regexp.MustCompile("^" + NameRegex + "$") // clients. type Schema struct { storeAccessor func(context.Context) store.KVStore + collectionsOrdered []string collectionsByPrefix map[string]collection collectionsByName map[string]collection } @@ -138,3 +150,148 @@ func NewSchemaFromAccessor(accessor func(context.Context) store.KVStore) Schema 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 +} + +// DefaultGenesis implements the appmodule.HasGenesis.DefaultGenesis method. +func (s Schema) DefaultGenesis(target appmodule.GenesisTarget) error { + for _, name := range s.collectionsOrdered { + err := s.defaultGenesis(target, name) + if err != nil { + return fmt.Errorf("failed to instantiate default genesis for %s: %w", name, err) + } + } + + return nil +} + +func (s Schema) defaultGenesis(target appmodule.GenesisTarget, name string) error { + wc, err := target(name) + if err != nil { + return err + } + defer wc.Close() + + coll, err := s.getCollection(name) + if err != nil { + return err + } + + return coll.defaultGenesis(wc) +} + +// ValidateGenesis implements the appmodule.HasGenesis.ValidateGenesis method. +func (s Schema) ValidateGenesis(source appmodule.GenesisSource) error { + for _, name := range s.collectionsOrdered { + err := s.validateGenesis(source, name) + if err != nil { + return fmt.Errorf("failed genesis validation of %s: %w", name, err) + } + } + return nil +} + +func (s Schema) validateGenesis(source appmodule.GenesisSource, name string) error { + rc, err := source(name) + if err != nil { + return err + } + defer rc.Close() + + coll, err := s.getCollection(name) + if err != nil { + return err + } + + err = coll.validateGenesis(rc) + if err != nil { + return err + } + + return nil +} + +// InitGenesis implements the appmodule.HasGenesis.InitGenesis method. +func (s Schema) InitGenesis(ctx context.Context, source appmodule.GenesisSource) error { + for _, name := range s.collectionsOrdered { + err := s.initGenesis(ctx, source, name) + if err != nil { + return fmt.Errorf("failed genesis initialisation of %s: %w", name, err) + } + } + + return nil +} + +func (s Schema) initGenesis(ctx context.Context, source appmodule.GenesisSource, name string) error { + rc, err := source(name) + if err != nil { + return err + } + defer rc.Close() + + coll, err := s.getCollection(name) + if err != nil { + return err + } + + err = coll.importGenesis(ctx, rc) + if err != nil { + return err + } + + return nil +} + +// ExportGenesis implements the appmodule.HasGenesis.ExportGenesis method. +func (s Schema) ExportGenesis(ctx context.Context, target appmodule.GenesisTarget) error { + for _, name := range s.collectionsOrdered { + err := s.exportGenesis(ctx, target, name) + if err != nil { + return fmt.Errorf("failed to export genesis for %s: %w", name, err) + } + } + + return nil +} + +func (s Schema) exportGenesis(ctx context.Context, target appmodule.GenesisTarget, name string) error { + wc, err := target(name) + if err != nil { + return err + } + defer wc.Close() + + coll, err := s.getCollection(name) + if err != nil { + return err + } + + return coll.exportGenesis(ctx, wc) +} + +func (s Schema) getCollection(name string) (collection, error) { + coll, ok := s.collectionsByName[name] + if !ok { + return nil, fmt.Errorf("unknown collection: %s", name) + } + return coll, nil +} diff --git a/collections/values.go b/collections/values.go index 0bc6ec913e..898df66a69 100644 --- a/collections/values.go +++ b/collections/values.go @@ -2,10 +2,18 @@ package collections import ( "encoding/binary" + "encoding/json" "fmt" + "strconv" ) -var Uint64Value ValueCodec[uint64] = uint64Value{} +var ( + // Uint64Value implements a ValueCodec for uint64. It converts the uint64 to big endian bytes. + // The JSON representation is the string format of uint64. + Uint64Value ValueCodec[uint64] = uint64Value{} + // StringValue implements a ValueCodec for string. + StringValue ValueCodec[string] = stringValue{} +) type uint64Value struct{} @@ -27,3 +35,56 @@ func (u uint64Value) Stringify(value uint64) string { func (u uint64Value) ValueType() string { return Uint64Key.KeyType() } + +func (u uint64Value) EncodeJSON(value uint64) ([]byte, error) { + return uint64EncodeJSON(value) +} + +func (u uint64Value) DecodeJSON(b []byte) (uint64, error) { + return uint64DecodeJSON(b) +} + +func uint64EncodeJSON(value uint64) ([]byte, error) { + str := `"` + strconv.FormatUint(value, 10) + `"` + return []byte(str), nil +} + +func uint64DecodeJSON(b []byte) (uint64, error) { + var str string + err := json.Unmarshal(b, &str) + if err != nil { + return 0, err + } + return strconv.ParseUint(str, 10, 64) +} + +type stringValue struct{} + +func (stringValue) Encode(value string) ([]byte, error) { + return []byte(value), nil +} + +func (stringValue) Decode(b []byte) (string, error) { + return string(b), nil +} + +func (stringValue) EncodeJSON(value string) ([]byte, error) { + return json.Marshal(value) +} + +func (stringValue) DecodeJSON(b []byte) (string, error) { + var s string + err := json.Unmarshal(b, &s) + if err != nil { + return "", err + } + return s, nil +} + +func (stringValue) Stringify(value string) string { + return value +} + +func (stringValue) ValueType() string { + return "string" +} diff --git a/collections/values_test.go b/collections/values_test.go index 3a63dd4234..b27d8e8d2f 100644 --- a/collections/values_test.go +++ b/collections/values_test.go @@ -16,3 +16,13 @@ func TestUint64Value(t *testing.T) { require.ErrorIs(t, err, ErrEncoding) }) } + +func TestUInt64JSON(t *testing.T) { + var x uint64 = 3076 + bz, err := uint64EncodeJSON(x) + require.NoError(t, err) + require.Equal(t, []byte(`"3076"`), bz) + y, err := uint64DecodeJSON(bz) + require.NoError(t, err) + require.Equal(t, x, y) +}