Skip to content

Commit

Permalink
Add WithSrcTag option.
Browse files Browse the repository at this point in the history
  • Loading branch information
zhuliquan committed Aug 19, 2023
1 parent f9573a9 commit 88c57d8
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 14 deletions.
37 changes: 25 additions & 12 deletions copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand Down Expand Up @@ -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() {
Expand All @@ -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

Expand Down
72 changes: 70 additions & 2 deletions copy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}

0 comments on commit 88c57d8

Please sign in to comment.