diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go index b5f7f93c9d5..43d3a4feccd 100644 --- a/pkg/conversion/converter.go +++ b/pkg/conversion/converter.go @@ -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) diff --git a/pkg/conversion/converter_test.go b/pkg/conversion/converter_test.go index 96c569cd6cd..c12c7633a06 100644 --- a/pkg/conversion/converter_test.go +++ b/pkg/conversion/converter_test.go @@ -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 diff --git a/pkg/conversion/decode.go b/pkg/conversion/decode.go index 530f74ab571..c2c67e72092 100644 --- a/pkg/conversion/decode.go +++ b/pkg/conversion/decode.go @@ -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 } diff --git a/pkg/conversion/encode.go b/pkg/conversion/encode.go index 554a6dc4437..e82ddce9236 100644 --- a/pkg/conversion/encode.go +++ b/pkg/conversion/encode.go @@ -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 } diff --git a/pkg/conversion/scheme.go b/pkg/conversion/scheme.go index af88ba4e63e..ce1cd66f559 100644 --- a/pkg/conversion/scheme.go +++ b/pkg/conversion/scheme.go @@ -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 diff --git a/pkg/conversion/scheme_test.go b/pkg/conversion/scheme_test.go index c8232ff4ea6..bcb304c357f 100644 --- a/pkg/conversion/scheme_test.go +++ b/pkg/conversion/scheme_test.go @@ -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) + } +}