add
This commit is contained in:
40
README.md
40
README.md
@@ -1,3 +1,43 @@
|
|||||||
# copier
|
# copier
|
||||||
|
|
||||||
Golang object deep copy library
|
Golang object deep copy library
|
||||||
|
|
||||||
|
|
||||||
|
map->map
|
||||||
|
slice->slice
|
||||||
|
struct->struct
|
||||||
|
map->struct
|
||||||
|
struct->map
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/chenquan/copier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
type User2 struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
user := User{
|
||||||
|
Name: "chenquan",
|
||||||
|
Age: 18,
|
||||||
|
}
|
||||||
|
user2 := User2{}
|
||||||
|
err := copier.Copy(&user2, &user)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
fmt.Println(user2)
|
||||||
|
}
|
||||||
171
copier.go
Normal file
171
copier.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package copier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Copy(src, dst any, opts ...option) error {
|
||||||
|
opt := getOpt(opts...)
|
||||||
|
return copy(src, dst, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copy(src, dst any, opt *options) error {
|
||||||
|
var (
|
||||||
|
srcValue = indirect(reflect.ValueOf(dst))
|
||||||
|
dstValue = indirect(reflect.ValueOf(src))
|
||||||
|
)
|
||||||
|
|
||||||
|
if !dstValue.IsValid() {
|
||||||
|
return ErrInvalidCopyFrom
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dstValue.CanSet() {
|
||||||
|
return ErrInvalidCopyDestination
|
||||||
|
}
|
||||||
|
|
||||||
|
return deepCopy(srcValue, dstValue, 0, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepCopy(dst, src reflect.Value, depth int, opt *options) error {
|
||||||
|
switch dst.Kind() {
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
// slice -> slice
|
||||||
|
return copySlice(src, dst, depth, opt)
|
||||||
|
case reflect.Map:
|
||||||
|
switch src.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
// map -> map
|
||||||
|
return copyMap(src, dst, depth, opt)
|
||||||
|
case reflect.Struct:
|
||||||
|
// struct -> map
|
||||||
|
return copyStruct2Map(src, dst, depth, opt)
|
||||||
|
default:
|
||||||
|
return ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
switch src.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
// map -> struct
|
||||||
|
return copyMap2Struct(src, dst, depth, opt)
|
||||||
|
case reflect.Struct:
|
||||||
|
// struct -> struct
|
||||||
|
return copyStruct(src, dst, depth, opt)
|
||||||
|
default:
|
||||||
|
return ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Pointer:
|
||||||
|
if dst.IsNil() {
|
||||||
|
if !dst.CanSet() {
|
||||||
|
return ErrInvalidCopyDestination
|
||||||
|
}
|
||||||
|
p := reflect.New(dst.Type().Elem())
|
||||||
|
dst.Set(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
src = src.Elem()
|
||||||
|
dst = dst.Elem()
|
||||||
|
return deepCopy(dst, src, depth, opt)
|
||||||
|
default:
|
||||||
|
return copyDefault(src, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyStruct2Map(src, dst reflect.Value, depth int, opt *options) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMap2Struct(src, dst reflect.Value, depth int, opt *options) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMap(dst, src reflect.Value, depth int, opt *options) error {
|
||||||
|
if dst.IsNil() {
|
||||||
|
dst.Set(reflect.MakeMapWithSize(dst.Type(), src.Len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range src.MapKeys() {
|
||||||
|
value := src.MapIndex(key)
|
||||||
|
dst.SetMapIndex(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copySlice(dst, src reflect.Value, depth int, opt *options) error {
|
||||||
|
len := src.Len()
|
||||||
|
if dst.Len() > 0 && dst.Len() < src.Len() {
|
||||||
|
len = dst.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
if dst.Kind() == reflect.Slice && dst.Len() == 0 && src.Len() > 0 {
|
||||||
|
dstType := dst.Type().Elem()
|
||||||
|
newDst := reflect.MakeSlice(reflect.SliceOf(dstType), len, len)
|
||||||
|
dst.Set(newDst)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len; i++ {
|
||||||
|
if err := deepCopy(dst.Index(i), src.Index(i), depth, opt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
dstValue := fieldByName(dst, sf.Name, opt)
|
||||||
|
if !dstValue.IsValid() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deepCopy(dstValue, src.Field(i), 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()) {
|
||||||
|
dst.Set(src.Convert(dst.Type()))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch dst.Kind() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldByName(v reflect.Value, name string, opt *options) reflect.Value {
|
||||||
|
if opt.caseSensitive {
|
||||||
|
return v.FieldByName(name)
|
||||||
|
}
|
||||||
|
return v.FieldByNameFunc(func(s string) bool { return strings.EqualFold(s, name) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func indirect(reflectValue reflect.Value) reflect.Value {
|
||||||
|
for reflectValue.Kind() == reflect.Pointer {
|
||||||
|
reflectValue = reflectValue.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflectValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func indirectType(reflectType reflect.Type) (_ reflect.Type, isPtr bool) {
|
||||||
|
for reflectType.Kind() == reflect.Pointer || reflectType.Kind() == reflect.Slice {
|
||||||
|
reflectType = reflectType.Elem()
|
||||||
|
isPtr = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflectType, isPtr
|
||||||
|
}
|
||||||
28
copier_map_test.go
Normal file
28
copier_map_test.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package copier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCopyMap(t *testing.T) {
|
||||||
|
type person struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
src := map[string]*person{
|
||||||
|
"John": {
|
||||||
|
Name: "John",
|
||||||
|
Age: 30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := map[string]any{}
|
||||||
|
|
||||||
|
assert.NoError(t, Copy(&dst, src))
|
||||||
|
|
||||||
|
fmt.Println(src, dst)
|
||||||
|
}
|
||||||
60
copier_slice_test.go
Normal file
60
copier_slice_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package copier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSliceCopy(t *testing.T) {
|
||||||
|
type person struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
src := []person{
|
||||||
|
{Name: "Alice", Age: 20},
|
||||||
|
{Name: "Bob", Age: 30},
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := []person{}
|
||||||
|
|
||||||
|
Copy(&dst, src)
|
||||||
|
|
||||||
|
// Check if the copy was successful
|
||||||
|
if len(dst) != len(src) {
|
||||||
|
t.Errorf("Expected %d elements, got %d", len(src), len(dst))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("dst:", dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPtrSliceCopy(t *testing.T) {
|
||||||
|
type person struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
src := []*person{
|
||||||
|
{Name: "Alice", Age: 20},
|
||||||
|
{Name: "Bob", Age: 30},
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := []*person{}
|
||||||
|
|
||||||
|
Copy(&dst, src)
|
||||||
|
|
||||||
|
// Check if the copy was successful
|
||||||
|
if len(dst) != len(src) {
|
||||||
|
t.Errorf("Expected %d elements, got %d", len(src), len(dst))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, src[0].Name, dst[0].Name)
|
||||||
|
|
||||||
|
for i := range src {
|
||||||
|
assert.Equal(t, src[i].Name, dst[i].Name)
|
||||||
|
assert.Equal(t, src[i].Age, dst[i].Age)
|
||||||
|
assert.NotSame(t, src[i], dst[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
2
copier_test.go
Normal file
2
copier_test.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
package copier
|
||||||
|
|
||||||
11
errors.go
Normal file
11
errors.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package copier
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidCopyDestination = errors.New("copy destination must be non-nil and addressable")
|
||||||
|
ErrInvalidCopyFrom = errors.New("copy from must be non-nil and addressable")
|
||||||
|
ErrMapKeyNotMatch = errors.New("map's key type doesn't match")
|
||||||
|
ErrNotSupported = errors.New("not supported")
|
||||||
|
ErrFieldNameTagStartNotUpperCase = errors.New("copier field name tag must be start upper case")
|
||||||
|
)
|
||||||
11
go.mod
Normal file
11
go.mod
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module git.charlienet.top/go/copier
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.11.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
10
go.sum
Normal file
10
go.sum
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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/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/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=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
118
optiongs.go
Normal file
118
optiongs.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package copier
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
const (
|
||||||
|
noDepthLimited = -1
|
||||||
|
)
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
tagName string // 标签名
|
||||||
|
maxDepth int // 最大复制深度
|
||||||
|
ignoreEmpty bool // 复制时忽略空字段
|
||||||
|
caseSensitive bool // 复制时大小写敏感
|
||||||
|
must bool // 只复制具有must标识的字段
|
||||||
|
converters []TypeConverter // 类型转换器
|
||||||
|
fieldNameMapping map[string]string // 字段名转映射
|
||||||
|
nameConverter func(string) string // 字段名转换器
|
||||||
|
}
|
||||||
|
|
||||||
|
type option func(*options)
|
||||||
|
|
||||||
|
type TypeConverter struct {
|
||||||
|
FieldName string
|
||||||
|
SrcType any
|
||||||
|
DstType any
|
||||||
|
Fn func(src any) (dst any, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldNameConverter struct {
|
||||||
|
SrcFieldName string
|
||||||
|
DstFieldName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOpt(opts ...option) *options {
|
||||||
|
opt := DefaultOptions
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *options) NameConvert(name string) string {
|
||||||
|
if opt.nameConverter != nil {
|
||||||
|
name = opt.nameConverter(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if toname, ok := opt.fieldNameMapping[name]; ok {
|
||||||
|
name = toname
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt options) TypeConvert(value reflect.Value) (reflect.Value, bool) {
|
||||||
|
for _, c := range opt.converters {
|
||||||
|
_ = c
|
||||||
|
}
|
||||||
|
return value, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *options) ExceedMaxDepth(depth int) bool {
|
||||||
|
return opt.maxDepth != noDepthLimited && depth > opt.maxDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMaxDepth(depth int) option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.maxDepth = depth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithIgnoreEmpty() option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.ignoreEmpty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCaseSensitive() option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.caseSensitive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMust() option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.must = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithConverters(converters ...TypeConverter) option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.converters = converters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNameMapping(mappings map[string]string) option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.fieldNameMapping = mappings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNameFn(fn func(string) string) option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.nameConverter = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTagName(tagName string) option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.tagName = tagName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultOptions = &options{
|
||||||
|
maxDepth: noDepthLimited,
|
||||||
|
tagName: defaultTag,
|
||||||
|
}
|
||||||
63
tags.go
Normal file
63
tags.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package copier
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTag = "copier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tagt uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
tagMust tagt = 1 << iota
|
||||||
|
tagIgnore
|
||||||
|
tagToName
|
||||||
|
)
|
||||||
|
|
||||||
|
// 标签 copier 取值 must、ignore、toname 等
|
||||||
|
// copier:"must,toname=xxx" 必须复制,并重命名为 xxx
|
||||||
|
|
||||||
|
type tagOption struct {
|
||||||
|
flg tagt
|
||||||
|
toname string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTag(tag string) *tagOption {
|
||||||
|
opt := tagOption{}
|
||||||
|
|
||||||
|
flg := tagt(0)
|
||||||
|
for t := range strings.SplitSeq(tag, ",") {
|
||||||
|
tag, value, found := strings.Cut(t, "=")
|
||||||
|
switch tag {
|
||||||
|
case "-", "ignore":
|
||||||
|
flg |= tagIgnore
|
||||||
|
case "must":
|
||||||
|
flg |= tagMust
|
||||||
|
case "toname":
|
||||||
|
flg |= tagToName
|
||||||
|
if found {
|
||||||
|
opt.toname = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opt.flg = flg
|
||||||
|
|
||||||
|
return &opt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *tagOption) Contains(tag tagt) bool {
|
||||||
|
if o.flg == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.flg&tag == tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *tagOption) ToName() string {
|
||||||
|
if o.flg&tagToName == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.toname
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user