From f1c21c8fdf4a2a05c3f8444e6774e48c24ea9a63 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Fri, 21 Nov 2014 22:49:50 -0800 Subject: [PATCH] add boilerplate reduction struct field copier --- pkg/conversion/converter.go | 170 ++++++++++++++++++++++++------- pkg/conversion/converter_test.go | 92 +++++++++++++++++ pkg/conversion/scheme.go | 8 ++ 3 files changed, 235 insertions(+), 35 deletions(-) diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go index f66c866d804..920bf3e7afd 100644 --- a/pkg/conversion/converter.go +++ b/pkg/conversion/converter.go @@ -26,6 +26,11 @@ type typePair struct { dest reflect.Type } +type typeNamePair struct { + fieldType reflect.Type + fieldName string +} + // DebugLogger allows you to get debugging messages if necessary. type DebugLogger interface { Logf(format string, args ...interface{}) @@ -37,6 +42,15 @@ type Converter struct { // do the conversion. funcs map[typePair]reflect.Value + // This is a map from a source field type and name, to a list of destination + // field type and name. + structFieldDests map[typeNamePair][]typeNamePair + + // Allows for the opposite lookup of structFieldDests. So that SourceFromDest + // copy flag also works. So this is a map of destination field name, to potential + // source field name and type to look for. + structFieldSources map[typeNamePair][]typeNamePair + // If non-nil, will be called to print helpful debugging info. Quite verbose. Debug DebugLogger @@ -49,8 +63,10 @@ type Converter struct { // NewConverter creates a new Converter object. func NewConverter() *Converter { return &Converter{ - funcs: map[typePair]reflect.Value{}, - NameFunc: func(t reflect.Type) string { return t.Name() }, + funcs: map[typePair]reflect.Value{}, + NameFunc: func(t reflect.Type) string { return t.Name() }, + structFieldDests: map[typeNamePair][]typeNamePair{}, + structFieldSources: map[typeNamePair][]typeNamePair{}, } } @@ -177,6 +193,22 @@ func (c *Converter) Register(conversionFunc interface{}) error { return nil } +// SetStructFieldCopy registers a correspondence. Whenever a struct field is encountered +// which has a type and name matching srcFieldType and srcFieldName, it wil be copied +// into the field in the destination struct matching destFieldType & Name, if such a +// field exists. +// May be called multiple times, even for the same source field & type--all applicable +// copies will be performed. +func (c *Converter) SetStructFieldCopy(srcFieldType interface{}, srcFieldName string, destFieldType interface{}, destFieldName string) error { + st := reflect.TypeOf(srcFieldType) + dt := reflect.TypeOf(destFieldType) + srcKey := typeNamePair{st, srcFieldName} + destKey := typeNamePair{dt, destFieldName} + c.structFieldDests[srcKey] = append(c.structFieldDests[srcKey], destKey) + c.structFieldSources[destKey] = append(c.structFieldSources[destKey], srcKey) + return nil +} + // FieldMatchingFlags contains a list of ways in which struct fields could be // copied. These constants may be | combined. type FieldMatchingFlags int @@ -201,6 +233,10 @@ const ( // IsSet returns true if the given flag or combination of flags is set. func (f FieldMatchingFlags) IsSet(flag FieldMatchingFlags) bool { + if flag == DestFromSource { + // The bit logic doesn't work on the default value. + return f&SourceToDest != SourceToDest + } return f&flag == flag } @@ -274,39 +310,7 @@ func (c *Converter) convert(sv, dv reflect.Value, scope *scope) error { switch dv.Kind() { case reflect.Struct: - listType := dt - 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 scope.flags.IsSet(IgnoreMissingFields): - // No error. - 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, scope); err != nil { - return err - } - } + return c.convertStruct(sv, dv, scope) case reflect.Slice: if sv.IsNil() { // Don't make a zero-length slice. @@ -350,3 +354,99 @@ func (c *Converter) convert(sv, dv reflect.Value, scope *scope) error { } return nil } + +func (c *Converter) convertStruct(sv, dv reflect.Value, scope *scope) error { + dt, st := dv.Type(), sv.Type() + + listType := dt + if scope.flags.IsSet(SourceToDest) { + listType = st + } + for i := 0; i < listType.NumField(); i++ { + f := listType.Field(i) + if found, err := c.checkStructField(f.Name, sv, dv, scope); found { + if err != nil { + return err + } + continue + } + 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 scope.flags.IsSet(IgnoreMissingFields): + // No error. + 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, scope); err != nil { + return err + } + } + return nil +} + +// checkStructField returns true if the field name matches any of the struct +// field copying rules. The error should be ignored if it returns false. +func (c *Converter) checkStructField(fieldName string, sv, dv reflect.Value, scope *scope) (bool, error) { + replacementMade := false + if scope.flags.IsSet(DestFromSource) { + df := dv.FieldByName(fieldName) + if !df.IsValid() { + return false, nil + } + destKey := typeNamePair{df.Type(), fieldName} + // Check each of the potential source (type, name) pairs to see if they're + // present in sv. + for _, potentialSourceKey := range c.structFieldSources[destKey] { + sf := sv.FieldByName(potentialSourceKey.fieldName) + if !sf.IsValid() { + continue + } + if sf.Type() == potentialSourceKey.fieldType { + // Both the source's name and type matched, so copy. + if err := c.convert(sf, df, scope); err != nil { + return true, err + } + replacementMade = true + } + } + return replacementMade, nil + } + + sf := sv.FieldByName(fieldName) + if !sf.IsValid() { + return false, nil + } + srcKey := typeNamePair{sf.Type(), fieldName} + // Check each of the potential dest (type, name) pairs to see if they're + // present in dv. + for _, potentialDestKey := range c.structFieldDests[srcKey] { + df := dv.FieldByName(potentialDestKey.fieldName) + if !df.IsValid() { + continue + } + if df.Type() == potentialDestKey.fieldType { + // Both the dest's name and type matched, so copy. + if err := c.convert(sf, df, scope); err != nil { + return true, err + } + replacementMade = true + } + } + return replacementMade, nil +} diff --git a/pkg/conversion/converter_test.go b/pkg/conversion/converter_test.go index 573c9791ded..8fd45a66c31 100644 --- a/pkg/conversion/converter_test.go +++ b/pkg/conversion/converter_test.go @@ -290,3 +290,95 @@ func TestConverter_flags(t *testing.T) { } } } + +func TestConverter_FieldRename(t *testing.T) { + type WeirdMeta struct { + Name string + Type string + } + type NameMeta struct { + Name string + } + type TypeMeta struct { + Type string + } + type A struct { + WeirdMeta + } + type B struct { + TypeMeta + NameMeta + } + + c := NewConverter() + err := c.SetStructFieldCopy(WeirdMeta{}, "WeirdMeta", TypeMeta{}, "TypeMeta") + if err != nil { + t.Fatalf("unexpected error %v", err) + } + err = c.SetStructFieldCopy(WeirdMeta{}, "WeirdMeta", NameMeta{}, "NameMeta") + if err != nil { + t.Fatalf("unexpected error %v", err) + } + err = c.SetStructFieldCopy(TypeMeta{}, "TypeMeta", WeirdMeta{}, "WeirdMeta") + if err != nil { + t.Fatalf("unexpected error %v", err) + } + err = c.SetStructFieldCopy(NameMeta{}, "NameMeta", WeirdMeta{}, "WeirdMeta") + if err != nil { + t.Fatalf("unexpected error %v", err) + } + c.Debug = t + + aVal := &A{ + WeirdMeta: WeirdMeta{ + Name: "Foo", + Type: "Bar", + }, + } + + bVal := &B{ + TypeMeta: TypeMeta{"Bar"}, + NameMeta: NameMeta{"Foo"}, + } + + table := map[string]struct { + from, to, expect interface{} + flags FieldMatchingFlags + }{ + "to": { + aVal, + &B{}, + bVal, + AllowDifferentFieldTypeNames | SourceToDest | IgnoreMissingFields, + }, + "from": { + bVal, + &A{}, + aVal, + AllowDifferentFieldTypeNames | SourceToDest, + }, + "toDestFirst": { + aVal, + &B{}, + bVal, + AllowDifferentFieldTypeNames, + }, + "fromDestFirst": { + bVal, + &A{}, + aVal, + AllowDifferentFieldTypeNames | IgnoreMissingFields, + }, + } + + for name, item := range table { + err := c.Convert(item.from, item.to, item.flags, nil) + if err != nil { + t.Errorf("%v: unexpected error: %v", name, err) + continue + } + if e, a := item.expect, item.to; !reflect.DeepEqual(e, a) { + t.Errorf("%v: unexpected diff: %v", name, objDiff(e, a)) + } + } +} diff --git a/pkg/conversion/scheme.go b/pkg/conversion/scheme.go index f93ef42ca17..a42c83ed32e 100644 --- a/pkg/conversion/scheme.go +++ b/pkg/conversion/scheme.go @@ -193,6 +193,14 @@ func (s *Scheme) AddConversionFuncs(conversionFuncs ...interface{}) error { return nil } +// AddStructFieldConversion allows you to specify a mechanical copy for a moved +// or renamed struct field without writing an entire conversion function. See +// the comment in Converter.SetStructFieldCopy for parameter details. +// Call as many times as needed, even on the same fields. +func (s *Scheme) AddStructFieldConversion(srcFieldType interface{}, srcFieldName string, destFieldType interface{}, destFieldName string) error { + return s.converter.SetStructFieldCopy(srcFieldType, srcFieldName, destFieldType, destFieldName) +} + // 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. You can call this with types that haven't been registered (for example,