add boilerplate reduction struct field copier

This commit is contained in:
Daniel Smith 2014-11-21 22:49:50 -08:00
parent 518115c93d
commit f1c21c8fdf
3 changed files with 235 additions and 35 deletions

View File

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

View File

@ -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))
}
}
}

View File

@ -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,