Make needed changes in conversion package to support pluggability

This commit is contained in:
Daniel Smith 2014-09-09 22:51:33 -07:00
parent 91e9089819
commit 71e547124c
6 changed files with 448 additions and 68 deletions

View File

@ -39,12 +39,18 @@ type Converter struct {
// If non-nil, will be called to print helpful debugging info. Quite verbose.
Debug DebugLogger
// Name is called to retrieve the name of a type; this name is used for the
// purpose of deciding whether two types match or not (i.e., will we attempt to
// do a conversion). The default returns the go type name.
Name func(t reflect.Type) string
}
// NewConverter creates a new Converter object.
func NewConverter() *Converter {
return &Converter{
funcs: map[typePair]reflect.Value{},
Name: func(t reflect.Type) string { return t.Name() },
}
}
@ -55,6 +61,72 @@ type Scope interface {
// Call Convert to convert sub-objects. Note that if you call it with your own exact
// parameters, you'll run out of stack space before anything useful happens.
Convert(src, dest interface{}, flags FieldMatchingFlags) error
// SrcTags and DestTags contain the struct tags that src and dest had, respectively.
// If the enclosing object was not a struct, then these will contain no tags, of course.
SrcTag() reflect.StructTag
DestTag() reflect.StructTag
// Flags returns the flags with which the conversion was started.
Flags() FieldMatchingFlags
// Meta returns any information originally passed to Convert.
Meta() map[string]interface{}
}
// scope contains information about an ongoing conversion.
type scope struct {
converter *Converter
meta map[string]interface{}
flags FieldMatchingFlags
srcTagStack []reflect.StructTag
destTagStack []reflect.StructTag
}
// push adds a level to the src/dest tag stacks.
func (sa *scope) push() {
sa.srcTagStack = append(sa.srcTagStack, "")
sa.destTagStack = append(sa.destTagStack, "")
}
// pop removes a level to the src/dest tag stacks.
func (sa *scope) pop() {
n := len(sa.srcTagStack)
sa.srcTagStack = sa.srcTagStack[:n-1]
sa.destTagStack = sa.destTagStack[:n-1]
}
func (sa *scope) setSrcTag(tag reflect.StructTag) {
sa.srcTagStack[len(sa.srcTagStack)-1] = tag
}
func (sa *scope) setDestTag(tag reflect.StructTag) {
sa.destTagStack[len(sa.destTagStack)-1] = tag
}
// Convert continues a conversion.
func (sa *scope) Convert(src, dest interface{}, flags FieldMatchingFlags) error {
return sa.converter.Convert(src, dest, flags, sa.meta)
}
// SrcTag returns the tag of the struct containing the current source item, if any.
func (sa *scope) SrcTag() reflect.StructTag {
return sa.srcTagStack[len(sa.srcTagStack)-1]
}
// DestTag returns the tag of the struct containing the current dest item, if any.
func (sa *scope) DestTag() reflect.StructTag {
return sa.destTagStack[len(sa.destTagStack)-1]
}
// Flags returns the flags with which the current conversion was started.
func (sa *scope) Flags() FieldMatchingFlags {
return sa.flags
}
// Meta returns the meta object that was originally passed to Convert.
func (sa *scope) Meta() map[string]interface{} {
return sa.meta
}
// Register registers a conversion func with the Converter. conversionFunc must take
@ -82,7 +154,7 @@ func (c *Converter) Register(conversionFunc interface{}) error {
if ft.In(1).Kind() != reflect.Ptr {
return fmt.Errorf("expected pointer arg for 'in' param 1, got: %v", ft)
}
scopeType := Scope(c)
scopeType := Scope(nil)
if e, a := reflect.TypeOf(&scopeType).Elem(), ft.In(2); e != a {
return fmt.Errorf("expected '%v' arg for 'in' param 2, got '%v' (%v)", e, a, ft)
}
@ -127,8 +199,12 @@ func (f FieldMatchingFlags) IsSet(flag FieldMatchingFlags) bool {
// Convert will translate src to dest if it knows how. Both must be pointers.
// If no conversion func is registered and the default copying mechanism
// doesn't work on this type pair, an error will be returned.
// Read the comments on the various FieldMatchingFlags constants to understand
// what the 'flags' parameter does.
// 'meta' is given to allow you to pass information to conversion functions,
// it is not used by Convert other than storing it in the scope.
// Not safe for objects with cyclic references!
func (c *Converter) Convert(src, dest interface{}, flags FieldMatchingFlags) error {
func (c *Converter) Convert(src, dest interface{}, flags FieldMatchingFlags, meta map[string]interface{}) error {
dv, sv := reflect.ValueOf(dest), reflect.ValueOf(src)
if dv.Kind() != reflect.Ptr {
return fmt.Errorf("Need pointer, but got %#v", dest)
@ -141,18 +217,24 @@ func (c *Converter) Convert(src, dest interface{}, flags FieldMatchingFlags) err
if !dv.CanAddr() {
return fmt.Errorf("Can't write to dest")
}
return c.convert(sv, dv, flags)
s := &scope{
converter: c,
flags: flags,
meta: meta,
}
s.push() // Easy way to make SrcTag and DestTag never fail
return c.convert(sv, dv, s)
}
// convert recursively copies sv into dv, calling an appropriate conversion function if
// one is registered.
func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) error {
func (c *Converter) convert(sv, dv reflect.Value, scope *scope) error {
dt, st := dv.Type(), sv.Type()
if fv, ok := c.funcs[typePair{st, dt}]; ok {
if c.Debug != nil {
c.Debug.Logf("Calling custom conversion of '%v' to '%v'", st, dt)
}
args := []reflect.Value{sv.Addr(), dv.Addr(), reflect.ValueOf(Scope(c))}
args := []reflect.Value{sv.Addr(), dv.Addr(), reflect.ValueOf(scope)}
ret := fv.Call(args)[0].Interface()
// This convolution is necssary because nil interfaces won't convert
// to errors.
@ -162,7 +244,7 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro
return ret.(error)
}
if !flags.IsSet(AllowDifferentFieldTypeNames) && dt.Name() != st.Name() {
if !scope.flags.IsSet(AllowDifferentFieldTypeNames) && c.Name(dt) != c.Name(st) {
return fmt.Errorf("Can't convert %v to %v because type names don't match.", st, dt)
}
@ -180,28 +262,41 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro
c.Debug.Logf("Trying to convert '%v' to '%v'", st, dt)
}
scope.push()
defer scope.pop()
switch dv.Kind() {
case reflect.Struct:
listType := dt
if flags.IsSet(SourceToDest) {
if scope.flags.IsSet(SourceToDest) {
listType = st
}
for i := 0; i < listType.NumField(); i++ {
f := listType.Field(i)
df := dv.FieldByName(f.Name)
sf := sv.FieldByName(f.Name)
if sf.IsValid() {
// No need to check error, since we know it's valid.
field, _ := st.FieldByName(f.Name)
scope.setSrcTag(field.Tag)
}
if df.IsValid() {
field, _ := dt.FieldByName(f.Name)
scope.setDestTag(field.Tag)
}
// TODO: set top level of scope.src/destTagStack with these field tags here.
if !df.IsValid() || !sf.IsValid() {
switch {
case flags.IsSet(IgnoreMissingFields):
case scope.flags.IsSet(IgnoreMissingFields):
// No error.
case flags.IsSet(SourceToDest):
case scope.flags.IsSet(SourceToDest):
return fmt.Errorf("%v not present in dest (%v to %v)", f.Name, st, dt)
default:
return fmt.Errorf("%v not present in src (%v to %v)", f.Name, st, dt)
}
continue
}
if err := c.convert(sf, df, flags); err != nil {
if err := c.convert(sf, df, scope); err != nil {
return err
}
}
@ -213,7 +308,7 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro
}
dv.Set(reflect.MakeSlice(dt, sv.Len(), sv.Cap()))
for i := 0; i < sv.Len(); i++ {
if err := c.convert(sv.Index(i), dv.Index(i), flags); err != nil {
if err := c.convert(sv.Index(i), dv.Index(i), scope); err != nil {
return err
}
}
@ -224,7 +319,7 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro
return nil
}
dv.Set(reflect.New(dt.Elem()))
return c.convert(sv.Elem(), dv.Elem(), flags)
return c.convert(sv.Elem(), dv.Elem(), scope)
case reflect.Map:
if sv.IsNil() {
// Don't copy a nil ptr!
@ -234,11 +329,11 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro
dv.Set(reflect.MakeMap(dt))
for _, sk := range sv.MapKeys() {
dk := reflect.New(dt.Key()).Elem()
if err := c.convert(sk, dk, flags); err != nil {
if err := c.convert(sk, dk, scope); err != nil {
return err
}
dkv := reflect.New(dt.Elem()).Elem()
if err := c.convert(sv.MapIndex(sk), dkv, flags); err != nil {
if err := c.convert(sv.MapIndex(sk), dkv, scope); err != nil {
return err
}
dv.SetMapIndex(dk, dkv)

View File

@ -35,6 +35,7 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) {
}
type C struct{}
c := NewConverter()
c.Debug = t
err := c.Register(func(in *A, out *B, s Scope) error {
out.Bar = in.Foo
return s.Convert(&in.Baz, &out.Baz, 0)
@ -53,7 +54,7 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) {
x := A{"hello, intrepid test reader!", 3}
y := B{}
err = c.Convert(&x, &y, 0)
err = c.Convert(&x, &y, 0, nil)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
@ -67,7 +68,7 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) {
z := B{"all your test are belong to us", 42}
w := A{}
err = c.Convert(&z, &w, 0)
err = c.Convert(&z, &w, 0, nil)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
@ -85,36 +86,43 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) {
t.Fatalf("unexpected error %v", err)
}
err = c.Convert(&A{}, &C{}, 0)
err = c.Convert(&A{}, &C{}, 0, nil)
if err == nil {
t.Errorf("unexpected non-error")
}
}
func TestConverter_fuzz(t *testing.T) {
newAnonType := func() interface{} {
return reflect.New(reflect.TypeOf(externalTypeReturn()).Elem()).Interface()
}
// Use the same types from the scheme test.
table := []struct {
from, to, check interface{}
}{
{&TestType1{}, newAnonType(), &TestType1{}},
{newAnonType(), &TestType1{}, newAnonType()},
{&TestType1{}, &ExternalTestType1{}, &TestType1{}},
{&ExternalTestType1{}, &TestType1{}, &ExternalTestType1{}},
}
f := fuzz.New().NilChance(.5).NumElements(0, 100)
c := NewConverter()
c.Name = func(t reflect.Type) string {
// Hide the fact that we don't have separate packages for these things.
return map[reflect.Type]string{
reflect.TypeOf(TestType1{}): "TestType1",
reflect.TypeOf(ExternalTestType1{}): "TestType1",
reflect.TypeOf(TestType2{}): "TestType2",
reflect.TypeOf(ExternalTestType2{}): "TestType2",
}[t]
}
c.Debug = t
for i, item := range table {
for j := 0; j < *fuzzIters; j++ {
f.Fuzz(item.from)
err := c.Convert(item.from, item.to, 0)
err := c.Convert(item.from, item.to, 0, nil)
if err != nil {
t.Errorf("(%v, %v): unexpected error: %v", i, j, err)
continue
}
err = c.Convert(item.to, item.check, 0)
err = c.Convert(item.to, item.check, 0, nil)
if err != nil {
t.Errorf("(%v, %v): unexpected error: %v", i, j, err)
continue
@ -126,6 +134,72 @@ func TestConverter_fuzz(t *testing.T) {
}
}
func TestConverter_tags(t *testing.T) {
type Foo struct {
A string `test:"foo"`
}
type Bar struct {
A string `test:"bar"`
}
c := NewConverter()
c.Debug = t
err := c.Register(
func(in *string, out *string, s Scope) error {
if e, a := "foo", s.SrcTag().Get("test"); e != a {
t.Errorf("expected %v, got %v", e, a)
}
if e, a := "bar", s.DestTag().Get("test"); e != a {
t.Errorf("expected %v, got %v", e, a)
}
return nil
},
)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
c.Convert(&Foo{}, &Bar{}, 0, nil)
}
func TestConverter_meta(t *testing.T) {
type Foo struct{ A string }
type Bar struct{ A string }
c := NewConverter()
c.Debug = t
checks := 0
err := c.Register(
func(in *Foo, out *Bar, s Scope) error {
if s.Meta()["test"] != "passes" {
t.Errorf("Meta did not get passed!")
}
checks++
s.Convert(&in.A, &out.A, 0)
return nil
},
)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
err = c.Register(
func(in *string, out *string, s Scope) error {
if s.Meta()["test"] != "passes" {
t.Errorf("Meta did not get passed a second time!")
}
checks++
return nil
},
)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
err = c.Convert(&Foo{}, &Bar{}, 0, map[string]interface{}{"test": "passes"})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if checks != 2 {
t.Errorf("Registered functions did not get called.")
}
}
func TestConverter_flags(t *testing.T) {
type Foo struct{ A string }
type Bar struct{ A string }
@ -199,11 +273,12 @@ func TestConverter_flags(t *testing.T) {
}
f := fuzz.New().NilChance(.5).NumElements(0, 100)
c := NewConverter()
c.Debug = t
for i, item := range table {
for j := 0; j < *fuzzIters; j++ {
f.Fuzz(item.from)
err := c.Convert(item.from, item.to, item.flags)
err := c.Convert(item.from, item.to, item.flags, nil)
if item.shouldSucceed && err != nil {
t.Errorf("(%v, %v): unexpected error: %v", i, j, err)
continue

View File

@ -58,7 +58,7 @@ func (s *Scheme) Decode(data []byte) (interface{}, error) {
if err != nil {
return nil, err
}
err = s.converter.Convert(obj, objOut, 0)
err = s.converter.Convert(obj, objOut, 0, s.generateConvertMeta(version, s.InternalVersion))
if err != nil {
return nil, err
}
@ -112,7 +112,7 @@ func (s *Scheme) DecodeInto(data []byte, obj interface{}) error {
if err != nil {
return err
}
err = s.converter.Convert(external, obj, 0)
err = s.converter.Convert(external, obj, 0, s.generateConvertMeta(dataVersion, objVersion))
if err != nil {
return err
}

View File

@ -81,7 +81,7 @@ func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []by
if err != nil {
return nil, err
}
err = s.converter.Convert(obj, objOut, 0)
err = s.converter.Convert(obj, objOut, 0, s.generateConvertMeta(objVersion, destVersion))
if err != nil {
return nil, err
}

View File

@ -51,6 +51,10 @@ type Scheme struct {
// is registered for multiple versions, the last one wins.
typeToVersion map[reflect.Type]string
// typeToKind allows one to figure out the desired "kind" field for a given
// go object. Requirements and caveats are the same as typeToVersion.
typeToKind map[reflect.Type]string
// converter stores all registered conversion functions. It also has
// default coverting behavior.
converter *Converter
@ -73,14 +77,22 @@ type Scheme struct {
// NewScheme manufactures a new scheme.
func NewScheme() *Scheme {
return &Scheme{
s := &Scheme{
versionMap: map[string]map[string]reflect.Type{},
typeToVersion: map[reflect.Type]string{},
typeToKind: map[reflect.Type]string{},
converter: NewConverter(),
InternalVersion: "",
ExternalVersion: "v1",
MetaInsertionFactory: metaInsertion{},
}
s.converter.Name = func(t reflect.Type) string {
if kind, ok := s.typeToKind[t]; ok {
return kind
}
return t.Name()
}
return s
}
// AddKnownTypes registers all types passed in 'types' as being members of version 'version.
@ -104,9 +116,32 @@ func (s *Scheme) AddKnownTypes(version string, types ...interface{}) {
}
knownTypes[t.Name()] = t
s.typeToVersion[t] = version
s.typeToKind[t] = t.Name()
}
}
// AddKnownTypeWithName is like AddKnownTypes, but it lets you specify what this type should
// be encoded as. Useful for testing when you don't want to make multiple packages to define
// your structs.
func (s *Scheme) AddKnownTypeWithName(version, kind string, obj interface{}) {
knownTypes, found := s.versionMap[version]
if !found {
knownTypes = map[string]reflect.Type{}
s.versionMap[version] = knownTypes
}
t := reflect.TypeOf(obj)
if t.Kind() != reflect.Ptr {
panic("All types must be pointers to structs.")
}
t = t.Elem()
if t.Kind() != reflect.Struct {
panic("All types must be pointers to structs.")
}
knownTypes[kind] = t
s.typeToVersion[t] = version
s.typeToKind[t] = kind
}
// NewObject returns a new object of the given version and name,
// or an error if it hasn't been registered.
func (s *Scheme) NewObject(versionName, typeName string) (interface{}, error) {
@ -124,9 +159,23 @@ func (s *Scheme) NewObject(versionName, typeName string) (interface{}, error) {
// sub-objects. We deduce how to call these functions from the types of their two
// parameters; see the comment for Converter.Register.
//
// Note that, if you need to copy sub-objects that didn't change, it's safe to call
// s.Convert() inside your conversionFuncs, as long as you don't start a conversion
// chain that's infinitely recursive.
// Note that, if you need to copy sub-objects that didn't change, you can use the
// conversion.Scope object that will be passed to your conversion function.
// Additionally, all conversions started by Scheme will set the "srcVersion" and
// "destVersion" keys on the meta object. Example:
//
// s.AddConversionFuncs(
// func(in *InternalObject, out *ExternalObject, scope conversion.Scope) error {
// // You can depend on this being set to the source version, e.g., "".
// s.Meta()["srcVersion"].(string)
// // You can depend on this being set to the destination version,
// // e.g., "v1beta1".
// s.Meta()["destVersion"].(string)
// // Call scope.Convert to copy sub-fields.
// s.Convert(&in.SubFieldThatMoved, &out.NewLocation.NewName, 0)
// return nil
// },
// )
//
// Also note that the default behavior, if you don't add a conversion function, is to
// sanely copy fields that have the same names and same type names. It's OK if the
@ -144,9 +193,28 @@ func (s *Scheme) AddConversionFuncs(conversionFuncs ...interface{}) error {
// Convert will attempt to convert in into out. Both must be pointers. For easy
// testing of conversion functions. Returns an error if the conversion isn't
// possible.
// possible. You can call this with types that haven't been registered (for example,
// a to test conversion of types that are nested within registered types), but in
// that case, the conversion.Scope object passed to your conversion functions won't
// have "srcVersion" or "destVersion" keys set correctly in Meta().
func (s *Scheme) Convert(in, out interface{}) error {
return s.converter.Convert(in, out, 0)
inVersion := "unknown"
outVersion := "unknown"
if v, _, err := s.ObjectVersionAndKind(in); err == nil {
inVersion = v
}
if v, _, err := s.ObjectVersionAndKind(out); err == nil {
outVersion = v
}
return s.converter.Convert(in, out, 0, s.generateConvertMeta(inVersion, outVersion))
}
// generateConvertMeta assembles a map for the meta value we pass to Convert.
func (s *Scheme) generateConvertMeta(srcVersion, destVersion string) map[string]interface{} {
return map[string]interface{}{
"srcVersion": srcVersion,
"destVersion": destVersion,
}
}
// metaInsertion provides a default implementation of MetaInsertionFactory.
@ -194,11 +262,12 @@ func (s *Scheme) ObjectVersionAndKind(obj interface{}) (apiVersion, kind string,
return "", "", err
}
t := v.Type()
if version, ok := s.typeToVersion[t]; !ok {
version, vOK := s.typeToVersion[t]
kind, kOK := s.typeToKind[t]
if !vOK || !kOK {
return "", "", fmt.Errorf("Unregistered type: %v", t)
} else {
return version, t.Name(), nil
}
return version, kind, nil
}
// SetVersionAndKind sets the version and kind fields (with help from
@ -206,7 +275,7 @@ func (s *Scheme) ObjectVersionAndKind(obj interface{}) (apiVersion, kind string,
// must be a pointer.
func (s *Scheme) SetVersionAndKind(version, kind string, obj interface{}) error {
versionAndKind := s.MetaInsertionFactory.Create(version, kind)
return s.converter.Convert(versionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldTypeNames)
return s.converter.Convert(versionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldTypeNames, nil)
}
// maybeCopy copies obj if it is not a pointer, to get a settable/addressable

View File

@ -63,34 +63,28 @@ type TestType2 struct {
B int `yaml:"B,omitempty" json:"B,omitempty"`
}
// We depend on the name of the external and internal types matching. Ordinarily,
// we'd accomplish this with an additional package, but since this is a test, we
// can just enclose stuff in a function to simulate that.
func externalTypeReturn() interface{} {
type TestType2 struct {
A string `yaml:"A,omitempty" json:"A,omitempty"`
B int `yaml:"B,omitempty" json:"B,omitempty"`
}
type TestType1 struct {
MyWeirdCustomEmbeddedVersionKindField `json:",inline" yaml:",inline"`
A string `yaml:"A,omitempty" json:"A,omitempty"`
B int `yaml:"B,omitempty" json:"B,omitempty"`
C int8 `yaml:"C,omitempty" json:"C,omitempty"`
D int16 `yaml:"D,omitempty" json:"D,omitempty"`
E int32 `yaml:"E,omitempty" json:"E,omitempty"`
F int64 `yaml:"F,omitempty" json:"F,omitempty"`
G uint `yaml:"G,omitempty" json:"G,omitempty"`
H uint8 `yaml:"H,omitempty" json:"H,omitempty"`
I uint16 `yaml:"I,omitempty" json:"I,omitempty"`
J uint32 `yaml:"J,omitempty" json:"J,omitempty"`
K uint64 `yaml:"K,omitempty" json:"K,omitempty"`
L bool `yaml:"L,omitempty" json:"L,omitempty"`
M map[string]int `yaml:"M,omitempty" json:"M,omitempty"`
N map[string]TestType2 `yaml:"N,omitempty" json:"N,omitempty"`
O *TestType2 `yaml:"O,omitempty" json:"O,omitempty"`
P []TestType2 `yaml:"Q,omitempty" json:"Q,omitempty"`
}
return &TestType1{}
type ExternalTestType2 struct {
A string `yaml:"A,omitempty" json:"A,omitempty"`
B int `yaml:"B,omitempty" json:"B,omitempty"`
}
type ExternalTestType1 struct {
MyWeirdCustomEmbeddedVersionKindField `json:",inline" yaml:",inline"`
A string `yaml:"A,omitempty" json:"A,omitempty"`
B int `yaml:"B,omitempty" json:"B,omitempty"`
C int8 `yaml:"C,omitempty" json:"C,omitempty"`
D int16 `yaml:"D,omitempty" json:"D,omitempty"`
E int32 `yaml:"E,omitempty" json:"E,omitempty"`
F int64 `yaml:"F,omitempty" json:"F,omitempty"`
G uint `yaml:"G,omitempty" json:"G,omitempty"`
H uint8 `yaml:"H,omitempty" json:"H,omitempty"`
I uint16 `yaml:"I,omitempty" json:"I,omitempty"`
J uint32 `yaml:"J,omitempty" json:"J,omitempty"`
K uint64 `yaml:"K,omitempty" json:"K,omitempty"`
L bool `yaml:"L,omitempty" json:"L,omitempty"`
M map[string]int `yaml:"M,omitempty" json:"M,omitempty"`
N map[string]ExternalTestType2 `yaml:"N,omitempty" json:"N,omitempty"`
O *ExternalTestType2 `yaml:"O,omitempty" json:"O,omitempty"`
P []ExternalTestType2 `yaml:"Q,omitempty" json:"Q,omitempty"`
}
type ExternalInternalSame struct {
@ -124,8 +118,13 @@ var TestObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 100).Funcs(
// Returns a new Scheme set up with the test objects.
func GetTestScheme() *Scheme {
s := NewScheme()
s.AddKnownTypes("", &TestType1{}, &ExternalInternalSame{})
s.AddKnownTypes("v1", externalTypeReturn(), &ExternalInternalSame{})
// Ordinarily, we wouldn't add TestType2, but because this is a test and
// both types are from the same package, we need to get it into the system
// so that converter will match it with ExternalType2.
s.AddKnownTypes("", &TestType1{}, &TestType2{}, &ExternalInternalSame{})
s.AddKnownTypes("v1", &ExternalInternalSame{})
s.AddKnownTypeWithName("v1", "TestType1", &ExternalTestType1{})
s.AddKnownTypeWithName("v1", "TestType2", &ExternalTestType2{})
s.ExternalVersion = "v1"
s.InternalVersion = ""
s.MetaInsertionFactory = testMetaInsertionFactory{}
@ -270,3 +269,145 @@ func TestBadJSONRejection(t *testing.T) {
t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch)
}
}
func TestMetaValues(t *testing.T) {
type InternalSimple struct {
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
TestString string `json:"testString" yaml:"testString"`
}
type ExternalSimple struct {
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
TestString string `json:"testString" yaml:"testString"`
}
s := NewScheme()
s.InternalVersion = ""
s.ExternalVersion = "externalVersion"
s.AddKnownTypeWithName("", "Simple", &InternalSimple{})
s.AddKnownTypeWithName("externalVersion", "Simple", &ExternalSimple{})
internalToExternalCalls := 0
externalToInternalCalls := 0
// Register functions to verify that scope.Meta() gets set correctly.
err := s.AddConversionFuncs(
func(in *InternalSimple, out *ExternalSimple, scope Scope) error {
if e, a := "", scope.Meta()["srcVersion"].(string); e != a {
t.Errorf("Expected '%v', got '%v'", e, a)
}
if e, a := "externalVersion", scope.Meta()["destVersion"].(string); e != a {
t.Errorf("Expected '%v', got '%v'", e, a)
}
scope.Convert(&in.TestString, &out.TestString, 0)
internalToExternalCalls++
return nil
},
func(in *ExternalSimple, out *InternalSimple, scope Scope) error {
if e, a := "externalVersion", scope.Meta()["srcVersion"].(string); e != a {
t.Errorf("Expected '%v', got '%v'", e, a)
}
if e, a := "", scope.Meta()["destVersion"].(string); e != a {
t.Errorf("Expected '%v', got '%v'", e, a)
}
scope.Convert(&in.TestString, &out.TestString, 0)
externalToInternalCalls++
return nil
},
)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
simple := &InternalSimple{
TestString: "foo",
}
// Test Encode, Decode, and DecodeInto
data, err := s.EncodeToVersion(simple, "externalVersion")
obj2, err2 := s.Decode(data)
obj3 := &InternalSimple{}
err3 := s.DecodeInto(data, obj3)
if err != nil || err2 != nil {
t.Fatalf("Failure: '%v' '%v' '%v'", err, err2, err3)
}
if _, ok := obj2.(*InternalSimple); !ok {
t.Fatalf("Got wrong type")
}
if e, a := simple, obj2; !reflect.DeepEqual(e, a) {
t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a)
}
if e, a := simple, obj3; !reflect.DeepEqual(e, a) {
t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a)
}
// Test Convert
external := &ExternalSimple{}
err = s.Convert(simple, external)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if e, a := simple.TestString, external.TestString; e != a {
t.Errorf("Expected %v, got %v", e, a)
}
// Encode and Convert should each have caused an increment.
if e, a := 2, internalToExternalCalls; e != a {
t.Errorf("Expected %v, got %v", e, a)
}
// Decode and DecodeInto should each have caused an increment.
if e, a := 2, externalToInternalCalls; e != a {
t.Errorf("Expected %v, got %v", e, a)
}
}
func TestMetaValuesUnregisteredConvert(t *testing.T) {
type InternalSimple struct {
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
TestString string `json:"testString" yaml:"testString"`
}
type ExternalSimple struct {
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
TestString string `json:"testString" yaml:"testString"`
}
s := NewScheme()
s.InternalVersion = ""
s.ExternalVersion = "externalVersion"
// We deliberately don't register the types.
internalToExternalCalls := 0
// Register functions to verify that scope.Meta() gets set correctly.
err := s.AddConversionFuncs(
func(in *InternalSimple, out *ExternalSimple, scope Scope) error {
if e, a := "unknown", scope.Meta()["srcVersion"].(string); e != a {
t.Errorf("Expected '%v', got '%v'", e, a)
}
if e, a := "unknown", scope.Meta()["destVersion"].(string); e != a {
t.Errorf("Expected '%v', got '%v'", e, a)
}
scope.Convert(&in.TestString, &out.TestString, 0)
internalToExternalCalls++
return nil
},
)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
simple := &InternalSimple{TestString: "foo"}
external := &ExternalSimple{}
err = s.Convert(simple, external)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if e, a := simple.TestString, external.TestString; e != a {
t.Errorf("Expected %v, got %v", e, a)
}
// Verify that our conversion handler got called.
if e, a := 1, internalToExternalCalls; e != a {
t.Errorf("Expected %v, got %v", e, a)
}
}