diff --git a/copier.go b/copier.go index e4d63d0..b268aa3 100644 --- a/copier.go +++ b/copier.go @@ -1,6 +1,7 @@ package copier import ( + "fmt" "reflect" "strings" "time" @@ -37,6 +38,7 @@ func deepCopy(dst, src reflect.Value, depth int, opt *options) error { return nil } + // fmt.Println("do deep copy src:", src.Kind().String(), "dst:", dst.Kind().String()) switch src.Kind() { case reflect.Slice, reflect.Array: // slice -> slice @@ -47,17 +49,26 @@ func deepCopy(dst, src reflect.Value, depth int, opt *options) error { // map -> map return copyMap(dst, src, depth, opt) case reflect.Struct: - // struct -> map - return copyStruct2Map(dst, src, depth, opt) + // map -> struct + return copyMap2Struct(dst, src, depth, opt) + case reflect.Pointer: + elemType := dst.Type().Elem() + newPtr := reflect.New(elemType) + + if err := deepCopy(newPtr.Elem(), src, depth+1, opt); err != nil { + return err + } + + dst.Set(newPtr) + return nil default: return ErrNotSupported } - case reflect.Struct: switch dst.Kind() { case reflect.Map: - // map -> struct - return copyMap2Struct(dst, src, depth, opt) + // struct -> map + return copyStruct2Map(dst, src, depth, opt) case reflect.Struct: // struct -> struct return copyStruct(dst, src, depth, opt) @@ -90,10 +101,14 @@ func deepCopy(dst, src reflect.Value, depth int, opt *options) error { return deepCopy(dst, src, depth, opt) case reflect.Interface: - if src.Kind() != dst.Kind() { + if src.IsNil() { return nil } + if src.Kind() != dst.Kind() { + return set(dst, src) + } + src = src.Elem() newDst := reflect.New(src.Type().Elem()) @@ -103,16 +118,97 @@ func deepCopy(dst, src reflect.Value, depth int, opt *options) error { dst.Set(newDst) return nil + case reflect.Chan: + return ErrNotSupported default: - return copyDefault(dst, src) + return set(dst, src) } } func copyStruct2Map(dst, src reflect.Value, depth int, opt *options) error { + for i, n := 0, src.NumField(); i < n; i++ { + sf := src.Type().Field(i) + if sf.PkgPath != "" && !sf.Anonymous { + continue + } + + if sf.Anonymous { + switch sf.Type.Kind() { + case reflect.Struct, reflect.Pointer: + if err := deepCopy(dst, src.Field(i), depth+1, opt); err != nil { + return err + } + } + + continue + } + + tag := parseTag(sf.Tag.Get(opt.tagName)) + if tag.Contains(tagIgnore) { + continue + } + + name := toName(sf.Name, tag, opt) + + sField := src.Field(i) + + if opt.ignoreEmpty && sField.IsZero() { + continue + } + + if sField.Kind() == reflect.Pointer || sField.Kind() == reflect.Struct { + var newDst = reflect.ValueOf(make(map[string]any)) + sField = indirect(sField) + + if err := deepCopy(newDst, sField, depth+1, opt); err != nil { + return err + } + + dst.SetMapIndex(reflect.ValueOf(name), newDst) + } else { + dst.SetMapIndex(reflect.ValueOf(name), sField) + } + } + return nil } func copyMap2Struct(dst, src reflect.Value, depth int, opt *options) error { + // 循环结构,然后从map取值 + typ := dst.Type() + for i := 0; i < dst.NumField(); i++ { + field := dst.Field(i) + sf := typ.Field(i) + + if !field.CanSet() { + continue + } + + fieldName := getFieldName(sf) + if fieldName == "-" { + continue + } + + mapValue := src.MapIndex(reflect.ValueOf(fieldName)) + if !mapValue.IsValid() { + continue + } + + if mapValue.Kind() == reflect.Interface { + mapValue = mapValue.Elem() + } + + if mapValue.Kind() == reflect.Map || mapValue.Kind() == reflect.Array || mapValue.Kind() == reflect.Slice { + if err := deepCopy(field, mapValue, depth+1, opt); err != nil { + return err + } + } else { + if err := set(field, mapValue); err != nil { + return err + } + } + } + return nil } @@ -131,7 +227,14 @@ func copyMap(dst, src reflect.Value, depth int, opt *options) error { var copitedValue reflect.Value switch dstType.Elem().Kind() { case reflect.Interface: - copitedValue = reflect.New(value.Type()).Elem() + switch value.Kind() { + case reflect.Interface: + copitedValue = reflect.New(value.Elem().Type()).Elem() + default: + copitedValue = reflect.New(value.Type()).Elem() + } + + set(copitedValue, value) default: copitedValue = reflect.New(dstType.Elem()).Elem() } @@ -189,7 +292,7 @@ func copyStruct(dst, src reflect.Value, depth int, opt *options) error { name := toName(sf.Name, tag, opt) - dstValue := fieldByName(dst, name, opt) // fieldByName(dst, name, opt) + dstValue := fieldByName(dst, name, opt) if !dstValue.IsValid() { continue } @@ -206,18 +309,116 @@ func copyStruct(dst, src reflect.Value, depth int, opt *options) error { return nil } -func copyDefault(dst, src reflect.Value) error { - if src.Type().AssignableTo(dst.Type()) || src.Type().ConvertibleTo(dst.Type()) { +func set(dst, src reflect.Value) error { + if !src.IsValid() { + return ErrInvalidCopyFrom + } + + if src.Kind() == reflect.Interface { + src = src.Elem() + } + + if dst.Kind() == reflect.Pointer { + if dst.IsNil() { + if !dst.CanSet() { + return ErrInvalidCopyDestination + } + dst.Set(reflect.New(dst.Type().Elem())) + } + + dst = dst.Elem() + } + + if src.Type().AssignableTo(dst.Type()) { + dst.Set(src) + return nil + } + + if src.Type().ConvertibleTo(dst.Type()) { dst.Set(src.Convert(dst.Type())) return nil } - switch dst.Kind() { + if _, ok := dst.Interface().(time.Time); ok { + switch v := src.Interface().(type) { + case string: + if t, err := time.Parse(time.RFC3339, v); err == nil { + dst.Set(reflect.ValueOf(t)) + } + case int64: + dst.Set(reflect.ValueOf(time.Unix(v, 0))) + } + return nil + } + + switch dst.Kind() { + case reflect.String: + switch v := src.Interface().(type) { + case string: + dst.SetString(v) + case []byte: + dst.SetString(string(v)) + default: + dst.SetString(fmt.Sprintf("%v", v)) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + switch v := src.Interface().(type) { + case int: + dst.SetInt(int64(v)) + case int8: + dst.SetInt(int64(v)) + case int16: + dst.SetInt(int64(v)) + case int32: + dst.SetInt(int64(v)) + case int64: + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + switch v := src.Interface().(type) { + case uint: + dst.SetUint(uint64(v)) + case uint8: + dst.SetUint(uint64(v)) + case uint16: + dst.SetUint(uint64(v)) + case uint32: + dst.SetUint(uint64(v)) + } + case reflect.Float32, reflect.Float64: + switch v := src.Interface().(type) { + case float32: + dst.SetFloat(float64(v)) + case float64: + dst.SetFloat(v) + } + case reflect.Bool: + switch v := src.Interface().(type) { + case bool: + dst.SetBool(v) + } + default: + return ErrNotSupported } return nil } + +func getFieldName(field reflect.StructField) string { + if tag := field.Tag.Get("json"); tag != "" { + if commaIndex := strings.Index(tag, ","); commaIndex != -1 { + name := tag[:commaIndex] + if name != "" { + return name + } + } else if tag != "" { + return tag + } + } + + return field.Name +} + func toName(name string, tag *tagOption, opt *options) string { if tag != nil && tag.Contains(tagToName) { return tag.toname diff --git a/copier_map_test.go b/copier_map_test.go index 87995af..4bf3d60 100644 --- a/copier_map_test.go +++ b/copier_map_test.go @@ -3,6 +3,7 @@ package copier import ( "fmt" "testing" + "time" "github.com/charlienet/go-misc/json" "github.com/stretchr/testify/assert" @@ -14,14 +15,14 @@ func TestCopyMap(t *testing.T) { Age int } - src := map[string]*person{ - "John": { + src := map[string]any{ + "John": person{ Name: "John", Age: 30, }, } - dst := map[string]*person{} + dst := map[string]person{} assert.NoError(t, Copy(&dst, src)) @@ -30,6 +31,18 @@ func TestCopyMap(t *testing.T) { fmt.Println(json.Struct2JsonIndent(dst)) } +func TestMapAnyCopy(t *testing.T) { + src := map[string]any{ + "department": "Engineering", + "level": 3, + } + + dst := map[string]any{} + assert.NoError(t, Copy(&dst, src)) + + fmt.Println(src, dst) +} + func TestDiffStructMapCopy(t *testing.T) { type person struct { Name string @@ -54,3 +67,168 @@ func TestDiffStructMapCopy(t *testing.T) { fmt.Println(src, dst) } + +func TestStructToMapCopy(t *testing.T) { + type person struct { + Name string + Age int + Address *Address + } + + src := person{ + Name: "John", + Age: 30, + Address: &Address{ + City: "Beijing", + }, + } + + dst := map[string]any{} + assert.NoError(t, Copy(&dst, src, WithIgnoreEmpty())) + fmt.Println(src, dst) + + println(json.Struct2JsonIndent(dst)) +} + +func TestAnonymousStructToMapCopy(t *testing.T) { + type person struct { + Name string + Age int + } + + type person2 struct { + *person + Address *Address + } + + src := person2{ + person: &person{ + Name: "John", + Age: 30, + }, + Address: &Address{ + City: "Beijing", + }, + } + + dst := map[string]any{} + assert.NoError(t, Copy(&dst, src, WithIgnoreEmpty())) + + println(dst, json.Struct2JsonIndent(dst)) + +} + +func TestMapToStructCopy(t *testing.T) { + type person struct { + Name string + Age int + Address *Address + } + + src := map[string]any{ + "Name": "John", + "Age": 30, + "Address": map[string]any{ + "City": "Beijing", + }, + } + + dst := person{} + assert.NoError(t, Copy(&dst, src, WithIgnoreEmpty())) + fmt.Println(dst, json.Struct2JsonIndent(dst)) +} + +func TestMixMapToStruct(t *testing.T) { + type Address struct { + Street string `json:"street"` + City string `json:"city"` + Country string `json:"country"` + } + + type Person struct { + Name string `json:"name"` + Age int `json:"age"` + Height float64 `json:"height"` + IsStudent bool `json:"is_student"` + Birthday time.Time `json:"birthday"` + Tags []string `json:"tags"` + Scores []int `json:"scores"` + Address Address `json:"address"` + Metadata map[string]any `json:"metadata"` + PtrField *string `json:"ptr_field"` + } + + src := map[string]any{ + "name": "Alice", + "age": 25, + "height": 165.5, + "is_student": true, + "birthday": "1998-05-15T06:00:00Z", + "tags": []any{"developer", "golang", "backend"}, + "scores": []any{95, 88, 92}, + "address": map[string]any{ + "street": "123 Main St", + "city": "Beijing", + "country": "China", + }, + "metadata": map[string]any{ + "department": "Engineering", + "level": 3, + "projects": []any{"projectA", "projectB"}, + }, + "ptr_field": "pointer value", + } + + var dst Person + assert.NoError(t, Copy(&dst, src, WithIgnoreEmpty())) + fmt.Println(json.Struct2JsonIndent(dst)) + +} + +func BenchmarkMapCopy(b *testing.B) { + type Address struct { + Street string `json:"street"` + City string `json:"city"` + Country string `json:"country"` + } + + type Person struct { + Name string `json:"name"` + Age int `json:"age"` + Height float64 `json:"height"` + IsStudent bool `json:"is_student"` + Birthday time.Time `json:"birthday"` + Tags []string `json:"tags"` + Scores []int `json:"scores"` + Address Address `json:"address"` + Metadata map[string]any `json:"metadata"` + PtrField *string `json:"ptr_field"` + } + + src := map[string]any{ + "name": "Alice", + "age": 25, + "height": 165.5, + "is_student": true, + "birthday": "1998-05-15T00:00:00Z", + "tags": []any{"developer", "golang", "backend"}, + "scores": []any{95, 88, 92}, + "address": map[string]any{ + "street": "123 Main St", + "city": "Beijing", + "country": "China", + }, + "metadata": map[string]any{ + "department": "Engineering", + "level": 3, + "projects": []any{"projectA", "projectB"}, + }, + "ptr_field": "pointer value", + } + + for b.Loop() { + var dst Person + assert.NoError(b, Copy(&dst, src, WithIgnoreEmpty())) + } + +} diff --git a/copier_slice_test.go b/copier_slice_test.go index fc06307..7cee2b0 100644 --- a/copier_slice_test.go +++ b/copier_slice_test.go @@ -58,3 +58,11 @@ func TestPtrSliceCopy(t *testing.T) { assert.NotSame(t, src[i], dst[i]) } } + +func TestAndSlice(t *testing.T) { + src := []any{"developer", "golang", "backend"} + dst := []string{} + + assert.NoError(t, Copy(&dst, src)) + assert.Equal(t, []string{"developer", "golang", "backend"}, dst) +}