diff --git a/.gitignore b/.gitignore index 6d742b3..b280430 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ ttt/ +cmd/ diff --git a/README.md b/README.md index 079dc57..1e376f0 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,56 @@ # Copier - I am a copier, I copy everything from one to another +I am a copier, I copy everything from one to another [![test status](https://github.com/jinzhu/copier/workflows/tests/badge.svg?branch=master "test status")](https://github.com/jinzhu/copier/actions) -## Features +## Key Features -* Copy from field to field with same name -* Copy from method to field with same name -* Copy from field to method with same name -* Copy from slice to slice -* Copy from struct to slice -* Copy from map to map -* Enforce copying a field with a tag -* Ignore a field with a tag -* Deep Copy +- Field-to-field and method-to-field copying based on matching names +- Support for copying data: + - From slice to slice + - From struct to slice + - From map to map +- Field manipulation through tags: + - Enforce field copying with `copier:"must"` + - Override fields even when `IgnoreEmpty` is set with `copier:"override"` + - Exclude fields from being copied with `copier:"-"` -## Usage +## Getting Started + +### Installation + +To start using Copier, install Go and run go get: + +```bash +go get -u github.com/jinzhu/copier +``` + +## Basic + +Import Copier into your application to access its copying capabilities ```go -package main +import "github.com/jinzhu/copier" +``` -import ( - "fmt" - "github.com/jinzhu/copier" -) +### Basic Copying +```go type User struct { - Name string - Role string - Age int32 - EmployeeCode int64 `copier:"EmployeeNum"` // specify field name - - // Explicitly ignored in the destination struct. - Salary int + Name string + Role string + Age int32 } func (user *User) DoubleAge() int32 { return 2 * user.Age } -// Tags in the destination Struct provide instructions to copier.Copy to ignore -// or enforce copying and to panic or return an error if a field was not copied. type Employee struct { - // Tell copier.Copy to panic if this field is not copied. - Name string `copier:"must"` - - // Tell copier.Copy to return an error if this field is not copied. - Age int32 `copier:"must,nopanic"` - - // Tell copier.Copy to explicitly ignore copying this field. - Salary int `copier:"-"` - + Name string + Age int32 DoubleAge int32 - EmployeeId int64 `copier:"EmployeeNum"` // specify field name SuperRole string } @@ -62,59 +59,271 @@ func (employee *Employee) Role(role string) { } func main() { - var ( - user = User{Name: "Jinzhu", Age: 18, Role: "Admin", Salary: 200000} - users = []User{{Name: "Jinzhu", Age: 18, Role: "Admin", Salary: 100000}, {Name: "jinzhu 2", Age: 30, Role: "Dev", Salary: 60000}} - employee = Employee{Salary: 150000} - employees = []Employee{} - ) + user := User{Name: "Jinzhu", Age: 18, Role: "Admin"} + employee := Employee{} copier.Copy(&employee, &user) + fmt.Printf("%#v\n", employee) + // Output: Employee{Name:"Jinzhu", Age:18, DoubleAge:36, SuperRole:"Super Admin"} +} +``` + +## Tag Usage Examples + +### `copier:"-"` - Ignoring Fields + +Fields tagged with `copier:"-"` are explicitly ignored by Copier during the copying process. + +```go +type Source struct { + Name string + Secret string // We do not want this to be copied. +} + +type Target struct { + Name string + Secret string `copier:"-"` +} + +func main() { + source := Source{Name: "John", Secret: "so_secret"} + target := Target{} + + copier.Copy(&target, &source) + fmt.Printf("Name: %s, Secret: '%s'\n", target.Name, target.Secret) + // Output: Name: John, Secret: '' +} +``` + +### `copier:"must"` - Enforcing Field Copy + +The `copier:"must"` tag forces a field to be copied, resulting in a panic or an error if the field cannot be copied. + +```go +type MandatorySource struct { + Identification int +} + +type MandatoryTarget struct { + ID int `copier:"must"` // This field must be copied, or it will panic/error. +} + +func main() { + source := MandatorySource{} + target := MandatoryTarget{ID: 10} + + // This will result in a panic or an error since ID is a must field but is empty in source. + if err := copier.Copy(&target, &source); err != nil { + log.Fatal(err) + } +} +``` + +### `copier:"must,nopanic"` - Enforcing Field Copy Without Panic + +Similar to `copier:"must"`, but Copier returns an error instead of panicking if the field is not copied. + +```go +type SafeSource struct { + ID string +} + +type SafeTarget struct { + Code string `copier:"must,nopanic"` // Enforce copying without panic. +} + +func main() { + source := SafeSource{} + target := SafeTarget{Code: "200"} + + if err := copier.Copy(&target, &source); err != nil { + log.Fatalln("Error:", err) + } + // This will not panic, but will return an error due to missing mandatory field. +} +``` + +### `copier:"override"` - Overriding Fields with IgnoreEmpty + +Fields tagged with `copier:"override"` are copied even if IgnoreEmpty is set to true in Copier options and works for nil values. + +```go +type SourceWithNil struct { + Details *string +} + +type TargetOverride struct { + Details *string `copier:"override"` // Even if source is nil, copy it. +} + +func main() { + details := "Important details" + source := SourceWithNil{Details: nil} + target := TargetOverride{Details: &details} + + copier.CopyWithOption(&target, &source, copier.Option{IgnoreEmpty: true}) + if target.Details == nil { + fmt.Println("Details field was overridden to nil.") + } +} +``` + +### Specifying Custom Field Names + +Use field tags to specify a custom field name when the source and destination field names do not match. + +```go +type SourceEmployee struct { + Identifier int64 +} + +type TargetWorker struct { + ID int64 `copier:"Identifier"` // Map Identifier from SourceEmployee to ID in TargetWorker +} + +func main() { + source := SourceEmployee{Identifier: 1001} + target := TargetWorker{} + + copier.Copy(&target, &source) + fmt.Printf("Worker ID: %d\n", target.ID) + // Output: Worker ID: 1001 +} +``` + +## Other examples + +### Copy from Method to Field with Same Name + +Illustrates copying from a method to a field and vice versa. + +```go +// Assuming User and Employee structs defined earlier with method and field respectively. + +func main() { + user := User{Name: "Jinzhu", Age: 18} + employee := Employee{} + + copier.Copy(&employee, &user) + fmt.Printf("DoubleAge: %d\n", employee.DoubleAge) + // Output: DoubleAge: 36, demonstrating method to field copying. +} +``` + +### Copy Struct to Slice + +```go +func main() { + user := User{Name: "Jinzhu", Age: 18, Role: "Admin"} + var employees []Employee + + copier.Copy(&employees, &user) + fmt.Printf("%#v\n", employees) + // Output: []Employee{{Name: "Jinzhu", Age: 18, DoubleAge: 36, SuperRole: "Super Admin"}} +} +``` - fmt.Printf("%#v \n", employee) - // Employee{ - // Name: "Jinzhu", // Copy from field - // Age: 18, // Copy from field - // Salary:150000, // Copying explicitly ignored - // DoubleAge: 36, // Copy from method - // EmployeeId: 0, // Ignored - // SuperRole: "Super Admin", // Copy to method - // } - - // Copy struct to slice - copier.Copy(&employees, &user) - - fmt.Printf("%#v \n", employees) - // []Employee{ - // {Name: "Jinzhu", Age: 18, Salary:0, DoubleAge: 36, EmployeeId: 0, SuperRole: "Super Admin"} - // } - - // Copy slice to slice - employees = []Employee{} - copier.Copy(&employees, &users) - - fmt.Printf("%#v \n", employees) - // []Employee{ - // {Name: "Jinzhu", Age: 18, Salary:0, DoubleAge: 36, EmployeeId: 0, SuperRole: "Super Admin"}, - // {Name: "jinzhu 2", Age: 30, Salary:0, DoubleAge: 60, EmployeeId: 0, SuperRole: "Super Dev"}, - // } - - // Copy map to map - map1 := map[int]int{3: 6, 4: 8} - map2 := map[int32]int8{} - copier.Copy(&map2, map1) - - fmt.Printf("%#v \n", map2) - // map[int32]int8{3:6, 4:8} +### Copy Slice to Slice + +```go +func main() { + users := []User{{Name: "Jinzhu", Age: 18, Role: "Admin"}, {Name: "jinzhu 2", Age: 30, Role: "Dev"}} + var employees []Employee + + copier.Copy(&employees, &users) + fmt.Printf("%#v\n", employees) + // Output: []Employee{{Name: "Jinzhu", Age: 18, DoubleAge: 36, SuperRole: "Super Admin"}, {Name: "jinzhu 2", Age: 30, DoubleAge: 60, SuperRole: "Super Dev"}} +} +``` + +### Copy Map to Map + +```go +func main() { + map1 := map[int]int{3: 6, 4: 8} + map2 := map[int32]int8{} + + copier.Copy(&map2, map1) + fmt.Printf("%#v\n", map2) + // Output: map[int32]int8{3:6, 4:8} } ``` -### Copy with Option +## Complex Data Copying: Nested Structures with Slices + +This example demonstrates how Copier can be used to copy data involving complex, nested structures, including slices of structs, to showcase its ability to handle intricate data copying scenarios. ```go -copier.CopyWithOption(&to, &from, copier.Option{IgnoreEmpty: true, DeepCopy: true}) +package main + +import ( + "fmt" + "github.com/jinzhu/copier" +) + +type Address struct { + City string + Country string +} + +type Contact struct { + Email string + Phones []string +} + +type Employee struct { + Name string + Age int32 + Addresses []Address + Contact *Contact +} + +type Manager struct { + Name string `copier:"must"` + Age int32 `copier:"must,nopanic"` + ManagedCities []string + Contact *Contact `copier:"override"` + SecondaryEmails []string +} + +func main() { + employee := Employee{ + Name: "John Doe", + Age: 30, + Addresses: []Address{ + {City: "New York", Country: "USA"}, + {City: "San Francisco", Country: "USA"}, + }, + Contact: nil, + } + + manager := Manager{ + ManagedCities: []string{"Los Angeles", "Boston"}, + Contact: &Contact{ + Email: "john.doe@example.com", + Phones: []string{"123-456-7890", "098-765-4321"}, + }, // since override is set this should be overridden with nil + SecondaryEmails: []string{"secondary@example.com"}, + } + + copier.CopyWithOption(&manager, &employee, copier.Option{IgnoreEmpty: true, DeepCopy: true}) + + fmt.Printf("Manager: %#v\n", manager) + // Output: Manager struct showcasing copied fields from Employee, + // including overridden and deeply copied nested slices. +} ``` +## Available tags + +| Tag | Description | +| ------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `copier:"-"` | Explicitly ignores the field during copying. | +| `copier:"must"` | Forces the field to be copied; Copier will panic or return an error if the field is not copied. | +| `copier:"nopanic"` | Copier will return an error instead of panicking. | +| `copier:"override"` | Forces the field to be copied even if `IgnoreEmpty` is set. Useful for overriding existing values with empty ones | +| `FieldName` | Specifies a custom field name for copying when field names do not match between structs. | + ## Contributing You can help to make the project better, check out [http://gorm.io/contribute.html](http://gorm.io/contribute.html) for things you can do. @@ -123,9 +332,9 @@ You can help to make the project better, check out [http://gorm.io/contribute.ht **jinzhu** -* -* -* +- +- +- ## License diff --git a/copier.go b/copier.go index d2f782a..175ad82 100644 --- a/copier.go +++ b/copier.go @@ -22,6 +22,9 @@ const ( // Ignore a destination field from being copied to. tagIgnore + // Denotes the fact that the field should be overridden, no matter if the IgnoreEmpty is set + tagOverride + // Denotes that the value as been copied hasCopied @@ -203,7 +206,7 @@ func copier(toValue interface{}, fromValue interface{}, opt Option) (err error) to.SetMapIndex(toKey, toValue) break } - elemType = reflect.PtrTo(elemType) + elemType = reflect.PointerTo(elemType) toValue = toValue.Addr() } } @@ -335,7 +338,8 @@ func copier(toValue interface{}, fromValue interface{}, opt Option) (err error) fieldNamesMapping := getFieldNamesMapping(mappings, fromType, toType) srcFieldName, destFieldName := getFieldName(name, flgs, fieldNamesMapping) - if fromField := fieldByNameOrZeroValue(source, srcFieldName); fromField.IsValid() && !shouldIgnore(fromField, opt.IgnoreEmpty) { + + if fromField := fieldByNameOrZeroValue(source, srcFieldName); fromField.IsValid() && !shouldIgnore(fromField, fieldFlags, opt.IgnoreEmpty) { // process for nested anonymous field destFieldNotSet := false if f, ok := dest.Type().FieldByName(destFieldName); ok { @@ -410,7 +414,7 @@ func copier(toValue interface{}, fromValue interface{}, opt Option) (err error) fromMethod = source.MethodByName(srcFieldName) } - if fromMethod.IsValid() && fromMethod.Type().NumIn() == 0 && fromMethod.Type().NumOut() == 1 && !shouldIgnore(fromMethod, opt.IgnoreEmpty) { + if fromMethod.IsValid() && fromMethod.Type().NumIn() == 0 && fromMethod.Type().NumOut() == 1 && !shouldIgnore(fromMethod, flgs.BitFlags[name], opt.IgnoreEmpty) { if toField := fieldByName(dest, destFieldName, opt.CaseSensitive); toField.IsValid() && toField.CanSet() { values := fromMethod.Call([]reflect.Value{}) if len(values) >= 1 { @@ -508,8 +512,8 @@ func copyUnexportedStructFields(to, from reflect.Value) { to.Set(tmp) } -func shouldIgnore(v reflect.Value, ignoreEmpty bool) bool { - return ignoreEmpty && v.IsZero() +func shouldIgnore(v reflect.Value, bitFlags uint8, ignoreEmpty bool) bool { + return ignoreEmpty && bitFlags&tagOverride == 0 && v.IsZero() } var deepFieldsLock sync.RWMutex @@ -593,6 +597,9 @@ func set(to, from reflect.Value, deepCopy bool, converters map[converterPair]Typ } // allocate new `to` variable with default value (eg. *string -> new(string)) to.Set(reflect.New(to.Type().Elem())) + } else if from.Kind() != reflect.Ptr && from.IsZero() { + to.Set(reflect.Zero(to.Type())) + return true, nil } // depointer `to` to = to.Elem() @@ -607,6 +614,7 @@ func set(to, from reflect.Value, deepCopy bool, converters map[converterPair]Typ } } if from.Kind() == reflect.Ptr && from.IsNil() { + to.Set(reflect.Zero(to.Type())) return true, nil } if _, ok := to.Addr().Interface().(sql.Scanner); !ok && (toKind == reflect.Struct || toKind == reflect.Map || toKind == reflect.Slice) { @@ -710,6 +718,8 @@ func parseTags(tag string) (flg uint8, name string, err error) { flg = flg | tagMust case "nopanic": flg = flg | tagNoPanic + case "override": + flg = flg | tagOverride default: if unicode.IsUpper([]rune(t)[0]) { name = strings.TrimSpace(t) @@ -734,6 +744,7 @@ func getFlags(dest, src reflect.Value, toType, fromType reflect.Type) (flags, er TagToFieldName: map[string]string{}, }, } + var toTypeFields, fromTypeFields []reflect.StructField if dest.IsValid() { toTypeFields = deepFields(toType) @@ -763,6 +774,7 @@ func getFlags(dest, src reflect.Value, toType, fromType reflect.Type) (flags, er if tags != "" { var name string var err error + if _, name, err = parseTags(tags); err != nil { return flags{}, err } else if name != "" { @@ -771,6 +783,7 @@ func getFlags(dest, src reflect.Value, toType, fromType reflect.Type) (flags, er } } } + return flgs, nil } diff --git a/copier_tags_test.go b/copier_tags_test.go index c8fe704..61ee238 100644 --- a/copier_tags_test.go +++ b/copier_tags_test.go @@ -16,13 +16,13 @@ type EmployeeTags struct { type User1 struct { Name string DOB string - Address string + Address string `copier:"override"` ID int } type User2 struct { DOB string - Address string + Address *string `copier:"override"` ID int } @@ -49,6 +49,52 @@ func TestCopyTagMust(t *testing.T) { copier.Copy(employee, user) } +func TestCopyTagOverrideZeroValue(t *testing.T) { + options := copier.Option{IgnoreEmpty: true} + employee := EmployeeTags{ID: 100, Address: ""} + user := User1{Name: "Dexter Ledesma", DOB: "1 November, 1970", Address: "21 Jump Street", ID: 12345} + + copier.CopyWithOption(&user, employee, options) + if user.Address != "" { + t.Error("Original Address was not overwritten") + } +} + +func TestCopyTagOverridePtrToZeroValue(t *testing.T) { + options := copier.Option{IgnoreEmpty: true} + address := "21 Jump Street" + user1 := User1{ID: 100, Address: ""} + user2 := User2{DOB: "1 November, 1970", Address: &address, ID: 12345} + + copier.CopyWithOption(&user2, user1, options) + if user2.Address != nil { + t.Error("Original Address was not overwritten") + } +} + +func TestCopyTagOverrideZeroValueToPtr(t *testing.T) { + options := copier.Option{IgnoreEmpty: true} + user1 := User2{DOB: "1 November, 1970", Address: nil, ID: 12345} + user2 := User1{ID: 100, Address: "1 November, 1970"} + + copier.CopyWithOption(&user2, user1, options) + if user1.Address != nil { + t.Error("Original Address was not overwritten") + } +} + +func TestCopyTagOverridePtr(t *testing.T) { + options := copier.Option{IgnoreEmpty: true} + address := "21 Jump Street" + user2 := User2{ID: 100, Address: nil} + user := User2{DOB: "1 November, 1970", Address: &address, ID: 12345} + + copier.CopyWithOption(&user, user2, options) + if user.Address != nil { + t.Error("Original Address was not overwritten") + } +} + func TestCopyTagFieldName(t *testing.T) { t.Run("another name field copy", func(t *testing.T) { type SrcTags struct {