add
This commit is contained in:
42
README.md
42
README.md
@@ -1,3 +1,43 @@
|
||||
# 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