Add amino compatibility layer for proto Any (#6151)
* WIP on Any amino compatibility layer * Add tests & JSON * Refactor Marshal/UnmarshalAny * remove extra test * Add support for nested Any's * Add docs * Update codec/any_test.go Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>
This commit is contained in:
co-authored by
Federico Kunze
parent
f3e3a30e5e
commit
9d022c17b7
@@ -0,0 +1,140 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
amino "github.com/tendermint/go-amino"
|
||||
)
|
||||
|
||||
type aminoCompat struct {
|
||||
bz []byte
|
||||
jsonBz []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (any Any) MarshalAmino() ([]byte, error) {
|
||||
ac := any.aminoCompat
|
||||
if ac == nil {
|
||||
return nil, fmt.Errorf("can't amino unmarshal")
|
||||
}
|
||||
return ac.bz, ac.err
|
||||
}
|
||||
|
||||
func (any *Any) UnmarshalAmino(bz []byte) error {
|
||||
any.aminoCompat = &aminoCompat{
|
||||
bz: bz,
|
||||
err: nil,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (any Any) MarshalJSON() ([]byte, error) {
|
||||
ac := any.aminoCompat
|
||||
if ac == nil {
|
||||
return nil, fmt.Errorf("can't JSON marshal")
|
||||
}
|
||||
return ac.jsonBz, ac.err
|
||||
}
|
||||
|
||||
func (any *Any) UnmarshalJSON(bz []byte) error {
|
||||
any.aminoCompat = &aminoCompat{
|
||||
jsonBz: bz,
|
||||
err: nil,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AminoUnpacker is an AnyUnpacker provided for backwards compatibility with
|
||||
// amino for the binary un-marshaling phase
|
||||
type AminoUnpacker struct {
|
||||
Cdc *amino.Codec
|
||||
}
|
||||
|
||||
var _ AnyUnpacker = AminoUnpacker{}
|
||||
|
||||
func (a AminoUnpacker) UnpackAny(any *Any, iface interface{}) error {
|
||||
ac := any.aminoCompat
|
||||
if ac == nil {
|
||||
return fmt.Errorf("can't amino unmarshal %T", iface)
|
||||
}
|
||||
err := a.Cdc.UnmarshalBinaryBare(ac.bz, iface)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val := reflect.ValueOf(iface).Elem().Interface()
|
||||
err = UnpackInterfaces(val, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
any.cachedValue = val
|
||||
return nil
|
||||
}
|
||||
|
||||
// AminoUnpacker is an AnyUnpacker provided for backwards compatibility with
|
||||
// amino for the binary marshaling phase
|
||||
type AminoPacker struct {
|
||||
Cdc *amino.Codec
|
||||
}
|
||||
|
||||
var _ AnyUnpacker = AminoPacker{}
|
||||
|
||||
func (a AminoPacker) UnpackAny(any *Any, _ interface{}) error {
|
||||
err := UnpackInterfaces(any.cachedValue, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bz, err := a.Cdc.MarshalBinaryBare(any.cachedValue)
|
||||
any.aminoCompat = &aminoCompat{
|
||||
bz: bz,
|
||||
err: err,
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// AminoUnpacker is an AnyUnpacker provided for backwards compatibility with
|
||||
// amino for the JSON marshaling phase
|
||||
type AminoJSONUnpacker struct {
|
||||
Cdc *amino.Codec
|
||||
}
|
||||
|
||||
var _ AnyUnpacker = AminoJSONUnpacker{}
|
||||
|
||||
func (a AminoJSONUnpacker) UnpackAny(any *Any, iface interface{}) error {
|
||||
ac := any.aminoCompat
|
||||
if ac == nil {
|
||||
return fmt.Errorf("can't amino unmarshal %T", iface)
|
||||
}
|
||||
err := a.Cdc.UnmarshalJSON(ac.jsonBz, iface)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val := reflect.ValueOf(iface).Elem().Interface()
|
||||
err = UnpackInterfaces(val, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
any.cachedValue = val
|
||||
return nil
|
||||
}
|
||||
|
||||
// AminoUnpacker is an AnyUnpacker provided for backwards compatibility with
|
||||
// amino for the JSON un-marshaling phase
|
||||
type AminoJSONPacker struct {
|
||||
Cdc *amino.Codec
|
||||
}
|
||||
|
||||
var _ AnyUnpacker = AminoJSONPacker{}
|
||||
|
||||
func (a AminoJSONPacker) UnpackAny(any *Any, _ interface{}) error {
|
||||
err := UnpackInterfaces(any.cachedValue, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bz, err := a.Cdc.MarshalJSON(any.cachedValue)
|
||||
any.aminoCompat = &aminoCompat{
|
||||
jsonBz: bz,
|
||||
err: err,
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
amino "github.com/tendermint/go-amino"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec/testdata"
|
||||
"github.com/cosmos/cosmos-sdk/codec/types"
|
||||
)
|
||||
|
||||
type TypeWithInterface struct {
|
||||
Animal testdata.Animal `json:"animal"`
|
||||
X int64 `json:"x,omitempty"`
|
||||
}
|
||||
|
||||
type Suite struct {
|
||||
suite.Suite
|
||||
cdc *amino.Codec
|
||||
a TypeWithInterface
|
||||
b testdata.HasAnimal
|
||||
spot *testdata.Dog
|
||||
}
|
||||
|
||||
func (s *Suite) SetupTest() {
|
||||
s.cdc = amino.NewCodec()
|
||||
s.cdc.RegisterInterface((*testdata.Animal)(nil), nil)
|
||||
s.cdc.RegisterConcrete(&testdata.Dog{}, "testdata/Dob", nil)
|
||||
|
||||
s.spot = &testdata.Dog{Size_: "small", Name: "Spot"}
|
||||
s.a = TypeWithInterface{Animal: s.spot}
|
||||
|
||||
any, err := types.NewAnyWithValue(s.spot)
|
||||
s.Require().NoError(err)
|
||||
s.b = testdata.HasAnimal{Animal: any}
|
||||
}
|
||||
|
||||
func (s *Suite) TestAminoBinary() {
|
||||
bz, err := s.cdc.MarshalBinaryBare(s.a)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// expect plain amino marshal to fail
|
||||
_, err = s.cdc.MarshalBinaryBare(s.b)
|
||||
s.Require().Error(err)
|
||||
|
||||
// expect unpack interfaces before amino marshal to succeed
|
||||
err = types.UnpackInterfaces(s.b, types.AminoPacker{Cdc: s.cdc})
|
||||
s.Require().NoError(err)
|
||||
bz2, err := s.cdc.MarshalBinaryBare(s.b)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(bz, bz2)
|
||||
|
||||
var c testdata.HasAnimal
|
||||
err = s.cdc.UnmarshalBinaryBare(bz, &c)
|
||||
s.Require().NoError(err)
|
||||
err = types.UnpackInterfaces(c, types.AminoUnpacker{Cdc: s.cdc})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(s.spot, c.Animal.GetCachedValue())
|
||||
}
|
||||
|
||||
func (s *Suite) TestAminoJSON() {
|
||||
bz, err := s.cdc.MarshalJSON(s.a)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// expect plain amino marshal to fail
|
||||
_, err = s.cdc.MarshalJSON(s.b)
|
||||
s.Require().Error(err)
|
||||
|
||||
// expect unpack interfaces before amino marshal to succeed
|
||||
err = types.UnpackInterfaces(s.b, types.AminoJSONPacker{Cdc: s.cdc})
|
||||
s.Require().NoError(err)
|
||||
bz2, err := s.cdc.MarshalJSON(s.b)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(string(bz), string(bz2))
|
||||
|
||||
var c testdata.HasAnimal
|
||||
err = s.cdc.UnmarshalJSON(bz, &c)
|
||||
s.Require().NoError(err)
|
||||
err = types.UnpackInterfaces(c, types.AminoJSONUnpacker{Cdc: s.cdc})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(s.spot, c.Animal.GetCachedValue())
|
||||
}
|
||||
|
||||
func (s *Suite) TestNested() {
|
||||
s.cdc.RegisterInterface((*testdata.HasAnimalI)(nil), nil)
|
||||
s.cdc.RegisterInterface((*testdata.HasHasAnimalI)(nil), nil)
|
||||
s.cdc.RegisterConcrete(&testdata.HasAnimal{}, "testdata/HasAnimal", nil)
|
||||
s.cdc.RegisterConcrete(&testdata.HasHasAnimal{}, "testdata/HasHasAnimal", nil)
|
||||
s.cdc.RegisterConcrete(&testdata.HasHasHasAnimal{}, "testdata/HasHasHasAnimal", nil)
|
||||
|
||||
any, err := types.NewAnyWithValue(&s.b)
|
||||
s.Require().NoError(err)
|
||||
hha := testdata.HasHasAnimal{HasAnimal: any}
|
||||
any2, err := types.NewAnyWithValue(&hha)
|
||||
s.Require().NoError(err)
|
||||
hhha := testdata.HasHasHasAnimal{HasHasAnimal: any2}
|
||||
|
||||
// marshal
|
||||
err = types.UnpackInterfaces(hhha, types.AminoPacker{Cdc: s.cdc})
|
||||
s.Require().NoError(err)
|
||||
bz, err := s.cdc.MarshalBinaryBare(hhha)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// unmarshal
|
||||
var hhha2 testdata.HasHasHasAnimal
|
||||
err = s.cdc.UnmarshalBinaryBare(bz, &hhha2)
|
||||
s.Require().NoError(err)
|
||||
err = types.UnpackInterfaces(hhha2, types.AminoUnpacker{Cdc: s.cdc})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Equal(s.spot, hhha2.TheHasHasAnimal().TheHasAnimal().TheAnimal())
|
||||
|
||||
// json marshal
|
||||
err = types.UnpackInterfaces(hhha, types.AminoJSONPacker{Cdc: s.cdc})
|
||||
s.Require().NoError(err)
|
||||
jsonBz, err := s.cdc.MarshalJSON(hhha)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// json unmarshal
|
||||
var hhha3 testdata.HasHasHasAnimal
|
||||
err = s.cdc.UnmarshalJSON(jsonBz, &hhha3)
|
||||
s.Require().NoError(err)
|
||||
err = types.UnpackInterfaces(hhha3, types.AminoJSONUnpacker{Cdc: s.cdc})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Equal(s.spot, hhha3.TheHasHasAnimal().TheHasAnimal().TheAnimal())
|
||||
}
|
||||
|
||||
func TestSuite(t *testing.T) {
|
||||
suite.Run(t, &Suite{})
|
||||
}
|
||||
+2
-37
@@ -1,8 +1,6 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -50,6 +48,8 @@ type Any struct {
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
|
||||
cachedValue interface{}
|
||||
|
||||
aminoCompat *aminoCompat
|
||||
}
|
||||
|
||||
// NewAnyWithValue constructs a new Any packed with the value provided or
|
||||
@@ -92,38 +92,3 @@ func (any *Any) GetCachedValue() interface{} {
|
||||
func (any *Any) ClearCachedValue() {
|
||||
any.cachedValue = nil
|
||||
}
|
||||
|
||||
// MarshalAny is a convenience function for packing the provided value in an
|
||||
// Any and then proto marshaling it to bytes
|
||||
func MarshalAny(x interface{}) ([]byte, error) {
|
||||
msg, ok := x.(proto.Message)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't proto marshal %T", x)
|
||||
}
|
||||
|
||||
any := &Any{}
|
||||
err := any.Pack(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return any.Marshal()
|
||||
}
|
||||
|
||||
// UnmarshalAny is a convenience function for proto unmarshaling an Any from
|
||||
// bz and then unpacking it to the interface pointer passed in as iface using
|
||||
// the provided AnyUnpacker or returning an error
|
||||
//
|
||||
// Ex:
|
||||
// var x MyInterface
|
||||
// err := UnmarshalAny(unpacker, &x, bz)
|
||||
func UnmarshalAny(unpacker AnyUnpacker, iface interface{}, bz []byte) error {
|
||||
any := &Any{}
|
||||
|
||||
err := any.Unmarshal(bz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return unpacker.UnpackAny(any, iface)
|
||||
}
|
||||
|
||||
@@ -84,7 +84,11 @@ func NewInterfaceRegistry() InterfaceRegistry {
|
||||
}
|
||||
|
||||
func (registry *interfaceRegistry) RegisterInterface(protoName string, iface interface{}, impls ...proto.Message) {
|
||||
registry.interfaceNames[protoName] = reflect.TypeOf(iface)
|
||||
typ := reflect.TypeOf(iface)
|
||||
if typ.Elem().Kind() != reflect.Interface {
|
||||
panic(fmt.Errorf("%T is not an interface type", iface))
|
||||
}
|
||||
registry.interfaceNames[protoName] = typ
|
||||
registry.RegisterImplementations(iface, impls...)
|
||||
}
|
||||
|
||||
@@ -98,7 +102,7 @@ func (registry *interfaceRegistry) RegisterImplementations(iface interface{}, im
|
||||
for _, impl := range impls {
|
||||
implType := reflect.TypeOf(impl)
|
||||
if !implType.AssignableTo(ityp) {
|
||||
panic(fmt.Errorf("type %T doesn't actually implement interface %T", implType, ityp))
|
||||
panic(fmt.Errorf("type %T doesn't actually implement interface %+v", impl, ityp))
|
||||
}
|
||||
|
||||
imap["/"+proto.MessageName(impl)] = implType
|
||||
@@ -125,7 +129,7 @@ func (registry *interfaceRegistry) UnpackAny(any *Any, iface interface{}) error
|
||||
|
||||
imap, found := registry.interfaceImpls[rt]
|
||||
if !found {
|
||||
return fmt.Errorf("no registered implementations of interface type %T", iface)
|
||||
return fmt.Errorf("no registered implementations of type %+v", rt)
|
||||
}
|
||||
|
||||
typ, found := imap[any.TypeUrl]
|
||||
|
||||
+42
-29
@@ -18,6 +18,14 @@ func NewTestInterfaceRegistry() types.InterfaceRegistry {
|
||||
&testdata.Dog{},
|
||||
&testdata.Cat{},
|
||||
)
|
||||
registry.RegisterImplementations(
|
||||
(*testdata.HasAnimalI)(nil),
|
||||
&testdata.HasAnimal{},
|
||||
)
|
||||
registry.RegisterImplementations(
|
||||
(*testdata.HasHasAnimalI)(nil),
|
||||
&testdata.HasHasAnimal{},
|
||||
)
|
||||
return registry
|
||||
}
|
||||
|
||||
@@ -47,35 +55,6 @@ func TestPackUnpack(t *testing.T) {
|
||||
require.Equal(t, spot, animal)
|
||||
}
|
||||
|
||||
func TestMarshalAny(t *testing.T) {
|
||||
registry := types.NewInterfaceRegistry()
|
||||
|
||||
kitty := &testdata.Cat{Moniker: "Kitty"}
|
||||
bz, err := types.MarshalAny(kitty)
|
||||
require.NoError(t, err)
|
||||
|
||||
var animal testdata.Animal
|
||||
|
||||
// empty registry should fail
|
||||
err = types.UnmarshalAny(registry, &animal, bz)
|
||||
require.Error(t, err)
|
||||
|
||||
// wrong type registration should fail
|
||||
registry.RegisterImplementations((*testdata.Animal)(nil), &testdata.Dog{})
|
||||
err = types.UnmarshalAny(registry, &animal, bz)
|
||||
require.Error(t, err)
|
||||
|
||||
// should pass
|
||||
registry = NewTestInterfaceRegistry()
|
||||
err = types.UnmarshalAny(registry, &animal, bz)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kitty, animal)
|
||||
|
||||
// nil should fail
|
||||
registry = NewTestInterfaceRegistry()
|
||||
err = types.UnmarshalAny(registry, nil, bz)
|
||||
}
|
||||
|
||||
type TestI interface {
|
||||
DoSomething()
|
||||
}
|
||||
@@ -93,6 +72,9 @@ func TestRegister(t *testing.T) {
|
||||
require.Panics(t, func() {
|
||||
registry.RegisterImplementations((*TestI)(nil), nil)
|
||||
})
|
||||
require.Panics(t, func() {
|
||||
registry.RegisterInterface("not_an_interface", (*testdata.Dog)(nil))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnpackInterfaces(t *testing.T) {
|
||||
@@ -118,3 +100,34 @@ func TestUnpackInterfaces(t *testing.T) {
|
||||
|
||||
require.Equal(t, spot, hasAny2.Animal.GetCachedValue())
|
||||
}
|
||||
|
||||
func TestNested(t *testing.T) {
|
||||
registry := NewTestInterfaceRegistry()
|
||||
|
||||
spot := &testdata.Dog{Name: "Spot"}
|
||||
any, err := types.NewAnyWithValue(spot)
|
||||
require.NoError(t, err)
|
||||
|
||||
ha := &testdata.HasAnimal{Animal: any}
|
||||
any2, err := types.NewAnyWithValue(ha)
|
||||
require.NoError(t, err)
|
||||
|
||||
hha := &testdata.HasHasAnimal{HasAnimal: any2}
|
||||
any3, err := types.NewAnyWithValue(hha)
|
||||
require.NoError(t, err)
|
||||
|
||||
hhha := testdata.HasHasHasAnimal{HasHasAnimal: any3}
|
||||
|
||||
// marshal
|
||||
bz, err := hhha.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
// unmarshal
|
||||
var hhha2 testdata.HasHasHasAnimal
|
||||
err = hhha2.Unmarshal(bz)
|
||||
require.NoError(t, err)
|
||||
err = types.UnpackInterfaces(hhha2, registry)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, spot, hhha2.TheHasHasAnimal().TheHasAnimal().TheAnimal())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user