From 1cc7fce5234eae343297a4fa9ee2d7b1c0e625a7 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Fri, 1 Aug 2014 14:24:12 -0700 Subject: [PATCH] Add documentation and tests to conversion. --- pkg/api/helper.go | 26 ++++++ pkg/conversion/converter.go | 16 ++-- pkg/conversion/converter_test.go | 130 ++++++++++++++++++++++++++++- pkg/conversion/decode.go | 20 ++--- pkg/conversion/doc.go | 34 ++++++++ pkg/conversion/encode.go | 45 +++------- pkg/conversion/scheme.go | 66 ++++++++------- pkg/conversion/scheme_test.go | 139 ++++++++++++++++++------------- 8 files changed, 331 insertions(+), 145 deletions(-) create mode 100644 pkg/conversion/doc.go diff --git a/pkg/api/helper.go b/pkg/api/helper.go index 91a631217b..0d7d5b6fde 100644 --- a/pkg/api/helper.go +++ b/pkg/api/helper.go @@ -31,6 +31,7 @@ func init() { conversionScheme = conversion.NewScheme() conversionScheme.InternalVersion = "" conversionScheme.ExternalVersion = "v1beta1" + conversionScheme.MetaInsertionFactory = metaInsertion{} AddKnownTypes("", PodList{}, Pod{}, @@ -248,3 +249,28 @@ func Decode(data []byte) (interface{}, error) { func DecodeInto(data []byte, obj interface{}) error { return conversionScheme.DecodeInto(data, obj) } + +// metaInsertion implements conversion.MetaInsertionFactory, which lets the conversion +// package figure out how to encode our object's types and versions. These fields are +// located in our JSONBase. +type metaInsertion struct { + JSONBase struct { + APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + } `json:",inline" yaml:",inline"` +} + +// Create returns a new metaInsertion with the version and kind fields set. +func (metaInsertion) Create(version, kind string) interface{} { + m := metaInsertion{} + m.JSONBase.APIVersion = version + m.JSONBase.Kind = kind + return &m +} + +// Interpret returns the version and kind information from in, which must be +// a metaInsertion pointer object. +func (metaInsertion) Interpret(in interface{}) (version, kind string) { + m := in.(*metaInsertion) + return m.JSONBase.APIVersion, m.JSONBase.Kind +} diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go index 025d7c8cc8..c496bc5e84 100644 --- a/pkg/conversion/converter.go +++ b/pkg/conversion/converter.go @@ -88,21 +88,21 @@ func (c *Converter) Register(conversionFunc interface{}) error { type FieldMatchingFlags int const ( - // Loop through source fields, search for matching dest field - // to copy it into. Destination fields with no corresponding - // source field will be ignored. - SourceToDest FieldMatchingFlags = 1 << iota // Loop through destiation fields, search for matching source // field to copy it from. Source fields with no corresponding // destination field will be ignored. If SourceToDest is // specified, this flag is ignored. If niether is specified, - // this flag is the default. - DestFromSource + // or no flags are passed, this flag is the default. + DestFromSource FieldMatchingFlags = 0 + // Loop through source fields, search for matching dest field + // to copy it into. Destination fields with no corresponding + // source field will be ignored. + SourceToDest FieldMatchingFlags = 1 << iota // Don't treat it as an error if the corresponding source or // dest field can't be found. IgnoreMissingFields // Don't require type names to match. - AllowDifferentFieldNames + AllowDifferentFieldTypeNames ) // Returns true if the given flag or combination of flags is set. @@ -147,7 +147,7 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro return ret.(error) } - if !flags.IsSet(AllowDifferentFieldNames) && dt.Name() != st.Name() { + if !flags.IsSet(AllowDifferentFieldTypeNames) && dt.Name() != st.Name() { return fmt.Errorf("Can't convert %v to %v because type names don't match.", st, dt) } diff --git a/pkg/conversion/converter_test.go b/pkg/conversion/converter_test.go index c79e2455d8..60fbd8bdbc 100644 --- a/pkg/conversion/converter_test.go +++ b/pkg/conversion/converter_test.go @@ -18,10 +18,13 @@ package conversion import ( "fmt" + "reflect" "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) -func TestConverter(t *testing.T) { +func TestConverter_CallsRegisteredFunctions(t *testing.T) { type A struct { Foo string } @@ -79,3 +82,128 @@ func TestConverter(t *testing.T) { t.Errorf("unexpected non-error") } } + +func TestConverter_fuzz(t *testing.T) { + newAnonType := func() interface{} { + return reflect.New(reflect.TypeOf(externalTypeReturn())).Interface() + } + // Use the same types from the scheme test. + table := []struct { + from, to, check interface{} + }{ + {&TestType1{}, newAnonType(), &TestType1{}}, + {newAnonType(), &TestType1{}, newAnonType()}, + } + + f := util.NewFuzzer() + c := NewConverter() + + for i, item := range table { + for j := 0; j < *fuzzIters; j++ { + f.Fuzz(item.from) + err := c.Convert(item.from, item.to, 0) + if err != nil { + t.Errorf("(%v, %v): unexpected error: %v", i, j, err) + continue + } + err = c.Convert(item.to, item.check, 0) + if err != nil { + t.Errorf("(%v, %v): unexpected error: %v", i, j, err) + continue + } + if e, a := item.from, item.check; !reflect.DeepEqual(e, a) { + t.Errorf("(%v, %v): unexpected diff: %v", i, j, objDiff(e, a)) + } + } + } +} + +func TestConverter_flags(t *testing.T) { + type Foo struct{ A string } + type Bar struct{ A string } + table := []struct { + from, to interface{} + flags FieldMatchingFlags + shouldSucceed bool + }{ + // Check that DestFromSource allows extra fields only in source. + { + from: &struct{ A string }{}, + to: &struct{ A, B string }{}, + flags: DestFromSource, + shouldSucceed: false, + }, { + from: &struct{ A, B string }{}, + to: &struct{ A string }{}, + flags: DestFromSource, + shouldSucceed: true, + }, + + // Check that SourceToDest allows for extra fields only in dest. + { + from: &struct{ A string }{}, + to: &struct{ A, B string }{}, + flags: SourceToDest, + shouldSucceed: true, + }, { + from: &struct{ A, B string }{}, + to: &struct{ A string }{}, + flags: SourceToDest, + shouldSucceed: false, + }, + + // Check that IgnoreMissingFields makes the above failure cases pass. + { + from: &struct{ A string }{}, + to: &struct{ A, B string }{}, + flags: DestFromSource | IgnoreMissingFields, + shouldSucceed: true, + }, { + from: &struct{ A, B string }{}, + to: &struct{ A string }{}, + flags: SourceToDest | IgnoreMissingFields, + shouldSucceed: true, + }, + + // Check that the field type name must match unless + // AllowDifferentFieldTypeNames is specified. + { + from: &struct{ A, B Foo }{}, + to: &struct{ A Bar }{}, + flags: DestFromSource, + shouldSucceed: false, + }, { + from: &struct{ A Foo }{}, + to: &struct{ A, B Bar }{}, + flags: SourceToDest, + shouldSucceed: false, + }, { + from: &struct{ A, B Foo }{}, + to: &struct{ A Bar }{}, + flags: DestFromSource | AllowDifferentFieldTypeNames, + shouldSucceed: true, + }, { + from: &struct{ A Foo }{}, + to: &struct{ A, B Bar }{}, + flags: SourceToDest | AllowDifferentFieldTypeNames, + shouldSucceed: true, + }, + } + f := util.NewFuzzer() + c := NewConverter() + + for i, item := range table { + for j := 0; j < *fuzzIters; j++ { + f.Fuzz(item.from) + err := c.Convert(item.from, item.to, item.flags) + if item.shouldSucceed && err != nil { + t.Errorf("(%v, %v): unexpected error: %v", i, j, err) + continue + } + if !item.shouldSucceed && err == nil { + t.Errorf("(%v, %v): unexpected non-error", i, j) + continue + } + } + } +} diff --git a/pkg/conversion/decode.go b/pkg/conversion/decode.go index 7c0da976f3..530f74ab57 100644 --- a/pkg/conversion/decode.go +++ b/pkg/conversion/decode.go @@ -28,12 +28,12 @@ import ( // s.InternalVersion type before being returned. Decode will refuse to decode // objects without a version, because that's probably an error. func (s *Scheme) Decode(data []byte) (interface{}, error) { - version, kind, err := s.DataAPIVersionAndKind(data) + version, kind, err := s.DataVersionAndKind(data) if err != nil { return nil, err } if version == "" { - return nil, fmt.Errorf("API Version not set in '%s'", string(data)) + return nil, fmt.Errorf("version not set in '%s'", string(data)) } obj, err := s.NewObject(version, kind) if err != nil { @@ -47,8 +47,7 @@ func (s *Scheme) Decode(data []byte) (interface{}, error) { } // Version and Kind should be blank in memory. - blankVersionAndKind := s.MetaInsertionFactory.Create("", "") - err = s.converter.Convert(blankVersionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldNames) + err = s.SetVersionAndKind("", "", obj) if err != nil { return nil, err } @@ -71,14 +70,14 @@ func (s *Scheme) Decode(data []byte) (interface{}, error) { // DecodeInto parses a YAML or JSON string and stores it in obj. Returns an error // if data.Kind is set and doesn't match the type of obj. Obj should be a // pointer to an api type. -// If obj's APIVersion doesn't match that in data, an attempt will be made to convert +// If obj's version doesn't match that in data, an attempt will be made to convert // data into obj's version. func (s *Scheme) DecodeInto(data []byte, obj interface{}) error { - dataVersion, dataKind, err := s.DataAPIVersionAndKind(data) + dataVersion, dataKind, err := s.DataVersionAndKind(data) if err != nil { return err } - objVersion, objKind, err := s.ObjectAPIVersionAndKind(obj) + objVersion, objKind, err := s.ObjectVersionAndKind(obj) if err != nil { return err } @@ -120,10 +119,5 @@ func (s *Scheme) DecodeInto(data []byte, obj interface{}) error { } // Version and Kind should be blank in memory. - blankVersionAndKind := s.MetaInsertionFactory.Create("", "") - err = s.converter.Convert(blankVersionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldNames) - if err != nil { - return err - } - return nil + return s.SetVersionAndKind("", "", obj) } diff --git a/pkg/conversion/doc.go b/pkg/conversion/doc.go new file mode 100644 index 0000000000..aea8afa68c --- /dev/null +++ b/pkg/conversion/doc.go @@ -0,0 +1,34 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package conversion provides go object versioning and encoding/decoding +// mechanisms. +// +// Specifically, conversion provides a way for you to define multiple versions +// of the same object. You may write functions which implement conversion logic, +// but for the fields which did not change, copying is automated. This makes it +// easy to modify the structures you use in memory without affecting the format +// you store on disk or respond to in your external API calls. +// +// The second offering of this package is automated encoding/decoding. The version +// and type of the object is recorded in the output, so it can be recreated upon +// reading. Currently, conversion writes JSON output, and interprets both JSON +// and YAML input. +// +// In the future, we plan to more explicitly separate the above two mechanisms, and +// add more serialization options, such as gob. +// +package conversion diff --git a/pkg/conversion/encode.go b/pkg/conversion/encode.go index 29b0ab5a0f..554a6dc443 100644 --- a/pkg/conversion/encode.go +++ b/pkg/conversion/encode.go @@ -17,7 +17,6 @@ limitations under the License. package conversion import ( - "bytes" "encoding/json" "fmt" ) @@ -37,17 +36,17 @@ func (s *Scheme) EncodeOrDie(obj interface{}) string { // struct. The type must have been registered. // // Memory/wire format differences: -// * Having to keep track of the Kind and APIVersion fields makes tests +// * Having to keep track of the Kind and Version fields makes tests // very annoying, so the rule is that they are set only in wire format // (json), not when in native (memory) format. This is possible because // both pieces of information are implicit in the go typed object. // * An exception: note that, if there are embedded API objects of known // type, for example, PodList{... Items []Pod ...}, these embedded // objects must be of the same version of the object they are embedded -// within, and their APIVersion and Kind must both be empty. -// * Note that the exception does not apply to the APIObject type, which -// recursively does Encode()/Decode(), and is capable of expressing any -// API object. +// within, and their Version and Kind must both be empty. +// * Note that the exception does not apply to a generic APIObject type +// which recursively does Encode()/Decode(), and is capable of +// expressing any API object. // * Only versioned objects should be encoded. This means that, if you pass // a native object, Encode will convert it to a versioned object. For // example, an api.Pod will get converted to a v1beta1.Pod. However, if @@ -71,7 +70,7 @@ func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []by return nil, fmt.Errorf("type %v is not registered and it will be impossible to Decode it, therefore Encode will refuse to encode it.", v.Type()) } - objVersion, objKind, err := s.ObjectAPIVersionAndKind(obj) + objVersion, objKind, err := s.ObjectVersionAndKind(obj) if err != nil { return nil, err } @@ -90,8 +89,7 @@ func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []by } // Version and Kind should be set on the wire. - setVersionAndKind := s.MetaInsertionFactory.Create(destVersion, objKind) - err = s.converter.Convert(setVersionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldNames) + err = s.SetVersionAndKind(destVersion, objKind, obj) if err != nil { return nil, err } @@ -102,35 +100,12 @@ func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []by return nil, err } - // Version and Kind should be blank in memory. - blankVersionAndKind := s.MetaInsertionFactory.Create("", "") - err = s.converter.Convert(blankVersionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldNames) + // Version and Kind should be blank in memory. Reset them, since it's + // possible that we modified a user object and not a copy above. + err = s.SetVersionAndKind("", "", obj) if err != nil { return nil, err } return data, nil - - meta, err := json.Marshal(s.MetaInsertionFactory.Create(destVersion, objKind)) - if err != nil { - return nil, err - } - // Stick these together, omitting the last } from meta and the first { from - // data. Add a comma to meta if necessary. - metaN := len(meta) - if len(data) > 2 { - meta[metaN-1] = ',' // Add comma - } else { - meta = meta[:metaN-1] // Just remove } - } - together := append(meta, data[1:]...) - if s.Indent { - var out bytes.Buffer - err := json.Indent(&out, together, "", " ") - if err != nil { - return nil, err - } - return out.Bytes(), nil - } - return together, nil } diff --git a/pkg/conversion/scheme.go b/pkg/conversion/scheme.go index 7f49b0cee4..730328a9e9 100644 --- a/pkg/conversion/scheme.go +++ b/pkg/conversion/scheme.go @@ -25,7 +25,7 @@ import ( // MetaInsertionFactory is used to create an object to store and retrieve // the version and kind information for all objects. The default uses the -// keys "apiVersion" and "kind" respectively. The object produced by this +// keys "version" and "kind" respectively. The object produced by this // factory is used to clear the version and kind fields in memory, so it // must match the layout of your actual api structs. (E.g., if you have your // version and kind field inside an inlined struct, this must produce an @@ -34,15 +34,12 @@ type MetaInsertionFactory interface { // Create should make a new object with two fields. // This object will be used to encode this metadata along with your // API objects, so the tags on the fields you use shouldn't conflict. - Create(apiVersion, kind string) interface{} + Create(version, kind string) interface{} // Interpret should take the same type of object that Create creates. // It should return the version and kind information from this object. - Interpret(interface{}) (apiVersion, kind string) + Interpret(interface{}) (version, kind string) } -// Default is a global scheme. -var Default = NewScheme() - // Scheme defines an entire encoding and decoding scheme. type Scheme struct { // versionMap allows one to figure out the go type of an object with @@ -70,7 +67,7 @@ type Scheme struct { // MetaInsertionFactory is used to create an object to store and retrieve // the version and kind information for all objects. The default uses the - // keys "apiVersion" and "kind" respectively. + // keys "version" and "kind" respectively. MetaInsertionFactory MetaInsertionFactory } @@ -86,8 +83,10 @@ func NewScheme() *Scheme { } } -// AddKnownTypes registers the types of the arguments to the marshaller of the package api. -// Encode() refuses the object unless its type is registered with AddKnownTypes. +// AddKnownTypes registers all types passed in 'types' as being members of version 'version. +// Encode() will refuse objects unless their type has been registered with AddKnownTypes. +// All objects passed to types should be structs, not pointers to structs. The name that go +// reports for the struct becomes the "kind" field when encoding. func (s *Scheme) AddKnownTypes(version string, types ...interface{}) { knownTypes, found := s.versionMap[version] if !found { @@ -117,17 +116,18 @@ func (s *Scheme) NewObject(versionName, typeName string) (interface{}, error) { } // AddConversionFuncs adds functions to the list of conversion functions. The given -// functions should know how to convert between two API objects. We deduce how to call -// it from the types of its two parameters; see the comment for Converter.Register. +// functions should know how to convert between two of your API objects, or their +// 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 -// Convert() inside your conversionFuncs, as long as you don't start a conversion +// s.Convert() inside your conversionFuncs, as long as you don't start a conversion // chain that's infinitely recursive. // // Also note that the default behavior, if you don't add a conversion function, is to -// sanely copy fields that have the same names. It's OK if the destination type has -// extra fields, but it must not remove any. So you only need to add a conversion -// function for things with changed/removed fields. +// sanely copy fields that have the same names and same type names. It's OK if the +// destination type has extra fields, but it must not remove any. So you only need to +// add conversion functions for things with changed/removed fields. func (s *Scheme) AddConversionFuncs(conversionFuncs ...interface{}) error { for _, f := range conversionFuncs { err := s.converter.Register(f) @@ -138,8 +138,8 @@ func (s *Scheme) AddConversionFuncs(conversionFuncs ...interface{}) error { return nil } -// 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 +// 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. func (s *Scheme) Convert(in, out interface{}) error { return s.converter.Convert(in, out, 0) @@ -147,32 +147,30 @@ func (s *Scheme) Convert(in, out interface{}) error { // metaInsertion provides a default implementation of MetaInsertionFactory. type metaInsertion struct { - JSONBase struct { - APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` - Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` - } `json:",inline" yaml:",inline"` + Version string `json:"version,omitempty" yaml:"version,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` } // Create should make a new object with two fields. // This object will be used to encode this metadata along with your // API objects, so the tags on the fields you use shouldn't conflict. -func (metaInsertion) Create(apiVersion, kind string) interface{} { +func (metaInsertion) Create(version, kind string) interface{} { m := metaInsertion{} - m.JSONBase.APIVersion = apiVersion - m.JSONBase.Kind = kind + m.Version = version + m.Kind = kind return &m } // Interpret should take the same type of object that Create creates. // It should return the version and kind information from this object. -func (metaInsertion) Interpret(in interface{}) (apiVersion, kind string) { +func (metaInsertion) Interpret(in interface{}) (version, kind string) { m := in.(*metaInsertion) - return m.JSONBase.APIVersion, m.JSONBase.Kind + return m.Version, m.Kind } // DataAPIVersionAndKind will return the APIVersion and Kind of the given wire-format // enconding of an API Object, or an error. -func (s *Scheme) DataAPIVersionAndKind(data []byte) (apiVersion, kind string, err error) { +func (s *Scheme) DataVersionAndKind(data []byte) (version, kind string, err error) { findKind := s.MetaInsertionFactory.Create("", "") // yaml is a superset of json, so we use it to decode here. That way, // we understand both. @@ -180,13 +178,13 @@ func (s *Scheme) DataAPIVersionAndKind(data []byte) (apiVersion, kind string, er if err != nil { return "", "", fmt.Errorf("couldn't get version/kind: %v", err) } - apiVersion, kind = s.MetaInsertionFactory.Interpret(findKind) - return apiVersion, kind, nil + version, kind = s.MetaInsertionFactory.Interpret(findKind) + return version, kind, nil } // ObjectVersionAndKind returns the API version and kind of the go object, // or an error if it's not a pointer or is unregistered. -func (s *Scheme) ObjectAPIVersionAndKind(obj interface{}) (apiVersion, kind string, err error) { +func (s *Scheme) ObjectVersionAndKind(obj interface{}) (apiVersion, kind string, err error) { v, err := enforcePtr(obj) if err != nil { return "", "", err @@ -199,6 +197,14 @@ func (s *Scheme) ObjectAPIVersionAndKind(obj interface{}) (apiVersion, kind stri } } +// SetVersionAndKind sets the version and kind fields (with help from +// MetaInsertionFactory). Returns an error if this isn't possible. obj +// 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) +} + // maybeCopy copies obj if it is not a pointer, to get a settable/addressable // object. Guaranteed to return a pointer. func maybeCopy(obj interface{}) interface{} { diff --git a/pkg/conversion/scheme_test.go b/pkg/conversion/scheme_test.go index ad5aa610bd..642585cc21 100644 --- a/pkg/conversion/scheme_test.go +++ b/pkg/conversion/scheme_test.go @@ -28,31 +28,33 @@ import ( var fuzzIters = flag.Int("fuzz_iters", 10, "How many fuzzing iterations to do.") -// Intended to be compatible with the default implementation of MetaInsertionFactory -type JSONBase struct { +// Test a weird version/kind embedding format. +type MyWeirdCustomEmbeddedVersionKindField struct { ID string `yaml:"ID,omitempty" json:"ID,omitempty"` - APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` - Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + APIVersion string `json:"myVersionKey,omitempty" yaml:"myVersionKey,omitempty"` + ObjectKind string `json:"myKindKey,omitempty" yaml:"myKindKey,omitempty"` + Z string `yaml:"Z,omitempty" json:"Z,omitempty"` + Y uint64 `yaml:"Y,omitempty" json:"Y,omitempty"` } type TestType1 struct { - JSONBase `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"` + 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"` } type TestType2 struct { @@ -69,39 +71,39 @@ func externalTypeReturn() interface{} { B int `yaml:"B,omitempty" json:"B,omitempty"` } type TestType1 struct { - JSONBase `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"` + 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 ExternalInternalSame struct { - JSONBase `json:",inline" yaml:",inline"` - A TestType2 `yaml:"A,omitempty" json:"A,omitempty"` + MyWeirdCustomEmbeddedVersionKindField `json:",inline" yaml:",inline"` + A TestType2 `yaml:"A,omitempty" json:"A,omitempty"` } // TestObjectFuzzer can randomly populate all the above objects. var TestObjectFuzzer = util.NewFuzzer( - func(j *JSONBase) { - // We have to customize the randomization of JSONBases because their + func(j *MyWeirdCustomEmbeddedVersionKindField) { + // We have to customize the randomization of MyWeirdCustomEmbeddedVersionKindFields because their // APIVersion and Kind must remain blank in memory. j.APIVersion = "" - j.Kind = "" + j.ObjectKind = "" j.ID = util.RandString() }, func(u *uint64) { @@ -125,9 +127,32 @@ func GetTestScheme() *Scheme { s.AddKnownTypes("v1", externalTypeReturn(), ExternalInternalSame{}) s.ExternalVersion = "v1" s.InternalVersion = "" + s.MetaInsertionFactory = testMetaInsertionFactory{} return s } +type testMetaInsertionFactory struct { + MyWeirdCustomEmbeddedVersionKindField struct { + APIVersion string `json:"myVersionKey,omitempty" yaml:"myVersionKey,omitempty"` + ObjectKind string `json:"myKindKey,omitempty" yaml:"myKindKey,omitempty"` + } `json:",inline" yaml:",inline"` +} + +// Create returns a new testMetaInsertionFactory with the version and kind fields set. +func (testMetaInsertionFactory) Create(version, kind string) interface{} { + m := testMetaInsertionFactory{} + m.MyWeirdCustomEmbeddedVersionKindField.APIVersion = version + m.MyWeirdCustomEmbeddedVersionKindField.ObjectKind = kind + return &m +} + +// Interpret returns the version and kind information from in, which must be +// a testMetaInsertionFactory pointer object. +func (testMetaInsertionFactory) Interpret(in interface{}) (version, kind string) { + m := in.(*testMetaInsertionFactory) + return m.MyWeirdCustomEmbeddedVersionKindField.APIVersion, m.MyWeirdCustomEmbeddedVersionKindField.ObjectKind +} + func objDiff(a, b interface{}) string { ab, err := json.Marshal(a) if err != nil { @@ -162,22 +187,20 @@ func runTest(t *testing.T, source interface{}) { if err != nil { t.Errorf("%v: %v (%v)", name, err, string(data)) return - } else { - if !reflect.DeepEqual(source, obj2) { - t.Errorf("1: %v: diff: %v", name, objDiff(source, obj2)) - return - } + } + if !reflect.DeepEqual(source, obj2) { + t.Errorf("1: %v: diff: %v", name, objDiff(source, obj2)) + return } obj3 := reflect.New(reflect.TypeOf(source).Elem()).Interface() err = s.DecodeInto(data, obj3) if err != nil { t.Errorf("2: %v: %v", name, err) return - } else { - if !reflect.DeepEqual(source, obj3) { - t.Errorf("3: %v: diff: %v", name, objDiff(source, obj3)) - return - } + } + if !reflect.DeepEqual(source, obj3) { + t.Errorf("3: %v: diff: %v", name, objDiff(source, obj3)) + return } } @@ -231,17 +254,17 @@ func TestEncode_Ptr(t *testing.T) { func TestBadJSONRejection(t *testing.T) { s := GetTestScheme() badJSONs := [][]byte{ - []byte(`{"apiVersion":"v1"}`), // Missing kind - []byte(`{"kind":"TestType1"}`), // Missing version - []byte(`{"apiVersion":"v1","kind":"bar"}`), // Unknown kind - []byte(`{"apiVersion":"bar","kind":"TestType1"}`), // Unknown version + []byte(`{"myVersionKey":"v1"}`), // Missing kind + []byte(`{"myKindKey":"TestType1"}`), // Missing version + []byte(`{"myVersionKey":"v1","myKindKey":"bar"}`), // Unknown kind + []byte(`{"myVersionKey":"bar","myKindKey":"TestType1"}`), // Unknown version } for _, b := range badJSONs { if _, err := s.Decode(b); err == nil { t.Errorf("Did not reject bad json: %s", string(b)) } } - badJSONKindMismatch := []byte(`{"apiVersion":"v1","kind": "ExternalInternalSame"}`) + badJSONKindMismatch := []byte(`{"myVersionKey":"v1","myKindKey":"ExternalInternalSame"}`) if err := s.DecodeInto(badJSONKindMismatch, &TestType1{}); err == nil { t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch) }