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:
Aaron Craelius
2020-05-06 20:47:03 +00:00
committed by GitHub
co-authored by Federico Kunze
parent f3e3a30e5e
commit 9d022c17b7
14 changed files with 939 additions and 94 deletions
+140
View File
@@ -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
}
+132
View File
@@ -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
View File
@@ -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)
}
+7 -3
View File
@@ -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
View File
@@ -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())
}