struct slice map
This commit is contained in:
146
copier.go
146
copier.go
@@ -3,20 +3,21 @@ package copier
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Copy(src, dst any, opts ...option) error {
|
||||
func Copy(dst, src any, opts ...option) error {
|
||||
opt := getOpt(opts...)
|
||||
return copy(src, dst, opt)
|
||||
return copy(dst, src, opt)
|
||||
}
|
||||
|
||||
func copy(src, dst any, opt *options) error {
|
||||
func copy(dst, src any, opt *options) error {
|
||||
var (
|
||||
srcValue = indirect(reflect.ValueOf(dst))
|
||||
dstValue = indirect(reflect.ValueOf(src))
|
||||
srcValue = indirect(reflect.ValueOf(src))
|
||||
dstValue = indirect(reflect.ValueOf(dst))
|
||||
)
|
||||
|
||||
if !dstValue.IsValid() {
|
||||
if !srcValue.IsValid() {
|
||||
return ErrInvalidCopyFrom
|
||||
}
|
||||
|
||||
@@ -24,60 +25,94 @@ func copy(src, dst any, opt *options) error {
|
||||
return ErrInvalidCopyDestination
|
||||
}
|
||||
|
||||
return deepCopy(srcValue, dstValue, 0, opt)
|
||||
return deepCopy(dstValue, srcValue, 0, opt)
|
||||
}
|
||||
|
||||
func deepCopy(dst, src reflect.Value, depth int, opt *options) error {
|
||||
switch dst.Kind() {
|
||||
if src.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if opt.ExceedMaxDepth(depth) {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch src.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
// slice -> slice
|
||||
return copySlice(src, dst, depth, opt)
|
||||
return copySlice(dst, src, depth, opt)
|
||||
case reflect.Map:
|
||||
switch src.Kind() {
|
||||
switch dst.Kind() {
|
||||
case reflect.Map:
|
||||
// map -> map
|
||||
return copyMap(src, dst, depth, opt)
|
||||
return copyMap(dst, src, depth, opt)
|
||||
case reflect.Struct:
|
||||
// struct -> map
|
||||
return copyStruct2Map(src, dst, depth, opt)
|
||||
return copyStruct2Map(dst, src, depth, opt)
|
||||
default:
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
switch src.Kind() {
|
||||
switch dst.Kind() {
|
||||
case reflect.Map:
|
||||
// map -> struct
|
||||
return copyMap2Struct(src, dst, depth, opt)
|
||||
return copyMap2Struct(dst, src, depth, opt)
|
||||
case reflect.Struct:
|
||||
// struct -> struct
|
||||
return copyStruct(src, dst, depth, opt)
|
||||
return copyStruct(dst, src, depth, opt)
|
||||
default:
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
case reflect.Func:
|
||||
if dst.Kind() == reflect.Func && dst.IsNil() {
|
||||
dst.Set(src)
|
||||
}
|
||||
return nil
|
||||
case reflect.Pointer:
|
||||
if dst.IsNil() {
|
||||
if dst.Kind() == reflect.Pointer && dst.IsNil() {
|
||||
if !dst.CanSet() {
|
||||
return ErrInvalidCopyDestination
|
||||
return nil
|
||||
}
|
||||
p := reflect.New(dst.Type().Elem())
|
||||
dst.Set(p)
|
||||
}
|
||||
|
||||
src = src.Elem()
|
||||
dst = dst.Elem()
|
||||
if src.Kind() == reflect.Pointer {
|
||||
src = indirect(src)
|
||||
}
|
||||
|
||||
if dst.Kind() == reflect.Pointer {
|
||||
dst = dst.Elem()
|
||||
}
|
||||
|
||||
return deepCopy(dst, src, depth, opt)
|
||||
|
||||
case reflect.Interface:
|
||||
if src.Kind() != dst.Kind() {
|
||||
return nil
|
||||
}
|
||||
|
||||
src = src.Elem()
|
||||
newDst := reflect.New(src.Type().Elem())
|
||||
|
||||
if err := deepCopy(newDst, src, depth, opt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dst.Set(newDst)
|
||||
return nil
|
||||
default:
|
||||
return copyDefault(src, dst)
|
||||
return copyDefault(dst, src)
|
||||
}
|
||||
}
|
||||
|
||||
func copyStruct2Map(src, dst reflect.Value, depth int, opt *options) error {
|
||||
func copyStruct2Map(dst, src reflect.Value, depth int, opt *options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyMap2Struct(src, dst reflect.Value, depth int, opt *options) error {
|
||||
func copyMap2Struct(dst, src reflect.Value, depth int, opt *options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -86,9 +121,26 @@ func copyMap(dst, src reflect.Value, depth int, opt *options) error {
|
||||
dst.Set(reflect.MakeMapWithSize(dst.Type(), src.Len()))
|
||||
}
|
||||
|
||||
for _, key := range src.MapKeys() {
|
||||
value := src.MapIndex(key)
|
||||
dst.SetMapIndex(key, value)
|
||||
dstType, _ := indirectType(dst.Type())
|
||||
|
||||
iter := src.MapRange()
|
||||
for iter.Next() {
|
||||
key := iter.Key()
|
||||
value := iter.Value()
|
||||
|
||||
var copitedValue reflect.Value
|
||||
switch dstType.Elem().Kind() {
|
||||
case reflect.Interface:
|
||||
copitedValue = reflect.New(value.Type()).Elem()
|
||||
default:
|
||||
copitedValue = reflect.New(dstType.Elem()).Elem()
|
||||
}
|
||||
|
||||
if err := deepCopy(copitedValue, value, depth+1, opt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dst.SetMapIndex(key, copitedValue)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -116,27 +168,48 @@ func copySlice(dst, src reflect.Value, depth int, opt *options) error {
|
||||
}
|
||||
|
||||
func copyStruct(dst, src reflect.Value, depth int, opt *options) error {
|
||||
typ := src.Type()
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
sf := typ.Field(i)
|
||||
if dst.CanSet() {
|
||||
if _, ok := src.Interface().(time.Time); ok {
|
||||
dst.Set(src)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
dstValue := fieldByName(dst, sf.Name, opt)
|
||||
typ := src.Type()
|
||||
for i, n := 0, src.NumField(); i < n; i++ {
|
||||
sf := typ.Field(i)
|
||||
if sf.PkgPath != "" && !sf.Anonymous {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := parseTag(sf.Tag.Get(opt.tagName))
|
||||
if tag.Contains(tagIgnore) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := toName(sf.Name, tag, opt)
|
||||
|
||||
dstValue := fieldByName(dst, name, opt) // fieldByName(dst, name, opt)
|
||||
if !dstValue.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := deepCopy(dstValue, src.Field(i), depth+1, opt); err != nil {
|
||||
sField := src.Field(i)
|
||||
if opt.ignoreEmpty && sField.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := deepCopy(dstValue, sField, depth+1, opt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDefault(dst, src reflect.Value) error {
|
||||
|
||||
if src.Type().AssignableTo(dst.Type()) || dst.Type().ConvertibleTo(src.Type()) {
|
||||
if src.Type().AssignableTo(dst.Type()) || src.Type().ConvertibleTo(dst.Type()) {
|
||||
dst.Set(src.Convert(dst.Type()))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch dst.Kind() {
|
||||
@@ -145,6 +218,13 @@ func copyDefault(dst, src reflect.Value) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
func toName(name string, tag *tagOption, opt *options) string {
|
||||
if tag != nil && tag.Contains(tagToName) {
|
||||
return tag.toname
|
||||
}
|
||||
|
||||
return opt.NameConvert(name)
|
||||
}
|
||||
|
||||
func fieldByName(v reflect.Value, name string, opt *options) reflect.Value {
|
||||
if opt.caseSensitive {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/charlienet/go-misc/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -20,7 +21,34 @@ func TestCopyMap(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
dst := map[string]any{}
|
||||
dst := map[string]*person{}
|
||||
|
||||
assert.NoError(t, Copy(&dst, src))
|
||||
|
||||
fmt.Println(src, dst)
|
||||
|
||||
fmt.Println(json.Struct2JsonIndent(dst))
|
||||
}
|
||||
|
||||
func TestDiffStructMapCopy(t *testing.T) {
|
||||
type person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
type user struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
src := map[string]*person{
|
||||
"John": {
|
||||
Name: "John",
|
||||
Age: 30,
|
||||
},
|
||||
}
|
||||
|
||||
dst := map[string]*user{}
|
||||
|
||||
assert.NoError(t, Copy(&dst, src))
|
||||
|
||||
|
||||
223
copier_test.go
223
copier_test.go
@@ -1,2 +1,225 @@
|
||||
package copier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
Age int ``
|
||||
Name string ``
|
||||
Address Address ``
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
Country string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person2 struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
func TestCopySameStruct(t *testing.T) {
|
||||
src := &Person{
|
||||
Name: "John",
|
||||
Age: 30,
|
||||
Address: Address{
|
||||
Country: "USA",
|
||||
City: "New York",
|
||||
},
|
||||
}
|
||||
|
||||
var dst Person
|
||||
|
||||
assert.NoError(t, Copy(&dst, src))
|
||||
|
||||
if dst.Name != src.Name || dst.Age != src.Age {
|
||||
t.Errorf("Expected %v, got %v", src, dst)
|
||||
}
|
||||
|
||||
fmt.Println("src,dst:", src, dst)
|
||||
}
|
||||
|
||||
func TestCopyDiffStruct(t *testing.T) {
|
||||
src := &Person{
|
||||
Name: "John",
|
||||
Age: 30,
|
||||
Address: Address{
|
||||
Country: "USA",
|
||||
City: "New York",
|
||||
},
|
||||
}
|
||||
|
||||
var dst Person2
|
||||
|
||||
assert.NoError(t, Copy(&dst, src))
|
||||
t.Log("src,dst:", src, dst)
|
||||
}
|
||||
|
||||
func TestCopyPtrStruct(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Address *Address
|
||||
}
|
||||
|
||||
src := User{
|
||||
Name: "John",
|
||||
Age: 30,
|
||||
Address: &Address{
|
||||
Country: "USA",
|
||||
City: "New York",
|
||||
},
|
||||
}
|
||||
|
||||
var dst User
|
||||
|
||||
assert.NoError(t, Copy(&dst, src))
|
||||
assert.Equal(t, src, dst)
|
||||
t.Log("src,dst:", src, dst)
|
||||
}
|
||||
|
||||
func TestAnonymousFields(t *testing.T) {
|
||||
t.Run("Should work with unexported ptr fields", func(t *testing.T) {
|
||||
type nested struct {
|
||||
A string
|
||||
}
|
||||
type parentA struct {
|
||||
*nested
|
||||
}
|
||||
type parentB struct {
|
||||
*nested
|
||||
}
|
||||
|
||||
from := parentA{nested: &nested{A: "a"}}
|
||||
to := parentB{}
|
||||
|
||||
err := Copy(&to, &from)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
from.nested.A = "b"
|
||||
|
||||
if to.nested != nil {
|
||||
t.Errorf("should be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should work with unexported fields", func(t *testing.T) {
|
||||
type nested struct {
|
||||
A string
|
||||
}
|
||||
type parentA struct {
|
||||
nested
|
||||
}
|
||||
type parentB struct {
|
||||
nested
|
||||
}
|
||||
|
||||
from := parentA{nested: nested{A: "a"}}
|
||||
to := parentB{}
|
||||
|
||||
err := Copy(&to, &from)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
from.nested.A = "b"
|
||||
|
||||
if to.nested.A == from.nested.A {
|
||||
t.Errorf("should be different")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should work with exported ptr fields", func(t *testing.T) {
|
||||
type Nested struct {
|
||||
A string
|
||||
}
|
||||
type parentA struct {
|
||||
*Nested
|
||||
}
|
||||
type parentB struct {
|
||||
*Nested
|
||||
}
|
||||
|
||||
fieldValue := "a"
|
||||
from := parentA{Nested: &Nested{A: fieldValue}}
|
||||
to := parentB{}
|
||||
|
||||
err := Copy(&to, &from)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
from.Nested.A = "b"
|
||||
|
||||
if to.Nested.A != fieldValue {
|
||||
t.Errorf("should not change")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should work with exported ptr fields with same name src field", func(t *testing.T) {
|
||||
type Nested struct {
|
||||
A string
|
||||
}
|
||||
type parentA struct {
|
||||
A string
|
||||
}
|
||||
type parentB struct {
|
||||
*Nested
|
||||
}
|
||||
|
||||
fieldValue := "a"
|
||||
from := parentA{A: fieldValue}
|
||||
to := parentB{}
|
||||
|
||||
err := Copy(&to, &from)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
from.A = "b"
|
||||
|
||||
if to.Nested.A != fieldValue {
|
||||
t.Errorf("should not change")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should work with exported fields", func(t *testing.T) {
|
||||
type Nested struct {
|
||||
A string
|
||||
}
|
||||
type parentA struct {
|
||||
Nested
|
||||
}
|
||||
type parentB struct {
|
||||
Nested
|
||||
}
|
||||
|
||||
fieldValue := "a"
|
||||
from := parentA{Nested: Nested{A: fieldValue}}
|
||||
to := parentB{}
|
||||
|
||||
err := Copy(&to, &from)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
from.Nested.A = "b"
|
||||
|
||||
if to.Nested.A != fieldValue {
|
||||
t.Errorf("should not change")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
8
go.mod
8
go.mod
@@ -2,10 +2,16 @@ module git.charlienet.top/go/copier
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/stretchr/testify v1.11.1
|
||||
require (
|
||||
github.com/charlienet/go-misc v0.0.0-20250920151122-cb147afeabdf
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
13
go.sum
13
go.sum
@@ -1,7 +1,20 @@
|
||||
github.com/charlienet/go-misc v0.0.0-20250920151122-cb147afeabdf h1:AQS/jJma5vCm03dbHd2h8FcT/fWlhACpiqMWeuz3XgU=
|
||||
github.com/charlienet/go-misc v0.0.0-20250920151122-cb147afeabdf/go.mod h1:+NpQcYzYcl/sLr+d04plyLWd2BS6dRmPoccyKn36Yl8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
||||
37
optiongs.go
37
optiongs.go
@@ -7,14 +7,15 @@ const (
|
||||
)
|
||||
|
||||
type options struct {
|
||||
tagName string // 标签名
|
||||
maxDepth int // 最大复制深度
|
||||
ignoreEmpty bool // 复制时忽略空字段
|
||||
caseSensitive bool // 复制时大小写敏感
|
||||
must bool // 只复制具有must标识的字段
|
||||
converters []TypeConverter // 类型转换器
|
||||
fieldNameMapping map[string]string // 字段名转映射
|
||||
nameConverter func(string) string // 字段名转换器
|
||||
tagName string // 标签名
|
||||
maxDepth int // 最大复制深度
|
||||
ignoreEmpty bool // 复制时忽略空字段
|
||||
caseSensitive bool // 复制时大小写敏感
|
||||
must bool // 只复制具有must标识的字段
|
||||
convertersByName map[string]TypeConverter // 根据名称处理的类型转换器
|
||||
converters []TypeConverter // 根据源和目标类型处理的类型转换器
|
||||
fieldNameMapping map[string]string // 字段名转映射
|
||||
nameConverter func(string) string // 字段名转换器
|
||||
}
|
||||
|
||||
type option func(*options)
|
||||
@@ -32,7 +33,11 @@ type FieldNameConverter struct {
|
||||
}
|
||||
|
||||
func getOpt(opts ...option) *options {
|
||||
opt := DefaultOptions
|
||||
opt := &options{
|
||||
maxDepth: noDepthLimited,
|
||||
tagName: defaultTag,
|
||||
convertersByName: make(map[string]TypeConverter),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(opt)
|
||||
@@ -88,6 +93,15 @@ func WithMust() option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithTypeConvertByName(name string, f func(src any) (dst any, err error)) option {
|
||||
return func(o *options) {
|
||||
o.convertersByName[name] = TypeConverter{
|
||||
FieldName: name,
|
||||
Fn: f,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithConverters(converters ...TypeConverter) option {
|
||||
return func(o *options) {
|
||||
o.converters = converters
|
||||
@@ -111,8 +125,3 @@ func WithTagName(tagName string) option {
|
||||
o.tagName = tagName
|
||||
}
|
||||
}
|
||||
|
||||
var DefaultOptions = &options{
|
||||
maxDepth: noDepthLimited,
|
||||
tagName: defaultTag,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user