diff --git a/copy.go b/copy.go index b24b6dd..31b9687 100644 --- a/copy.go +++ b/copy.go @@ -2,11 +2,12 @@ package fieldmask_utils import ( + "reflect" + "strings" + "github.com/pkg/errors" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" - "reflect" - "strings" ) // StructToStruct copies `src` struct to `dst` struct using the given FieldFilter. @@ -71,16 +72,16 @@ func structToStruct(filter FieldFilter, src, dst *reflect.Value, userOptions *op for i := 0; i < src.NumField(); i++ { srcType := src.Type() - fieldName := srcType.Field(i).Name - dstName := dstKey(userOptions.DstTag, srcType.Field(i)) + srcName := fieldName(userOptions.SrcTag, srcType.Field(i)) + dstName := fieldName(userOptions.DstTag, srcType.Field(i)) - subFilter, ok := filter.Filter(fieldName) + subFilter, ok := filter.Filter(srcName) if !ok { // Skip this field. continue } - srcField := src.FieldByName(fieldName) + srcField := src.Field(i) if !srcField.CanInterface() { continue } @@ -235,8 +236,12 @@ func structToStruct(filter FieldFilter, src, dst *reflect.Value, userOptions *op // options are used in StructToStruct and StructToMap functions to modify the copying behavior. type options struct { + // DstTag can be used to customize the dst field name according to the field's tag, i.g. json. DstTag string + // SrcTag can be used to customize the src field name according to the field's tag, i.g. json. + SrcTag string + // CopyListSize can control the number of elements copied from src depending on src's Value CopyListSize func(src *reflect.Value) int @@ -267,6 +272,13 @@ func WithTag(s string) Option { } } +// WithSrcTag sets an option that gets the source field name from the field's tag. +func WithSrcTag(s string) Option { + return func(o *options) { + o.SrcTag = s + } +} + // WithCopyListSize sets CopyListSize func you can set copy size according to src. func WithCopyListSize(f func(src *reflect.Value) int) Option { return func(o *options) { @@ -286,7 +298,8 @@ func newDefaultOptions() *options { return &options{CopyListSize: func(src *reflect.Value) int { return src.Len() }} } -func dstKey(tag string, f reflect.StructField) string { +// fieldName gets the field name according to the field's tag, or gets StructField.Name default when the field's tag is empty. +func fieldName(tag string, f reflect.StructField) string { if tag == "" { return f.Name } @@ -321,19 +334,19 @@ func structToMap(filter FieldFilter, src, dst reflect.Value, userOptions *option } srcType := src.Type() for i := 0; i < src.NumField(); i++ { - fieldName := srcType.Field(i).Name + srcName := fieldName(userOptions.SrcTag, srcType.Field(i)) if !isExported(srcType.Field(i)) { // Unexported fields can not be copied. continue } - subFilter, ok := filter.Filter(fieldName) + subFilter, ok := filter.Filter(srcName) if !ok { // Skip this field. continue } - srcField := indirect(src.FieldByName(fieldName)) - dstName := dstKey(userOptions.DstTag, srcType.Field(i)) + srcField := indirect(src.Field(i)) + dstName := fieldName(userOptions.DstTag, srcType.Field(i)) mapValue := indirect(dst.MapIndex(reflect.ValueOf(dstName))) if !mapValue.IsValid() { if srcField.IsValid() { @@ -345,7 +358,7 @@ func structToMap(filter FieldFilter, src, dst reflect.Value, userOptions *option } } if userOptions.MapVisitor != nil { - result := userOptions.MapVisitor(filter, src, mapValue, fieldName, dstName, srcField) + result := userOptions.MapVisitor(filter, src, mapValue, srcName, dstName, srcField) if result.UpdatedDst != nil { mapValue = *result.UpdatedDst diff --git a/copy_test.go b/copy_test.go index 8e827be..c75d418 100644 --- a/copy_test.go +++ b/copy_test.go @@ -3,12 +3,13 @@ package fieldmask_utils_test import ( "encoding/json" "fmt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "reflect" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + fieldmask_utils "github.com/mennanov/fieldmask-utils" ) @@ -2198,3 +2199,70 @@ func TestStructToStruct_CopyArraySizeAccordingFieldName(t *testing.T) { Field2: [3]int{1, 2, 3}, }, dst) } + +func TestStructToStruct_WithSrcTag(t *testing.T) { + type A struct { + Field1 string + Field2 int `db:"some_field"` + } + type B struct { + Field1 string `struct:"a_name"` + A A `db:"another_name,omitempty"` + } + src := &B{ + Field1: "B Field1", + A: A{ + Field1: "A Field 1", + Field2: 1, + }, + } + dst := &B{} + mask := fieldmask_utils.MaskFromString("Field1,A{Field2}") + err := fieldmask_utils.StructToStruct(mask, src, dst, fieldmask_utils.WithSrcTag("db")) + require.NoError(t, err) + assert.Equal(t, &B{Field1: src.Field1}, dst) + + mask, _ = fieldmask_utils.MaskFromPaths([]string{"Field1", "another_name.some_field"}, func(s string) string { return s }) + dst = &B{} + err = fieldmask_utils.StructToStruct(mask, src, dst, fieldmask_utils.WithSrcTag("db")) + require.NoError(t, err) + assert.Equal(t, &B{Field1: src.Field1, A: A{Field2: src.A.Field2}}, dst) +} + +func TestStructToMap_WithSrcTag(t *testing.T) { + type A struct { + Field1 string + Field2 int `db:"some_field1" json:"some_field1_json"` + Field3 bool `db:"some_field2" json:"some_field2_json"` + } + type B struct { + Field1 string `struct:"a_name"` + A A `db:"another_name,omitempty" json:"another_name_json"` + } + src := &B{ + Field1: "B Field1", + A: A{ + Field1: "A Field 1", + Field2: 1, + }, + } + mask := fieldmask_utils.MaskFromString("Field1,A{Field2}") + dst := make(map[string]interface{}) + err := fieldmask_utils.StructToMap(mask, src, dst, fieldmask_utils.WithTag("json"), fieldmask_utils.WithSrcTag("db")) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{ + "Field1": src.Field1, + }, dst) + + mask, _ = fieldmask_utils.MaskFromPaths([]string{"Field1", "another_name.some_field1", "another_name.some_field2"}, func(s string) string { return s }) + dst = make(map[string]interface{}) + err = fieldmask_utils.StructToMap(mask, src, dst, fieldmask_utils.WithTag("json"), fieldmask_utils.WithSrcTag("db")) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{ + "Field1": src.Field1, + "another_name_json": map[string]interface{}{ + "some_field1_json": src.A.Field2, + "some_field2_json": false, + }, + }, dst) +}