mirror of
https://github.com/tendermint/tendermint.git
synced 2026-07-29 11:32:56 +00:00
rpc: simplify the encoding of interface-typed arguments in JSON (#7600)
Add package jsontypes that implements a subset of the custom libs/json package. Specifically it handles encoding and decoding of interface types wrapped in "tagged" JSON objects. It omits the deep reflection on arbitrary types, preserving only the handling of type tags wrapper encoding. - Register interface types (Evidence, PubKey, PrivKey) for tagged encoding. - Update the existing implementations to satisfy the type. - Register those types with the jsontypes registry. - Add string tags to 64-bit integer fields where needed. - Add marshalers to structs that export interface-typed fields.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
// Package jsontypes supports decoding for interface types whose concrete
|
||||
// implementations need to be stored as JSON. To do this, concrete values are
|
||||
// packaged in wrapper objects having the form:
|
||||
//
|
||||
// {
|
||||
// "type": "<type-tag>",
|
||||
// "value": <json-encoding-of-value>
|
||||
// }
|
||||
//
|
||||
// This package provides a registry for type tag strings and functions to
|
||||
// encode and decode wrapper objects.
|
||||
package jsontypes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// The Tagged interface must be implemented by a type in order to register it
|
||||
// with the jsontypes package. The TypeTag method returns a string label that
|
||||
// is used to distinguish objects of that type.
|
||||
type Tagged interface {
|
||||
TypeTag() string
|
||||
}
|
||||
|
||||
// registry records the mapping from type tags to value types. Values in this
|
||||
// map must be normalized to non-pointer types.
|
||||
var registry = struct {
|
||||
types map[string]reflect.Type
|
||||
}{types: make(map[string]reflect.Type)}
|
||||
|
||||
// register adds v to the type registry. It reports an error if the tag
|
||||
// returned by v is already registered.
|
||||
func register(v Tagged) error {
|
||||
tag := v.TypeTag()
|
||||
if t, ok := registry.types[tag]; ok {
|
||||
return fmt.Errorf("type tag %q already registered to %v", tag, t)
|
||||
}
|
||||
typ := reflect.TypeOf(v)
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
registry.types[tag] = typ
|
||||
return nil
|
||||
}
|
||||
|
||||
// MustRegister adds v to the type registry. It will panic if the tag returned
|
||||
// by v is already registered. This function is meant for use during program
|
||||
// initialization.
|
||||
func MustRegister(v Tagged) {
|
||||
if err := register(v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
Type string `json:"type"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
|
||||
// Marshal marshals a JSON wrapper object containing v. If v == nil, Marshal
|
||||
// returns the JSON "null" value without error.
|
||||
func Marshal(v Tagged) ([]byte, error) {
|
||||
if v == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(wrapper{
|
||||
Type: v.TypeTag(),
|
||||
Value: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals a JSON wrapper object into v. It reports an error if
|
||||
// the data do not encode a valid wrapper object, if the wrapper's type tag is
|
||||
// not registered with jsontypes, or if the resulting value is not compatible
|
||||
// with the type of v.
|
||||
func Unmarshal(data []byte, v interface{}) error {
|
||||
// Verify that the target is some kind of pointer.
|
||||
target := reflect.ValueOf(v)
|
||||
if target.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("target %T is not a pointer", v)
|
||||
}
|
||||
|
||||
var w wrapper
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&w); err != nil {
|
||||
return fmt.Errorf("invalid type wrapper: %w", err)
|
||||
}
|
||||
typ, ok := registry.types[w.Type]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown type tag: %q", w.Type)
|
||||
} else if !typ.AssignableTo(target.Elem().Type()) {
|
||||
return fmt.Errorf("type %v not assignable to %T", typ, v)
|
||||
}
|
||||
|
||||
obj := reflect.New(typ)
|
||||
if err := json.Unmarshal(w.Value, obj.Interface()); err != nil {
|
||||
return fmt.Errorf("decoding wrapped value: %w", err)
|
||||
}
|
||||
target.Elem().Set(obj.Elem())
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package jsontypes_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tendermint/tendermint/internal/jsontypes"
|
||||
)
|
||||
|
||||
type testType struct {
|
||||
Field string `json:"field"`
|
||||
}
|
||||
|
||||
func (*testType) TypeTag() string { return "test/TaggedType" }
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
const wantEncoded = `{"type":"test/TaggedType","value":{"field":"hello"}}`
|
||||
|
||||
t.Run("MustRegisterOK", func(t *testing.T) {
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
t.Fatalf("Registration panicked: %v", x)
|
||||
}
|
||||
}()
|
||||
jsontypes.MustRegister((*testType)(nil))
|
||||
})
|
||||
|
||||
t.Run("MustRegisterFail", func(t *testing.T) {
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
t.Logf("Got expected panic: %v", x)
|
||||
}
|
||||
}()
|
||||
jsontypes.MustRegister((*testType)(nil))
|
||||
t.Fatal("Registration should not have succeeded")
|
||||
})
|
||||
|
||||
t.Run("MarshalNil", func(t *testing.T) {
|
||||
bits, err := jsontypes.Marshal(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal failed: %v", err)
|
||||
}
|
||||
if got := string(bits); got != "null" {
|
||||
t.Errorf("Marshal nil: got %#q, want null", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RoundTrip", func(t *testing.T) {
|
||||
obj := testType{Field: "hello"}
|
||||
bits, err := jsontypes.Marshal(&obj)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal %T failed: %v", obj, err)
|
||||
}
|
||||
if got := string(bits); got != wantEncoded {
|
||||
t.Errorf("Marshal %T: got %#q, want %#q", obj, got, wantEncoded)
|
||||
}
|
||||
|
||||
var cmp testType
|
||||
if err := jsontypes.Unmarshal(bits, &cmp); err != nil {
|
||||
t.Errorf("Unmarshal %#q failed: %v", string(bits), err)
|
||||
}
|
||||
if obj != cmp {
|
||||
t.Errorf("Unmarshal %#q: got %+v, want %+v", string(bits), cmp, obj)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Unregistered", func(t *testing.T) {
|
||||
obj := testType{Field: "hello"}
|
||||
bits, err := jsontypes.Marshal(&obj)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal %T failed: %v", obj, err)
|
||||
}
|
||||
if got := string(bits); got != wantEncoded {
|
||||
t.Errorf("Marshal %T: got %#q, want %#q", obj, got, wantEncoded)
|
||||
}
|
||||
|
||||
var cmp struct {
|
||||
Field string `json:"field"`
|
||||
}
|
||||
if err := jsontypes.Unmarshal(bits, &cmp); err != nil {
|
||||
t.Errorf("Unmarshal %#q: got %+v, want %+v", string(bits), cmp, obj)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -52,6 +52,7 @@ func (pk privKeyWithNilPubKey) Sign(msg []byte) ([]byte, error) { return pk.orig
|
||||
func (pk privKeyWithNilPubKey) PubKey() crypto.PubKey { return nil }
|
||||
func (pk privKeyWithNilPubKey) Equals(pk2 crypto.PrivKey) bool { return pk.orig.Equals(pk2) }
|
||||
func (pk privKeyWithNilPubKey) Type() string { return "privKeyWithNilPubKey" }
|
||||
func (privKeyWithNilPubKey) TypeTag() string { return "test/privKeyWithNilPubKey" }
|
||||
|
||||
func TestSecretConnectionHandshake(t *testing.T) {
|
||||
fooSecConn, barSecConn := makeSecretConnPair(t)
|
||||
|
||||
Reference in New Issue
Block a user