Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(collections): add alternative value codec #16773

Merged
merged 7 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
add alternative value codec
  • Loading branch information
unknown unknown committed Jun 27, 2023
commit 8793088e93db91b2f47e6f273e9693c826eea4d1
47 changes: 47 additions & 0 deletions collections/codec/alternative_value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package codec

// NewAltValueCodec returns a new AltValueCodec. canonicalValueCodec is the codec that you want the value
// to be encoded and decoded as, alternativeDecoder is a function that will attempt to decode the value
// in case the canonicalValueCodec fails to decode it.
func NewAltValueCodec[V any](canonicalValueCodec ValueCodec[V], alternativeDecoder func([]byte) (V, error)) ValueCodec[V] {
return AltValueCodec[V]{
canonicalValueCodec: canonicalValueCodec,
alternativeDecoder: alternativeDecoder,
}
}

// AltValueCodec is a codec that can decode a value from state in an alternative format.
// This is useful for migrating data from one format to another. For example, in x/bank
// balances were initially encoded as sdk.Coin, now they are encoded as math.Int.
// The AltValueCodec will be trying to decode the value as math.Int, and if that fails,
// it will attempt to decode it as sdk.Coin.
// NOTE: if the canonical format can also decode the alternative format, then this codec
// will produce undefined and undesirable behaviour.
type AltValueCodec[V any] struct {
canonicalValueCodec ValueCodec[V]
alternativeDecoder func([]byte) (V, error)
}

// Decode will attempt to decode the value from state using the canonical value codec.
// If it fails to decode, it will attempt to decode the value using the alternative decoder.
func (a AltValueCodec[V]) Decode(b []byte) (V, error) {
v, err := a.canonicalValueCodec.Decode(b)
if err != nil {
return a.alternativeDecoder(b)
}
return v, nil
}

// Below there is the implementation of ValueCodec relying on the canonical value codec.

func (a AltValueCodec[V]) Encode(value V) ([]byte, error) { return a.canonicalValueCodec.Encode(value) }

func (a AltValueCodec[V]) EncodeJSON(value V) ([]byte, error) {
return a.canonicalValueCodec.EncodeJSON(value)
}

func (a AltValueCodec[V]) DecodeJSON(b []byte) (V, error) { return a.canonicalValueCodec.DecodeJSON(b) }

func (a AltValueCodec[V]) Stringify(value V) string { return a.canonicalValueCodec.Stringify(value) }

func (a AltValueCodec[V]) ValueType() string { return a.canonicalValueCodec.ValueType() }
52 changes: 52 additions & 0 deletions collections/codec/alternative_value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package codec_test

import (
"encoding/json"
"testing"

"cosmossdk.io/collections/codec"
"cosmossdk.io/collections/colltest"
"github.com/stretchr/testify/require"
)

type altValue struct {
Value uint64 `json:"value"`
}

func TestAltValueCodec(t *testing.T) {
// we assume we want to migrate the value from json(altValue) to just be
// the raw value uint64.
canonical := codec.KeyToValueCodec(codec.NewUint64Key[uint64]())
alternative := func(v []byte) (uint64, error) {
var alt altValue
err := json.Unmarshal(v, &alt)
if err != nil {
return 0, err
}
return alt.Value, nil
}

cdc := codec.NewAltValueCodec(canonical, alternative)

t.Run("decodes alternative value", func(t *testing.T) {
expected := uint64(100)
alternativeEncodedBytes, err := json.Marshal(altValue{Value: expected})
require.NoError(t, err)
got, err := cdc.Decode(alternativeEncodedBytes)
require.NoError(t, err)
require.Equal(t, expected, got)
})

t.Run("decodes canonical value", func(t *testing.T) {
expected := uint64(100)
canonicalEncodedBytes, err := cdc.Encode(expected)
require.NoError(t, err)
got, err := cdc.Decode(canonicalEncodedBytes)
require.NoError(t, err)
require.Equal(t, expected, got)
})

t.Run("conformance", func(t *testing.T) {
colltest.TestValueCodec(t, cdc, uint64(100))
})
}