|
// 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
|
|
}
|