├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── deepcopier.go ├── examples ├── go.mod ├── go.sum ├── rest-usage │ ├── 1 │ │ └── main.go │ ├── 2 │ │ └── main.go │ ├── 3 │ │ └── main.go │ ├── README.rst │ └── schema.sql └── simple │ └── main.go ├── go.mod ├── go.sum └── tests ├── deepcopier_test.go ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.13 4 | script: make test 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ulule 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | cd tests; go test -race 4 | cd tests; go test -cover 5 | cd tests; go test -v 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deepcopier 2 | 3 | [![Build Status](https://secure.travis-ci.org/ulule/deepcopier.svg?branch=master)](http://travis-ci.org/ulule/deepcopier) 4 | 5 | This package is meant to make copying of structs to/from others structs a bit easier. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | go get -u github.com/ulule/deepcopier 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```golang 16 | // Deep copy instance1 into instance2 17 | Copy(instance1).To(instance2) 18 | 19 | // Deep copy instance1 into instance2 and passes the following context (which 20 | // is basically a map[string]interface{}) as first argument 21 | // to methods of instance2 that defined the struct tag "context". 22 | Copy(instance1).WithContext(map[string]interface{}{"foo": "bar"}).To(instance2) 23 | 24 | // Deep copy instance2 into instance1 25 | Copy(instance1).From(instance2) 26 | 27 | // Deep copy instance2 into instance1 and passes the following context (which 28 | // is basically a map[string]interface{}) as first argument 29 | // to methods of instance1 that defined the struct tag "context". 30 | Copy(instance1).WithContext(map[string]interface{}{"foo": "bar"}).From(instance2) 31 | ``` 32 | 33 | Available options for `deepcopier` struct tag: 34 | 35 | | Option | Description | 36 | | --------- | -------------------------------------------------------------------- | 37 | | `field` | Field or method name in source instance | 38 | | `skip` | Ignores the field | 39 | | `context` | Takes a `map[string]interface{}` as first argument (for methods) | 40 | | `force` | Set the value of a `sql.Null*` field (instead of copying the struct) | 41 | 42 | **Options example:** 43 | 44 | ```golang 45 | type Source struct { 46 | Name string 47 | SkipMe string 48 | SQLNullStringToSQLNullString sql.NullString 49 | SQLNullStringToString sql.NullString 50 | 51 | } 52 | 53 | func (Source) MethodThatTakesContext(c map[string]interface{}) string { 54 | return "whatever" 55 | } 56 | 57 | type Destination struct { 58 | FieldWithAnotherNameInSource string `deepcopier:"field:Name"` 59 | SkipMe string `deepcopier:"skip"` 60 | MethodThatTakesContext string `deepcopier:"context"` 61 | SQLNullStringToSQLNullString sql.NullString 62 | SQLNullStringToString string `deepcopier:"force"` 63 | } 64 | 65 | ``` 66 | 67 | Example: 68 | 69 | ```golang 70 | package main 71 | 72 | import ( 73 | "fmt" 74 | 75 | "github.com/ulule/deepcopier" 76 | ) 77 | 78 | // Model 79 | type User struct { 80 | // Basic string field 81 | Name string 82 | // Deepcopier supports https://golang.org/pkg/database/sql/driver/#Valuer 83 | Email sql.NullString 84 | } 85 | 86 | func (u *User) MethodThatTakesContext(ctx map[string]interface{}) string { 87 | // do whatever you want 88 | return "hello from this method" 89 | } 90 | 91 | // Resource 92 | type UserResource struct { 93 | DisplayName string `deepcopier:"field:Name"` 94 | SkipMe string `deepcopier:"skip"` 95 | MethodThatTakesContext string `deepcopier:"context"` 96 | Email string `deepcopier:"force"` 97 | 98 | } 99 | 100 | func main() { 101 | user := &User{ 102 | Name: "gilles", 103 | Email: sql.NullString{ 104 | Valid: true, 105 | String: "gilles@example.com", 106 | }, 107 | } 108 | 109 | resource := &UserResource{} 110 | 111 | deepcopier.Copy(user).To(resource) 112 | 113 | fmt.Println(resource.DisplayName) 114 | fmt.Println(resource.Email) 115 | } 116 | ``` 117 | 118 | Looking for more information about the usage? 119 | 120 | We wrote [an introduction article](https://github.com/ulule/deepcopier/blob/master/examples/rest-usage/README.rst). 121 | Have a look and feel free to give us your feedback. 122 | 123 | ## Contributing 124 | 125 | * Ping us on twitter [@oibafsellig](https://twitter.com/oibafsellig), [@thoas](https://twitter.com/thoas) 126 | * Fork the [project](https://github.com/ulule/deepcopier) 127 | * Help us improving and fixing [issues](https://github.com/ulule/deepcopier/issues) 128 | 129 | Don't hesitate ;) 130 | -------------------------------------------------------------------------------- /deepcopier.go: -------------------------------------------------------------------------------- 1 | package deepcopier 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | // TagName is the deepcopier struct tag name. 12 | TagName = "deepcopier" 13 | // FieldOptionName is the from field option name for struct tag. 14 | FieldOptionName = "field" 15 | // ContextOptionName is the context option name for struct tag. 16 | ContextOptionName = "context" 17 | // SkipOptionName is the skip option name for struct tag. 18 | SkipOptionName = "skip" 19 | // ForceOptionName is the skip option name for struct tag. 20 | ForceOptionName = "force" 21 | ) 22 | 23 | type ( 24 | // TagOptions is a map that contains extracted struct tag options. 25 | TagOptions map[string]string 26 | 27 | // Options are copier options. 28 | Options struct { 29 | // Context given to WithContext() method. 30 | Context map[string]interface{} 31 | // Reversed reverses struct tag checkings. 32 | Reversed bool 33 | } 34 | ) 35 | 36 | // DeepCopier deep copies a struct to/from a struct. 37 | type DeepCopier struct { 38 | dst interface{} 39 | src interface{} 40 | ctx map[string]interface{} 41 | } 42 | 43 | // Copy sets source or destination. 44 | func Copy(src interface{}) *DeepCopier { 45 | return &DeepCopier{src: src} 46 | } 47 | 48 | // WithContext injects the given context into the builder instance. 49 | func (dc *DeepCopier) WithContext(ctx map[string]interface{}) *DeepCopier { 50 | dc.ctx = ctx 51 | return dc 52 | } 53 | 54 | // To sets the destination. 55 | func (dc *DeepCopier) To(dst interface{}) error { 56 | dc.dst = dst 57 | return process(dc.dst, dc.src, Options{Context: dc.ctx}) 58 | } 59 | 60 | // From sets the given the source as destination and destination as source. 61 | func (dc *DeepCopier) From(src interface{}) error { 62 | dc.dst = dc.src 63 | dc.src = src 64 | return process(dc.dst, dc.src, Options{Context: dc.ctx, Reversed: true}) 65 | } 66 | 67 | // process handles copy. 68 | func process(dst interface{}, src interface{}, args ...Options) error { 69 | var ( 70 | options = Options{} 71 | srcValue = reflect.Indirect(reflect.ValueOf(src)) 72 | dstValue = reflect.Indirect(reflect.ValueOf(dst)) 73 | srcFieldNames = getFieldNames(src) 74 | srcMethodNames = getMethodNames(src) 75 | ) 76 | 77 | if len(args) > 0 { 78 | options = args[0] 79 | } 80 | 81 | if !dstValue.CanAddr() { 82 | return fmt.Errorf("destination %+v is unaddressable", dstValue.Interface()) 83 | } 84 | 85 | for _, f := range srcFieldNames { 86 | var ( 87 | srcFieldValue = srcValue.FieldByName(f) 88 | srcFieldType, srcFieldFound = srcValue.Type().FieldByName(f) 89 | srcFieldName = srcFieldType.Name 90 | dstFieldName = srcFieldName 91 | tagOptions TagOptions 92 | ) 93 | 94 | if !srcFieldFound { 95 | continue 96 | } 97 | 98 | if options.Reversed { 99 | tagOptions = getTagOptions(srcFieldType.Tag.Get(TagName)) 100 | if v, ok := tagOptions[FieldOptionName]; ok && v != "" { 101 | dstFieldName = v 102 | } 103 | } else { 104 | if name, opts := getRelatedField(dst, srcFieldName); name != "" { 105 | dstFieldName, tagOptions = name, opts 106 | } 107 | } 108 | 109 | if _, ok := tagOptions[SkipOptionName]; ok { 110 | continue 111 | } 112 | 113 | var ( 114 | dstFieldType, dstFieldFound = dstValue.Type().FieldByName(dstFieldName) 115 | dstFieldValue = dstValue.FieldByName(dstFieldName) 116 | ) 117 | 118 | if !dstFieldFound { 119 | continue 120 | } 121 | 122 | // Force option for empty interfaces and nullable types 123 | _, force := tagOptions[ForceOptionName] 124 | 125 | // Valuer -> ptr 126 | if isNullableType(srcFieldType.Type) && dstFieldValue.Kind() == reflect.Ptr && force { 127 | // We have same nullable type on both sides 128 | if srcFieldValue.Type().AssignableTo(dstFieldType.Type) { 129 | dstFieldValue.Set(srcFieldValue) 130 | continue 131 | } 132 | 133 | v, _ := srcFieldValue.Interface().(driver.Valuer).Value() 134 | if v == nil { 135 | continue 136 | } 137 | 138 | valueType := reflect.TypeOf(v) 139 | 140 | ptr := reflect.New(valueType) 141 | ptr.Elem().Set(reflect.ValueOf(v)) 142 | 143 | if valueType.AssignableTo(dstFieldType.Type.Elem()) { 144 | dstFieldValue.Set(ptr) 145 | } 146 | 147 | continue 148 | } 149 | 150 | // Valuer -> value 151 | if isNullableType(srcFieldType.Type) { 152 | // We have same nullable type on both sides 153 | if srcFieldValue.Type().AssignableTo(dstFieldType.Type) { 154 | dstFieldValue.Set(srcFieldValue) 155 | continue 156 | } 157 | 158 | if force { 159 | v, _ := srcFieldValue.Interface().(driver.Valuer).Value() 160 | if v == nil { 161 | continue 162 | } 163 | 164 | rv := reflect.ValueOf(v) 165 | if rv.Type().AssignableTo(dstFieldType.Type) { 166 | dstFieldValue.Set(rv) 167 | } 168 | } 169 | 170 | continue 171 | } 172 | 173 | if dstFieldValue.Kind() == reflect.Interface { 174 | if force { 175 | dstFieldValue.Set(srcFieldValue) 176 | } 177 | continue 178 | } 179 | 180 | // Ptr -> Value 181 | if srcFieldType.Type.Kind() == reflect.Ptr && !srcFieldValue.IsNil() && dstFieldType.Type.Kind() != reflect.Ptr { 182 | indirect := reflect.Indirect(srcFieldValue) 183 | 184 | if indirect.Type().AssignableTo(dstFieldType.Type) { 185 | dstFieldValue.Set(indirect) 186 | continue 187 | } 188 | } 189 | 190 | // Other types 191 | if srcFieldType.Type.AssignableTo(dstFieldType.Type) { 192 | dstFieldValue.Set(srcFieldValue) 193 | } 194 | } 195 | 196 | for _, m := range srcMethodNames { 197 | name, opts := getRelatedField(dst, m) 198 | if name == "" { 199 | continue 200 | } 201 | 202 | if _, ok := opts[SkipOptionName]; ok { 203 | continue 204 | } 205 | 206 | method := reflect.ValueOf(src).MethodByName(m) 207 | if !method.IsValid() { 208 | return fmt.Errorf("method %s is invalid", m) 209 | } 210 | 211 | var ( 212 | dstFieldType, _ = dstValue.Type().FieldByName(name) 213 | dstFieldValue = dstValue.FieldByName(name) 214 | _, withContext = opts[ContextOptionName] 215 | _, force = opts[ForceOptionName] 216 | ) 217 | 218 | args := []reflect.Value{} 219 | if withContext { 220 | args = []reflect.Value{reflect.ValueOf(options.Context)} 221 | } 222 | 223 | var ( 224 | result = method.Call(args)[0] 225 | resultInterface = result.Interface() 226 | resultValue = reflect.ValueOf(resultInterface) 227 | resultType = resultValue.Type() 228 | ) 229 | 230 | // Value -> Ptr 231 | if dstFieldValue.Kind() == reflect.Ptr && force { 232 | ptr := reflect.New(resultType) 233 | ptr.Elem().Set(resultValue) 234 | 235 | if ptr.Type().AssignableTo(dstFieldType.Type) { 236 | dstFieldValue.Set(ptr) 237 | } 238 | 239 | continue 240 | } 241 | 242 | // Ptr -> value 243 | if resultValue.Kind() == reflect.Ptr && force { 244 | if resultValue.Elem().Type().AssignableTo(dstFieldType.Type) { 245 | dstFieldValue.Set(resultValue.Elem()) 246 | } 247 | 248 | continue 249 | } 250 | 251 | if resultType.AssignableTo(dstFieldType.Type) && result.IsValid() { 252 | dstFieldValue.Set(result) 253 | } 254 | } 255 | 256 | return nil 257 | } 258 | 259 | // getTagOptions parses deepcopier tag field and returns options. 260 | func getTagOptions(value string) TagOptions { 261 | options := TagOptions{} 262 | 263 | for _, opt := range strings.Split(value, ";") { 264 | o := strings.Split(opt, ":") 265 | 266 | // deepcopier:"keyword; without; value;" 267 | if len(o) == 1 { 268 | options[o[0]] = "" 269 | } 270 | 271 | // deepcopier:"key:value; anotherkey:anothervalue" 272 | if len(o) == 2 { 273 | options[strings.TrimSpace(o[0])] = strings.TrimSpace(o[1]) 274 | } 275 | } 276 | 277 | return options 278 | } 279 | 280 | // getRelatedField returns first matching field. 281 | func getRelatedField(instance interface{}, name string) (string, TagOptions) { 282 | var ( 283 | value = reflect.Indirect(reflect.ValueOf(instance)) 284 | fieldName string 285 | tagOptions TagOptions 286 | ) 287 | 288 | for i := 0; i < value.NumField(); i++ { 289 | var ( 290 | vField = value.Field(i) 291 | tField = value.Type().Field(i) 292 | tagOptions = getTagOptions(tField.Tag.Get(TagName)) 293 | ) 294 | 295 | if tField.Type.Kind() == reflect.Struct && tField.Anonymous { 296 | if n, o := getRelatedField(vField.Interface(), name); n != "" { 297 | return n, o 298 | } 299 | } 300 | 301 | if v, ok := tagOptions[FieldOptionName]; ok && v == name { 302 | return tField.Name, tagOptions 303 | } 304 | 305 | if tField.Name == name { 306 | return tField.Name, tagOptions 307 | } 308 | } 309 | 310 | return fieldName, tagOptions 311 | } 312 | 313 | // getMethodNames returns instance's method names. 314 | func getMethodNames(instance interface{}) []string { 315 | var methods []string 316 | 317 | t := reflect.TypeOf(instance) 318 | for i := 0; i < t.NumMethod(); i++ { 319 | methods = append(methods, t.Method(i).Name) 320 | } 321 | 322 | return methods 323 | } 324 | 325 | // getFieldNames returns instance's field names. 326 | func getFieldNames(instance interface{}) []string { 327 | var ( 328 | fields []string 329 | v = reflect.Indirect(reflect.ValueOf(instance)) 330 | t = v.Type() 331 | ) 332 | 333 | if t.Kind() != reflect.Struct { 334 | return nil 335 | } 336 | 337 | for i := 0; i < v.NumField(); i++ { 338 | var ( 339 | vField = v.Field(i) 340 | tField = v.Type().Field(i) 341 | ) 342 | 343 | // Is exportable? 344 | if tField.PkgPath != "" { 345 | continue 346 | } 347 | 348 | if tField.Type.Kind() == reflect.Struct && tField.Anonymous { 349 | fields = append(fields, getFieldNames(vField.Interface())...) 350 | continue 351 | } 352 | 353 | fields = append(fields, tField.Name) 354 | } 355 | 356 | return fields 357 | } 358 | 359 | // isNullableType returns true if the given type is a nullable one. 360 | func isNullableType(t reflect.Type) bool { 361 | return t.ConvertibleTo(reflect.TypeOf((*driver.Valuer)(nil)).Elem()) 362 | } 363 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ulule/deepcopier/examples 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/ant0ine/go-json-rest v3.3.2+incompatible 7 | github.com/jinzhu/gorm v1.9.12 8 | github.com/lib/pq v1.4.0 9 | github.com/ulule/deepcopier v0.0.0 10 | ) 11 | 12 | replace github.com/ulule/deepcopier => ../ 13 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/ant0ine/go-json-rest v3.3.2+incompatible h1:nBixrkLFiDNAW0hauKDLc8yJI6XfrQumWvytE1Hk14E= 2 | github.com/ant0ine/go-json-rest v3.3.2+incompatible/go.mod h1:q6aCt0GfU6LhpBsnZ/2U+mwe+0XB5WStbmwyoPfc+sk= 3 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 4 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 5 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 6 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 7 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 8 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 9 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 10 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 11 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 12 | github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= 13 | github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= 14 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 15 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 16 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 17 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 18 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 19 | github.com/lib/pq v1.4.0 h1:TmtCFbH+Aw0AixwyttznSMQDgbR5Yed/Gg6S8Funrhc= 20 | github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 21 | github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw= 22 | github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 24 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 25 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= 26 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 27 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 28 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 32 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 33 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 34 | -------------------------------------------------------------------------------- /examples/rest-usage/1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ant0ine/go-json-rest/rest" 6 | "github.com/jinzhu/gorm" 7 | _ "github.com/lib/pq" 8 | "log" 9 | "net/http" 10 | "os" 11 | "time" 12 | ) 13 | 14 | type Account struct { 15 | ID uint `gorm:"primary_key"` 16 | FirstName string 17 | LastName string 18 | Username string 19 | Password string 20 | Email string 21 | DateJoined time.Time 22 | } 23 | 24 | type Accounts struct { 25 | Db *gorm.DB 26 | } 27 | 28 | func (a *Accounts) Detail(w rest.ResponseWriter, r *rest.Request) { 29 | account := &Account{} 30 | result := a.Db.First(&account, "username = ?", r.PathParam("username")) 31 | 32 | if result.RecordNotFound() { 33 | rest.NotFound(w, r) 34 | return 35 | } 36 | 37 | w.WriteJson(&account) 38 | } 39 | 40 | func main() { 41 | dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable", 42 | os.Getenv("DATABASE_USER"), 43 | os.Getenv("DATABASE_NAME")) 44 | 45 | db, err := gorm.Open("postgres", dsn) 46 | 47 | fmt.Println(dsn) 48 | 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | db.DB() 54 | db.DB().Ping() 55 | db.DB().SetMaxIdleConns(10) 56 | db.DB().SetMaxOpenConns(100) 57 | db.SingularTable(true) 58 | db.LogMode(true) 59 | 60 | api := rest.NewApi() 61 | 62 | api.Use(rest.DefaultDevStack...) 63 | 64 | accounts := &Accounts{Db: db} 65 | 66 | router, err := rest.MakeRouter( 67 | rest.Get("/users/:username", accounts.Detail), 68 | ) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | api.SetApp(router) 73 | log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) 74 | } 75 | -------------------------------------------------------------------------------- /examples/rest-usage/2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ant0ine/go-json-rest/rest" 6 | "github.com/jinzhu/gorm" 7 | _ "github.com/lib/pq" 8 | "github.com/ulule/deepcopier" 9 | "log" 10 | "net/http" 11 | "os" 12 | "time" 13 | ) 14 | 15 | type Account struct { 16 | ID uint `gorm:"primary_key"` 17 | FirstName string 18 | LastName string 19 | Username string 20 | Password string 21 | Email string 22 | DateJoined time.Time 23 | } 24 | 25 | type AccountResource struct { 26 | ID uint `json:"id"` 27 | Username string `json:"username"` 28 | FirstName string `json:"first_name"` 29 | LastName string `json:"last_name"` 30 | Name string `json:"name"` 31 | Email string `json:"email"` 32 | DateJoined time.Time `json:"date_joined"` 33 | } 34 | 35 | func (a Account) Name() string { 36 | return fmt.Sprintf("%s %s", a.FirstName, a.LastName) 37 | } 38 | 39 | type Accounts struct { 40 | Db *gorm.DB 41 | } 42 | 43 | func (a *Accounts) Detail(w rest.ResponseWriter, r *rest.Request) { 44 | account := &Account{} 45 | result := a.Db.First(&account, "username = ?", r.PathParam("username")) 46 | 47 | if result.RecordNotFound() { 48 | rest.NotFound(w, r) 49 | return 50 | } 51 | 52 | resource := &AccountResource{} 53 | 54 | deepcopier.Copy(account).To(resource) 55 | 56 | w.WriteJson(&resource) 57 | } 58 | 59 | func main() { 60 | dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable", 61 | os.Getenv("DATABASE_USER"), 62 | os.Getenv("DATABASE_NAME")) 63 | 64 | db, err := gorm.Open("postgres", dsn) 65 | 66 | fmt.Println(dsn) 67 | 68 | if err != nil { 69 | panic(err) 70 | } 71 | 72 | db.DB() 73 | db.DB().Ping() 74 | db.DB().SetMaxIdleConns(10) 75 | db.DB().SetMaxOpenConns(100) 76 | db.SingularTable(true) 77 | db.LogMode(true) 78 | 79 | api := rest.NewApi() 80 | 81 | api.Use(rest.DefaultDevStack...) 82 | 83 | accounts := &Accounts{Db: db} 84 | 85 | router, err := rest.MakeRouter( 86 | rest.Get("/users/:username", accounts.Detail), 87 | ) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | api.SetApp(router) 92 | log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) 93 | } 94 | -------------------------------------------------------------------------------- /examples/rest-usage/3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ant0ine/go-json-rest/rest" 6 | "github.com/jinzhu/gorm" 7 | _ "github.com/lib/pq" 8 | "github.com/ulule/deepcopier" 9 | "log" 10 | "net/http" 11 | "os" 12 | "time" 13 | ) 14 | 15 | type Account struct { 16 | ID uint `gorm:"primary_key"` 17 | FirstName string 18 | LastName string 19 | Username string 20 | Password string 21 | Email string 22 | DateJoined time.Time 23 | } 24 | 25 | type AccountResource struct { 26 | ID uint `json:"id"` 27 | Username string `json:"username"` 28 | FirstName string `json:"first_name"` 29 | LastName string `json:"last_name"` 30 | Name string `json:"name"` 31 | Email string `json:"email"` 32 | DateJoined time.Time `json:"date_joined"` 33 | ApiUrl string `deepcopier:"context" json:"api_url"` 34 | } 35 | 36 | func (a Account) Name() string { 37 | return fmt.Sprintf("%s %s", a.FirstName, a.LastName) 38 | } 39 | 40 | func (a Account) ApiUrl(context map[string]interface{}) string { 41 | return fmt.Sprintf("%s/users/%s", context["base_url"], a.Username) 42 | } 43 | 44 | type Accounts struct { 45 | Db *gorm.DB 46 | } 47 | 48 | func (a *Accounts) Detail(w rest.ResponseWriter, r *rest.Request) { 49 | account := &Account{} 50 | result := a.Db.First(&account, "username = ?", r.PathParam("username")) 51 | 52 | if result.RecordNotFound() { 53 | rest.NotFound(w, r) 54 | return 55 | } 56 | 57 | resource := &AccountResource{} 58 | 59 | context := map[string]interface{}{"base_url": r.BaseUrl()} 60 | 61 | deepcopier.Copy(account).WithContext(context).To(resource) 62 | 63 | w.WriteJson(&resource) 64 | } 65 | 66 | func main() { 67 | dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable", 68 | os.Getenv("DATABASE_USER"), 69 | os.Getenv("DATABASE_NAME")) 70 | 71 | db, err := gorm.Open("postgres", dsn) 72 | 73 | fmt.Println(dsn) 74 | 75 | if err != nil { 76 | panic(err) 77 | } 78 | 79 | db.DB() 80 | db.DB().Ping() 81 | db.DB().SetMaxIdleConns(10) 82 | db.DB().SetMaxOpenConns(100) 83 | db.SingularTable(true) 84 | db.LogMode(true) 85 | 86 | api := rest.NewApi() 87 | 88 | api.Use(rest.DefaultDevStack...) 89 | 90 | accounts := &Accounts{Db: db} 91 | 92 | router, err := rest.MakeRouter( 93 | rest.Get("/users/:username", accounts.Detail), 94 | ) 95 | if err != nil { 96 | log.Fatal(err) 97 | } 98 | api.SetApp(router) 99 | log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) 100 | } 101 | -------------------------------------------------------------------------------- /examples/rest-usage/README.rst: -------------------------------------------------------------------------------- 1 | Introducing deepcopier, a Go library to make copying of structs a bit easier 2 | ============================================================================ 3 | 4 | Context 5 | ------- 6 | 7 | We are currently refactoring our API_ at Ulule from our monolithic Python 8 | stack with `django-tastypie`_ to a separate Go microservice_. 9 | 10 | When working with models in Go, you don't want to expose all columns and 11 | also implement more methods without writing a lot of code, because everyone 12 | knows programmers are lazy ;) 13 | 14 | deepcopier_ will help you in your daily job when you want to copy a struct into 15 | another one (think resource) or from another one (think payload). 16 | 17 | Installation 18 | ------------ 19 | 20 | Assuming you are already a Go developer, you have your environment up and ready, 21 | so run this command in your shell: 22 | 23 | :: 24 | 25 | $ go get github.com/ulule/deepcopier 26 | 27 | You are now ready to use this library. 28 | 29 | Usage 30 | ----- 31 | 32 | To demonstrate why you should use this library, we will build a dead simple REST 33 | API in READ only. 34 | 35 | We will use postgresql_ as database so I'm also assuming you 36 | already have postgresql_ installed on your laptop :) 37 | 38 | Let's create the database! 39 | 40 | :: 41 | 42 | $ psql postgres 43 | psql (9.4.1) 44 | Type "help" for help. 45 | 46 | postgres=# create user dummy with password ''; 47 | CREATE ROLE 48 | postgres=# create database dummy with owner dummy; 49 | CREATE DATABASE 50 | postgres=# \d 51 | No relations found. 52 | 53 | We now have a perfectly capable database with no tables, let's jump to the 54 | SQL schema. 55 | 56 | .. code-block:: sql 57 | 58 | CREATE TABLE account ( 59 | id serial PRIMARY KEY, 60 | first_name VARCHAR(50), 61 | last_name VARCHAR(50), 62 | username VARCHAR (50) UNIQUE NOT NULL, 63 | password VARCHAR (50) NOT NULL, 64 | email VARCHAR (355) UNIQUE NOT NULL, 65 | date_joined TIMESTAMP NOT NULL 66 | ); 67 | 68 | Transfer this schema to postgresql_. 69 | 70 | :: 71 | 72 | $ psql -U dummy 73 | psql (9.4.1) 74 | Type "help" for help. 75 | dummy=# CREATE TABLE account( 76 | dummy(# id serial PRIMARY KEY, 77 | dummy(# first_name VARCHAR (50), 78 | dummy(# last_name VARCHAR (50), 79 | dummy(# username VARCHAR (50) UNIQUE NOT NULL, 80 | dummy(# password VARCHAR (50) NOT NULL, 81 | dummy(# email VARCHAR (355) UNIQUE NOT NULL, 82 | dummy(# date_joined TIMESTAMP NOT NULL 83 | dummy(# ); 84 | CREATE TABLE 85 | dummy=# \d 86 | List of relations 87 | Schema | Name | Type | Owner 88 | --------+----------------+----------+------- 89 | public | account | table | thoas 90 | public | account_id_seq | sequence | thoas 91 | (2 rows) 92 | 93 | dummy=# 94 | 95 | First insertions incoming! 96 | 97 | :: 98 | 99 | dummy=# INSERT INTO account (username, first_name, last_name, password, email, date_joined) VALUES ('thoas', 'Florent', 'Messa', '8d56e93bcc8d63a171b5630282264341', 'foo@bar.com', '2015-07-31 15:10:10'); 100 | 101 | At this point, we have a schema in a great database, we need to setup our 102 | REST API. 103 | 104 | We will use: 105 | 106 | * `go-json-rest`_ to handle requests 107 | * gorm_ to manipulate the database as an ORM 108 | 109 | In your shell, run this to install them 110 | 111 | :: 112 | 113 | $ go get -u github.com/jinzhu/gorm 114 | $ go get github.com/ant0ine/go-json-rest/rest 115 | 116 | We will define a first attempt of our API to retrieve user information based 117 | on its username. 118 | 119 | We will rewrite our API three times so you need to focus. 120 | 121 | .. code-block:: go 122 | 123 | // main.go 124 | package main 125 | 126 | import ( 127 | "fmt" 128 | "github.com/ant0ine/go-json-rest/rest" 129 | "github.com/jinzhu/gorm" 130 | _ "github.com/lib/pq" 131 | "log" 132 | "net/http" 133 | "os" 134 | "time" 135 | ) 136 | 137 | type Account struct { 138 | ID uint `gorm:"primary_key"` 139 | FirstName string 140 | LastName string 141 | Username string 142 | Password string 143 | Email string 144 | DateJoined time.Time 145 | } 146 | 147 | type Accounts struct { 148 | Db gorm.DB 149 | } 150 | 151 | func (a *Accounts) Detail(w rest.ResponseWriter, r *rest.Request) { 152 | account := &Account{} 153 | result := a.Db.First(&account, "username = ?", r.PathParam("username")) 154 | 155 | if result.RecordNotFound() { 156 | rest.NotFound(w, r) 157 | return 158 | } 159 | 160 | w.WriteJson(&account) 161 | } 162 | 163 | func main() { 164 | dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable", 165 | os.Getenv("DATABASE_USER"), 166 | os.Getenv("DATABASE_NAME")) 167 | 168 | db, err := gorm.Open("postgres", dsn) 169 | 170 | fmt.Println(dsn) 171 | 172 | if err != nil { 173 | panic(err) 174 | } 175 | 176 | db.DB() 177 | db.DB().Ping() 178 | db.DB().SetMaxIdleConns(10) 179 | db.DB().SetMaxOpenConns(100) 180 | db.SingularTable(true) 181 | db.LogMode(true) 182 | 183 | api := rest.NewApi() 184 | 185 | api.Use(rest.DefaultDevStack...) 186 | 187 | accounts := &Accounts{Db: db} 188 | 189 | router, err := rest.MakeRouter( 190 | rest.Get("/users/:username", accounts.Detail), 191 | ) 192 | if err != nil { 193 | log.Fatal(err) 194 | } 195 | api.SetApp(router) 196 | log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) 197 | } 198 | 199 | Let's start the server then 200 | 201 | :: 202 | 203 | $ DATABASE_USER=dummy DATABASE_NAME=dummy go run main.go 204 | 205 | and retrieve the response. 206 | 207 | :: 208 | 209 | $ curl http://localhost:8080/users/thoas 210 | { 211 | "ID": 1, 212 | "Username": "thoas", 213 | "FirstName": "Florent", 214 | "LastName": "Messa", 215 | "Password": "8d56e93bcc8d63a171b5630282264341", 216 | "Email": "foo@bar.com", 217 | "DateJoined": "2015-07-31T15:10:10Z" 218 | } 219 | 220 | Wait a minute? You are exposing the user's password... this not 221 | what we are excepting... We want this specific format 222 | 223 | .. code-block:: json 224 | 225 | { 226 | "id": 1, 227 | "username": "thoas", 228 | "first_name": "Florent", 229 | "last_name": "Messa", 230 | "name": "Florent Messa", 231 | "email": "foo@bar.com", 232 | "date_joined": "2015-07-31T15:10:10Z", 233 | "api_url": "http://localhost:8080/users/thoas" 234 | } 235 | 236 | Implement a separate struct named ``AccountResource`` 237 | 238 | .. code-block:: go 239 | 240 | type AccountResource struct { 241 | ID uint `json:"id"` 242 | Username string `json:"username"` 243 | FirstName string `json:"first_name"` 244 | LastName string `json:"last_name"` 245 | Name string `json:"name"` 246 | Email string `json:"email"` 247 | DateJoined time.Time `json:"date_joined"` 248 | } 249 | 250 | func (a Account) Name() string { 251 | return fmt.Sprintf("%s %s", a.FirstName, a.LastName) 252 | } 253 | 254 | and rewrite ``Accounts.Detail`` to use deepcopier_ 255 | 256 | .. code-block:: go 257 | 258 | func (a *Accounts) Detail(w rest.ResponseWriter, r *rest.Request) { 259 | account := &Account{} 260 | result := a.Db.First(&account, "username = ?", r.PathParam("username")) 261 | 262 | if result.RecordNotFound() { 263 | rest.NotFound(w, r) 264 | return 265 | } 266 | 267 | resource := &AccountResource{} 268 | 269 | deepcopier.Copy(account).To(resource) 270 | 271 | w.WriteJson(&resource) 272 | } 273 | 274 | We are good now, we can inspect our result 275 | 276 | :: 277 | 278 | $ curl http://localhost:8080/users/thoas 279 | { 280 | "id": 1, 281 | "username": "thoas", 282 | "first_name": "Florent", 283 | "last_name": "Messa", 284 | "name": "Florent Messa", 285 | "email": "foo@bar.com", 286 | "date_joined": "2015-07-31T15:10:10Z" 287 | } 288 | 289 | Easy, right? 290 | 291 | We will now rewrite for the last time ``Accounts.Detail`` to provide 292 | some context to retrieve the base url in ``api_url`` attribute. 293 | 294 | .. code-block:: go 295 | 296 | func (a *Accounts) Detail(w rest.ResponseWriter, r *rest.Request) { 297 | account := &Account{} 298 | result := a.Db.First(&account, "username = ?", r.PathParam("username")) 299 | 300 | if result.RecordNotFound() { 301 | rest.NotFound(w, r) 302 | return 303 | } 304 | 305 | resource := &AccountResource{} 306 | 307 | context := map[string]interface{}{"base_url": r.BaseUrl()} 308 | 309 | deepcopier.Copy(account).WithContext(context).To(resource) 310 | 311 | w.WriteJson(&resource) 312 | } 313 | 314 | We need to update ``AccountResource`` to implement the ``ApiUrl`` new method 315 | 316 | .. code-block:: go 317 | 318 | type AccountResource struct { 319 | ID uint `json:"id"` 320 | Username string `json:"username"` 321 | FirstName string `json:"first_name"` 322 | LastName string `json:"last_name"` 323 | Name string `json:"name"` 324 | Email string `json:"email"` 325 | DateJoined time.Time `json:"date_joined"` 326 | ApiUrl string `deepcopier:"context" json:"api_url"` 327 | } 328 | 329 | func (a Account) Name() string { 330 | return fmt.Sprintf("%s %s", a.FirstName, a.LastName) 331 | } 332 | 333 | func (a Account) ApiUrl(context map[string]interface{}) string { 334 | return fmt.Sprintf("%s/users/%s", context["base_url"], a.Username) 335 | } 336 | 337 | We have now the final result of what we excepted for the first time :) 338 | 339 | :: 340 | 341 | $ curl http://localhost:8080/users/thoas 342 | { 343 | "id": 1, 344 | "username": "thoas", 345 | "first_name": "Florent", 346 | "last_name": "Messa", 347 | "name": "Florent Messa", 348 | "email": "foo@bar.com", 349 | "date_joined": "2015-07-31T15:10:10Z", 350 | "api_url": "http://localhost:8080/users/thoas" 351 | } 352 | 353 | If you have reached to the bottom you belong to the brave! 354 | 355 | It has been a long introduction, hope your enjoy it! 356 | 357 | Contributing to deepcopier 358 | -------------------------- 359 | 360 | * Ping us on twitter `@oibafsellig `_, `@thoas `_ 361 | * Fork the `project `_ 362 | * Fix `bugs `_ 363 | 364 | Don't hesitate ;) 365 | 366 | 367 | .. _API: http://developers.ulule.com/ 368 | .. _django-tastypie: https://github.com/django-tastypie/django-tastypie 369 | .. _microservice: http://martinfowler.com/articles/microservices.html 370 | .. _React.js: http://facebook.github.io/react/ 371 | .. _postgresql: http://www.postgresql.org/ 372 | .. _go-json-rest: https://github.com/ant0ine/go-json-rest 373 | .. _gorm: https://github.com/jinzhu/gorm 374 | .. _deepcopier: https://github.com/ulule/deepcopier 375 | -------------------------------------------------------------------------------- /examples/rest-usage/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE account ( 2 | id serial PRIMARY KEY, 3 | first_name VARCHAR(50), 4 | last_name VARCHAR(50), 5 | username VARCHAR (50) UNIQUE NOT NULL, 6 | password VARCHAR (50) NOT NULL, 7 | email VARCHAR (355) UNIQUE NOT NULL, 8 | date_joined TIMESTAMP NOT NULL 9 | ); 10 | 11 | insert into account (username, first_name, last_name, password, email, date_joined) values ('thoas', 'Florent', 'Messa', '8d56e93bcc8d63a171b5630282264341', 'foo@bar.com', '2015-07-31 15:10:10'); 12 | insert into account (username, first_name, last_name, password, email, date_joined) values ('gilles', 'Gilles', 'Fabio', '8d56e93bcc8d63a171b5630282264341', 'bar@foo.com', '2015-07-31 16:10:10'); 13 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ulule/deepcopier" 6 | ) 7 | 8 | // Model 9 | type User struct { 10 | Name string 11 | } 12 | 13 | func (u *User) MethodThatTakesContext(ctx map[string]interface{}) string { 14 | // do whatever you want 15 | return "" 16 | } 17 | 18 | // Resource 19 | type UserResource struct { 20 | DisplayName string `deepcopier:"field:Name"` 21 | SkipMe string `deepcopier:"skip"` 22 | MethodThatTakesContext string `deepcopier:"context"` 23 | } 24 | 25 | func main() { 26 | user := &User{ 27 | Name: "gilles", 28 | } 29 | 30 | resource := &UserResource{} 31 | 32 | deepcopier.Copy(user).To(resource) 33 | 34 | fmt.Println(resource.DisplayName) 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ulule/deepcopier 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulule/deepcopier/45decc6639b634774af79cefff24e6b9670f5847/go.sum -------------------------------------------------------------------------------- /tests/deepcopier_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | "time" 7 | 8 | "github.com/guregu/null" 9 | "github.com/lib/pq" 10 | uuid "github.com/satori/go.uuid" 11 | assert "github.com/stretchr/testify/require" 12 | "github.com/ulule/deepcopier" 13 | ) 14 | 15 | func TestField(t *testing.T) { 16 | type ( 17 | Rel struct { 18 | Int int 19 | } 20 | 21 | Src struct { 22 | Int int 23 | IntPtr *int 24 | Slice []string 25 | SlicePtr *[]string 26 | Map map[string]interface{} 27 | MapPtr *map[string]interface{} 28 | Struct Rel 29 | StructPtr *Rel 30 | Skipped string `deepcopier:"skip"` 31 | } 32 | 33 | Dst struct { 34 | Int int 35 | IntPtr *int 36 | Slice []string 37 | SlicePtr *[]string 38 | Map map[string]interface{} 39 | MapPtr *map[string]interface{} 40 | Struct Rel 41 | StructPtr *Rel 42 | Skipped string `deepcopier:"skip"` 43 | } 44 | 45 | Renamed struct { 46 | MyInt int `deepcopier:"field:Int"` 47 | MyIntPtr *int `deepcopier:"field:IntPtr"` 48 | MySlice []string `deepcopier:"field:Slice"` 49 | MySlicePtr *[]string `deepcopier:"field:SlicePtr"` 50 | MyMap map[string]interface{} `deepcopier:"field:Map"` 51 | MyMapPtr *map[string]interface{} `deepcopier:"field:MapPtr"` 52 | MyStruct Rel `deepcopier:"field:Struct"` 53 | MyStructPtr *Rel `deepcopier:"field:StructPtr"` 54 | Skipped string `deepcopier:"skip"` 55 | } 56 | ) 57 | 58 | var ( 59 | integer = 1 60 | rel = Rel{Int: 1} 61 | slc = []string{"one", "two"} 62 | mp = map[string]interface{}{"one": 1} 63 | ) 64 | 65 | src := &Src{ 66 | Int: integer, 67 | IntPtr: &integer, 68 | Slice: slc, 69 | SlicePtr: &slc, 70 | Map: mp, 71 | MapPtr: &mp, 72 | Struct: rel, 73 | StructPtr: &rel, 74 | Skipped: "I should be skipped", 75 | } 76 | 77 | srcRenamed := &Renamed{ 78 | MyInt: integer, 79 | MyIntPtr: &integer, 80 | MySlice: slc, 81 | MySlicePtr: &slc, 82 | MyMap: mp, 83 | MyMapPtr: &mp, 84 | MyStruct: rel, 85 | MyStructPtr: &rel, 86 | Skipped: "I should be skipped", 87 | } 88 | 89 | // 90 | // To() 91 | // 92 | 93 | dst := &Dst{} 94 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 95 | assert.Equal(t, src.Int, dst.Int) 96 | assert.Equal(t, src.IntPtr, dst.IntPtr) 97 | assert.Equal(t, src.Slice, dst.Slice) 98 | assert.Equal(t, src.SlicePtr, dst.SlicePtr) 99 | assert.Equal(t, src.Map, dst.Map) 100 | assert.Equal(t, src.MapPtr, dst.MapPtr) 101 | assert.Equal(t, src.Struct, dst.Struct) 102 | assert.Equal(t, src.StructPtr, dst.StructPtr) 103 | assert.Zero(t, dst.Skipped) 104 | 105 | dstRenamed := &Renamed{} 106 | assert.Nil(t, deepcopier.Copy(src).To(dstRenamed)) 107 | assert.Equal(t, src.Int, dstRenamed.MyInt) 108 | assert.Equal(t, src.IntPtr, dstRenamed.MyIntPtr) 109 | assert.Equal(t, src.Slice, dstRenamed.MySlice) 110 | assert.Equal(t, src.SlicePtr, dstRenamed.MySlicePtr) 111 | assert.Equal(t, src.Map, dstRenamed.MyMap) 112 | assert.Equal(t, src.MapPtr, dstRenamed.MyMapPtr) 113 | assert.Equal(t, src.Struct, dstRenamed.MyStruct) 114 | assert.Equal(t, src.StructPtr, dstRenamed.MyStructPtr) 115 | assert.Zero(t, dstRenamed.Skipped) 116 | 117 | // 118 | // From() 119 | // 120 | 121 | dst = &Dst{} 122 | assert.Nil(t, deepcopier.Copy(dst).From(src)) 123 | assert.Equal(t, src.Int, dst.Int) 124 | assert.Equal(t, src.IntPtr, dst.IntPtr) 125 | assert.Equal(t, src.Slice, dst.Slice) 126 | assert.Equal(t, src.SlicePtr, dst.SlicePtr) 127 | assert.Equal(t, src.Map, dst.Map) 128 | assert.Equal(t, src.MapPtr, dst.MapPtr) 129 | assert.Equal(t, src.Struct, dst.Struct) 130 | assert.Equal(t, src.StructPtr, dst.StructPtr) 131 | assert.Zero(t, dst.Skipped) 132 | 133 | dst = &Dst{} 134 | assert.Nil(t, deepcopier.Copy(dst).From(srcRenamed)) 135 | assert.Equal(t, srcRenamed.MyInt, dst.Int) 136 | assert.Equal(t, srcRenamed.MyIntPtr, dst.IntPtr) 137 | assert.Equal(t, srcRenamed.MySlice, dst.Slice) 138 | assert.Equal(t, srcRenamed.MySlicePtr, dst.SlicePtr) 139 | assert.Equal(t, srcRenamed.MyMap, dst.Map) 140 | assert.Equal(t, srcRenamed.MyMapPtr, dst.MapPtr) 141 | assert.Equal(t, srcRenamed.MyStruct, dst.Struct) 142 | assert.Equal(t, srcRenamed.MyStructPtr, dst.StructPtr) 143 | assert.Zero(t, dst.Skipped) 144 | } 145 | 146 | func TestField_PointerToValue(t *testing.T) { 147 | type ( 148 | Rel struct { 149 | Int int 150 | } 151 | 152 | Src struct { 153 | Int *int 154 | Slice *[]string 155 | Map *map[string]interface{} 156 | Struct *Rel 157 | } 158 | 159 | Dst struct { 160 | Int int 161 | Slice []string 162 | Map map[string]interface{} 163 | Struct Rel 164 | } 165 | 166 | SrcRenamed struct { 167 | MyInt *int `deepcopier:"field:Int"` 168 | MySlice *[]string `deepcopier:"field:Slice"` 169 | MyMap *map[string]interface{} `deepcopier:"field:Map"` 170 | MyStruct *Rel `deepcopier:"field:Struct"` 171 | } 172 | 173 | DstRenamed struct { 174 | MyInt int `deepcopier:"field:Int"` 175 | MySlice []string `deepcopier:"field:Slice"` 176 | MyMap map[string]interface{} `deepcopier:"field:Map"` 177 | MyStruct Rel `deepcopier:"field:Struct"` 178 | } 179 | ) 180 | 181 | var ( 182 | rel = Rel{Int: 1} 183 | integer = 1 184 | slc = []string{"one", "two"} 185 | mp = map[string]interface{}{"one": 1} 186 | ) 187 | 188 | src := &Src{ 189 | Int: &integer, 190 | Slice: &slc, 191 | Map: &mp, 192 | Struct: &rel, 193 | } 194 | 195 | srcRenamed := &SrcRenamed{ 196 | MyInt: &integer, 197 | MySlice: &slc, 198 | MyMap: &mp, 199 | MyStruct: &rel, 200 | } 201 | 202 | // 203 | // To() 204 | // 205 | 206 | dst := &Dst{} 207 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 208 | assert.Equal(t, *src.Int, dst.Int) 209 | assert.Equal(t, *src.Slice, dst.Slice) 210 | assert.Equal(t, *src.Map, dst.Map) 211 | assert.Equal(t, *src.Struct, dst.Struct) 212 | 213 | dstRenamed := &DstRenamed{} 214 | assert.Nil(t, deepcopier.Copy(src).To(dstRenamed)) 215 | assert.Equal(t, *src.Int, dstRenamed.MyInt) 216 | assert.Equal(t, *src.Slice, dstRenamed.MySlice) 217 | assert.Equal(t, *src.Map, dstRenamed.MyMap) 218 | assert.Equal(t, *src.Struct, dstRenamed.MyStruct) 219 | 220 | // 221 | // From() 222 | // 223 | 224 | dst = &Dst{} 225 | assert.Nil(t, deepcopier.Copy(dst).From(src)) 226 | assert.Equal(t, *src.Int, dst.Int) 227 | assert.Equal(t, *src.Slice, dst.Slice) 228 | assert.Equal(t, *src.Map, dst.Map) 229 | assert.Equal(t, *src.Struct, dst.Struct) 230 | 231 | dst = &Dst{} 232 | assert.Nil(t, deepcopier.Copy(dst).From(srcRenamed)) 233 | assert.Equal(t, *srcRenamed.MyInt, dst.Int) 234 | assert.Equal(t, *srcRenamed.MySlice, dst.Slice) 235 | assert.Equal(t, *srcRenamed.MyMap, dst.Map) 236 | assert.Equal(t, *srcRenamed.MyStruct, dst.Struct) 237 | } 238 | 239 | func TestField_Unexported(t *testing.T) { 240 | type ( 241 | Src struct { 242 | Exported int 243 | unexported string 244 | } 245 | 246 | Dst struct { 247 | Exported int 248 | unexported string 249 | } 250 | ) 251 | 252 | src := &Src{Exported: 1, unexported: "unexported"} 253 | 254 | // 255 | // To() 256 | // 257 | 258 | dst := &Dst{} 259 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 260 | assert.Equal(t, "", dst.unexported) 261 | 262 | // 263 | // From() 264 | // 265 | 266 | dst = &Dst{} 267 | assert.Nil(t, deepcopier.Copy(dst).From(src)) 268 | assert.Equal(t, "", dst.unexported) 269 | } 270 | 271 | func TestField_Unknown(t *testing.T) { 272 | type ( 273 | Original struct { 274 | Int int 275 | } 276 | 277 | Renamed struct { 278 | MyInt int `deepcopier:"field:Integer"` 279 | } 280 | ) 281 | 282 | // 283 | // To() 284 | // 285 | 286 | src := &Original{Int: 1} 287 | dstRenamed := &Renamed{} 288 | assert.Nil(t, deepcopier.Copy(src).To(dstRenamed)) 289 | assert.Equal(t, 0, dstRenamed.MyInt) 290 | 291 | // 292 | // From() 293 | // 294 | 295 | srcRenamed := &Renamed{MyInt: 1} 296 | dst := &Original{} 297 | assert.Nil(t, deepcopier.Copy(dst).From(srcRenamed)) 298 | assert.Equal(t, 0, dst.Int) 299 | } 300 | 301 | func TestField_EmptyInterface(t *testing.T) { 302 | type ( 303 | Rel struct { 304 | Int int 305 | } 306 | 307 | Src struct { 308 | Rel *Rel 309 | } 310 | 311 | SrcForce struct { 312 | Rel *Rel `deepcopier:"force"` 313 | } 314 | 315 | Dst struct { 316 | Rel interface{} 317 | } 318 | 319 | DstForce struct { 320 | Rel interface{} `deepcopier:"force"` 321 | } 322 | ) 323 | 324 | var ( 325 | rel = &Rel{Int: 1} 326 | src = &Src{Rel: rel} 327 | srcForce = &SrcForce{Rel: rel} 328 | ) 329 | 330 | // 331 | // Without force 332 | // 333 | 334 | dst := &Dst{} 335 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 336 | assert.Nil(t, dst.Rel) 337 | 338 | dst = &Dst{} 339 | assert.Nil(t, deepcopier.Copy(dst).From(src)) 340 | assert.Nil(t, dst.Rel) 341 | 342 | // 343 | // With force 344 | // 345 | 346 | dstForce := &DstForce{} 347 | assert.Nil(t, deepcopier.Copy(src).To(dstForce)) 348 | assert.Equal(t, src.Rel, dstForce.Rel) 349 | 350 | dstForce = &DstForce{} 351 | assert.Nil(t, deepcopier.Copy(dstForce).From(srcForce)) 352 | assert.Equal(t, srcForce.Rel, dstForce.Rel) 353 | } 354 | 355 | func TestField_NullTypes(t *testing.T) { 356 | type ( 357 | Src struct { 358 | PQNullTimeValid pq.NullTime 359 | PQNullTimeValidPtr pq.NullTime 360 | PQNullTimeInvalid pq.NullTime 361 | PQNullTimeInvalidPtr pq.NullTime 362 | 363 | NullStringValid null.String 364 | NullStringValidPtr null.String 365 | NullStringInvalid null.String 366 | NullStringInvalidPtr null.String 367 | 368 | SQLNullStringValid sql.NullString 369 | SQLNullStringValidPtr sql.NullString 370 | SQLNullStringInvalid sql.NullString 371 | SQLNullStringInvalidPtr sql.NullString 372 | 373 | SQLNullInt64Valid sql.NullInt64 374 | SQLNullInt64ValidPtr sql.NullInt64 375 | SQLNullInt64Invalid sql.NullInt64 376 | SQLNullInt64InvalidPtr sql.NullInt64 377 | 378 | SQLNullBoolValid sql.NullBool 379 | SQLNullBoolValidPtr sql.NullBool 380 | SQLNullBoolInvalid sql.NullBool 381 | SQLNullBoolInvalidPtr sql.NullBool 382 | } 383 | 384 | SrcForce struct { 385 | PQNullTimeValid pq.NullTime `deepcopier:"force"` 386 | PQNullTimeValidPtr pq.NullTime `deepcopier:"force"` 387 | PQNullTimeInvalid pq.NullTime `deepcopier:"force"` 388 | PQNullTimeInvalidPtr pq.NullTime `deepcopier:"force"` 389 | 390 | NullStringValid null.String `deepcopier:"force"` 391 | NullStringValidPtr null.String `deepcopier:"force"` 392 | NullStringInvalid null.String `deepcopier:"force"` 393 | NullStringInvalidPtr null.String `deepcopier:"force"` 394 | 395 | SQLNullStringValid sql.NullString `deepcopier:"force"` 396 | SQLNullStringValidPtr sql.NullString `deepcopier:"force"` 397 | SQLNullStringInvalid sql.NullString `deepcopier:"force"` 398 | SQLNullStringInvalidPtr sql.NullString `deepcopier:"force"` 399 | 400 | SQLNullInt64Valid sql.NullInt64 `deepcopier:"force"` 401 | SQLNullInt64ValidPtr sql.NullInt64 `deepcopier:"force"` 402 | SQLNullInt64Invalid sql.NullInt64 `deepcopier:"force"` 403 | SQLNullInt64InvalidPtr sql.NullInt64 `deepcopier:"force"` 404 | 405 | SQLNullBoolValid sql.NullBool `deepcopier:"force"` 406 | SQLNullBoolValidPtr sql.NullBool `deepcopier:"force"` 407 | SQLNullBoolInvalid sql.NullBool `deepcopier:"force"` 408 | SQLNullBoolInvalidPtr sql.NullBool `deepcopier:"force"` 409 | } 410 | 411 | Dst struct { 412 | PQNullTimeValid time.Time 413 | PQNullTimeValidPtr *time.Time 414 | PQNullTimeInvalid time.Time 415 | PQNullTimeInvalidPtr *time.Time 416 | 417 | NullStringValid string 418 | NullStringValidPtr *string 419 | NullStringInvalid string 420 | NullStringInvalidPtr *string 421 | 422 | SQLNullStringValid string 423 | SQLNullStringValidPtr *string 424 | SQLNullStringInvalid string 425 | SQLNullStringInvalidPtr *string 426 | 427 | SQLNullInt64Valid int64 428 | SQLNullInt64ValidPtr *int64 429 | SQLNullInt64Invalid int64 430 | SQLNullInt64InvalidPtr *int64 431 | 432 | SQLNullBoolValid bool 433 | SQLNullBoolValidPtr *bool 434 | SQLNullBoolInvalid bool 435 | SQLNullBoolInvalidPtr *bool 436 | } 437 | 438 | DstForce struct { 439 | PQNullTimeValid time.Time `deepcopier:"force"` 440 | PQNullTimeValidPtr *time.Time `deepcopier:"force"` 441 | PQNullTimeInvalid time.Time `deepcopier:"force"` 442 | PQNullTimeInvalidPtr *time.Time `deepcopier:"force"` 443 | 444 | NullStringValid string `deepcopier:"force"` 445 | NullStringValidPtr *string `deepcopier:"force"` 446 | NullStringInvalid string `deepcopier:"force"` 447 | NullStringInvalidPtr *string `deepcopier:"force"` 448 | 449 | SQLNullStringValid string `deepcopier:"force"` 450 | SQLNullStringValidPtr *string `deepcopier:"force"` 451 | SQLNullStringInvalid string `deepcopier:"force"` 452 | SQLNullStringInvalidPtr *string `deepcopier:"force"` 453 | 454 | SQLNullInt64Valid int64 `deepcopier:"force"` 455 | SQLNullInt64ValidPtr *int64 `deepcopier:"force"` 456 | SQLNullInt64Invalid int64 `deepcopier:"force"` 457 | SQLNullInt64InvalidPtr *int64 `deepcopier:"force"` 458 | 459 | SQLNullBoolValid bool `deepcopier:"force"` 460 | SQLNullBoolValidPtr *bool `deepcopier:"force"` 461 | SQLNullBoolInvalid bool `deepcopier:"force"` 462 | SQLNullBoolInvalidPtr *bool `deepcopier:"force"` 463 | } 464 | ) 465 | 466 | now := time.Now() 467 | 468 | src := &Src{ 469 | PQNullTimeValid: pq.NullTime{Valid: true, Time: now}, 470 | PQNullTimeValidPtr: pq.NullTime{Valid: true, Time: now}, 471 | PQNullTimeInvalid: pq.NullTime{Valid: false, Time: now}, 472 | PQNullTimeInvalidPtr: pq.NullTime{Valid: false, Time: now}, 473 | 474 | NullStringValid: null.NewString("hello", true), 475 | NullStringValidPtr: null.NewString("hello", true), 476 | NullStringInvalid: null.NewString("hello", false), 477 | NullStringInvalidPtr: null.NewString("hello", false), 478 | 479 | SQLNullStringValid: sql.NullString{Valid: true, String: "hello"}, 480 | SQLNullStringValidPtr: sql.NullString{Valid: true, String: "hello"}, 481 | SQLNullStringInvalid: sql.NullString{Valid: false, String: "hello"}, 482 | SQLNullStringInvalidPtr: sql.NullString{Valid: false, String: "hello"}, 483 | 484 | SQLNullInt64Valid: sql.NullInt64{Valid: true, Int64: 1}, 485 | SQLNullInt64ValidPtr: sql.NullInt64{Valid: true, Int64: 1}, 486 | SQLNullInt64Invalid: sql.NullInt64{Valid: false, Int64: 1}, 487 | SQLNullInt64InvalidPtr: sql.NullInt64{Valid: false, Int64: 1}, 488 | 489 | SQLNullBoolValid: sql.NullBool{Valid: true, Bool: true}, 490 | SQLNullBoolValidPtr: sql.NullBool{Valid: true, Bool: true}, 491 | SQLNullBoolInvalid: sql.NullBool{Valid: false, Bool: true}, 492 | SQLNullBoolInvalidPtr: sql.NullBool{Valid: false, Bool: true}, 493 | } 494 | 495 | srcForce := &SrcForce{ 496 | PQNullTimeValid: pq.NullTime{Valid: true, Time: now}, 497 | PQNullTimeValidPtr: pq.NullTime{Valid: true, Time: now}, 498 | PQNullTimeInvalid: pq.NullTime{Valid: false, Time: now}, 499 | PQNullTimeInvalidPtr: pq.NullTime{Valid: false, Time: now}, 500 | 501 | NullStringValid: null.NewString("hello", true), 502 | NullStringValidPtr: null.NewString("hello", true), 503 | NullStringInvalid: null.NewString("hello", false), 504 | NullStringInvalidPtr: null.NewString("hello", false), 505 | 506 | SQLNullStringValid: sql.NullString{Valid: true, String: "hello"}, 507 | SQLNullStringValidPtr: sql.NullString{Valid: true, String: "hello"}, 508 | SQLNullStringInvalid: sql.NullString{Valid: false, String: "hello"}, 509 | SQLNullStringInvalidPtr: sql.NullString{Valid: false, String: "hello"}, 510 | 511 | SQLNullInt64Valid: sql.NullInt64{Valid: true, Int64: 1}, 512 | SQLNullInt64ValidPtr: sql.NullInt64{Valid: true, Int64: 1}, 513 | SQLNullInt64Invalid: sql.NullInt64{Valid: false, Int64: 1}, 514 | SQLNullInt64InvalidPtr: sql.NullInt64{Valid: false, Int64: 1}, 515 | 516 | SQLNullBoolValid: sql.NullBool{Valid: true, Bool: true}, 517 | SQLNullBoolValidPtr: sql.NullBool{Valid: true, Bool: true}, 518 | SQLNullBoolInvalid: sql.NullBool{Valid: false, Bool: true}, 519 | SQLNullBoolInvalidPtr: sql.NullBool{Valid: false, Bool: true}, 520 | } 521 | 522 | // 523 | // Without force 524 | // 525 | 526 | dst := &Dst{} 527 | 528 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 529 | assert.Zero(t, dst.PQNullTimeValid) 530 | assert.Nil(t, dst.PQNullTimeValidPtr) 531 | assert.Zero(t, dst.PQNullTimeInvalid) 532 | assert.Nil(t, dst.PQNullTimeInvalidPtr) 533 | 534 | assert.Zero(t, dst.NullStringValid) 535 | assert.Nil(t, dst.NullStringValidPtr) 536 | assert.Zero(t, dst.NullStringInvalid) 537 | assert.Nil(t, dst.NullStringInvalidPtr) 538 | 539 | assert.Zero(t, dst.SQLNullStringValid) 540 | assert.Nil(t, dst.SQLNullStringValidPtr) 541 | assert.Zero(t, dst.SQLNullStringInvalid) 542 | assert.Nil(t, dst.SQLNullStringInvalidPtr) 543 | 544 | assert.Zero(t, dst.SQLNullInt64Valid) 545 | assert.Nil(t, dst.SQLNullInt64ValidPtr) 546 | assert.Zero(t, dst.SQLNullInt64Invalid) 547 | assert.Nil(t, dst.SQLNullInt64InvalidPtr) 548 | 549 | assert.Zero(t, dst.SQLNullBoolValid) 550 | assert.Nil(t, dst.SQLNullBoolValidPtr) 551 | assert.Zero(t, dst.SQLNullBoolInvalid) 552 | assert.Nil(t, dst.SQLNullBoolInvalidPtr) 553 | 554 | // 555 | // With force 556 | // 557 | 558 | dstForce := &DstForce{} 559 | assert.Nil(t, deepcopier.Copy(srcForce).To(dstForce)) 560 | 561 | assert.Equal(t, srcForce.PQNullTimeValid.Time, dstForce.PQNullTimeValid) 562 | assert.NotNil(t, dstForce.PQNullTimeValidPtr) 563 | assert.Equal(t, srcForce.PQNullTimeValidPtr.Time, *dstForce.PQNullTimeValidPtr) 564 | assert.Zero(t, dstForce.PQNullTimeInvalid) 565 | assert.Nil(t, dstForce.PQNullTimeInvalidPtr) 566 | 567 | assert.Equal(t, srcForce.NullStringValid.String, dstForce.NullStringValid) 568 | assert.NotNil(t, dstForce.NullStringValidPtr) 569 | assert.Equal(t, srcForce.NullStringValidPtr.String, *dstForce.NullStringValidPtr) 570 | assert.Zero(t, dstForce.NullStringInvalid) 571 | assert.Nil(t, dstForce.NullStringInvalidPtr) 572 | 573 | assert.Equal(t, srcForce.SQLNullStringValid.String, dstForce.SQLNullStringValid) 574 | assert.NotNil(t, dstForce.SQLNullStringValidPtr) 575 | assert.Equal(t, srcForce.SQLNullStringValidPtr.String, *dstForce.SQLNullStringValidPtr) 576 | assert.Zero(t, dstForce.SQLNullStringInvalid) 577 | assert.Nil(t, dstForce.SQLNullStringInvalidPtr) 578 | 579 | assert.Equal(t, srcForce.SQLNullInt64Valid.Int64, dstForce.SQLNullInt64Valid) 580 | assert.NotNil(t, dstForce.SQLNullInt64ValidPtr) 581 | assert.Equal(t, srcForce.SQLNullInt64ValidPtr.Int64, *dstForce.SQLNullInt64ValidPtr) 582 | assert.Zero(t, dstForce.SQLNullInt64Invalid) 583 | assert.Nil(t, dstForce.SQLNullInt64InvalidPtr) 584 | 585 | assert.Equal(t, srcForce.SQLNullBoolValid.Bool, dstForce.SQLNullBoolValid) 586 | assert.NotNil(t, dstForce.SQLNullBoolValidPtr) 587 | assert.Equal(t, srcForce.SQLNullBoolValidPtr.Bool, *dstForce.SQLNullBoolValidPtr) 588 | assert.Zero(t, dstForce.SQLNullBoolInvalid) 589 | assert.Nil(t, dstForce.SQLNullBoolInvalidPtr) 590 | } 591 | 592 | func TestField_SameNameWithDifferentType(t *testing.T) { 593 | type ( 594 | FooInt struct { 595 | Foo int 596 | } 597 | 598 | FooStr struct { 599 | Foo string 600 | } 601 | ) 602 | 603 | // 604 | // To() 605 | // 606 | 607 | srcInt := &FooInt{Foo: 1} 608 | dstStr := &FooStr{} 609 | 610 | assert.Nil(t, deepcopier.Copy(dstStr).From(srcInt)) 611 | assert.Empty(t, dstStr.Foo) 612 | 613 | // 614 | // From() 615 | // 616 | 617 | dstStr = &FooStr{} 618 | assert.Nil(t, deepcopier.Copy(dstStr).From(srcInt)) 619 | assert.Empty(t, dstStr.Foo) 620 | } 621 | 622 | func TestMethod(t *testing.T) { 623 | var ( 624 | c = map[string]interface{}{"message": "hello"} 625 | src = &MethodTesterFoo{TagFirst: "field-value"} 626 | dst = &MethodTesterBar{} 627 | ) 628 | 629 | // 630 | // To() 631 | // 632 | 633 | assert.Nil(t, deepcopier.Copy(src).WithContext(c).To(dst)) 634 | assert.Equal(t, c, dst.FooContext) 635 | assert.Equal(t, MethodTesterFoo{}.FooInteger(), dst.FooInteger) 636 | assert.Empty(t, dst.FooSkipped) 637 | assert.Equal(t, "method-value", dst.TagFirst) 638 | 639 | assert.Equal(t, MethodTesterFoo{}.FooSliceToSlicePtr(), *dst.FooSliceToSlicePtr) 640 | assert.Equal(t, *MethodTesterFoo{}.FooSlicePtrToSlice(), dst.FooSlicePtrToSlice) 641 | 642 | assert.Equal(t, MethodTesterFoo{}.FooStringToStringPtr(), *dst.FooStringToStringPtr) 643 | assert.Equal(t, *MethodTesterFoo{}.FooStringPtrToString(), dst.FooStringPtrToString) 644 | 645 | assert.Equal(t, MethodTesterFoo{}.FooMapToMapPtr(), *dst.FooMapToMapPtr) 646 | assert.Equal(t, *MethodTesterFoo{}.FooMapPtrToMap(), dst.FooMapPtrToMap) 647 | 648 | // 649 | // From() 650 | // 651 | 652 | dst = &MethodTesterBar{} 653 | assert.Nil(t, deepcopier.Copy(dst).WithContext(c).From(src)) 654 | assert.Equal(t, c, dst.FooContext) 655 | assert.Equal(t, MethodTesterFoo{}.FooInteger(), dst.FooInteger) 656 | assert.Empty(t, dst.FooSkipped) 657 | assert.Equal(t, "method-value", dst.TagFirst) 658 | 659 | assert.Equal(t, MethodTesterFoo{}.FooSliceToSlicePtr(), *dst.FooSliceToSlicePtr) 660 | assert.Equal(t, *MethodTesterFoo{}.FooSlicePtrToSlice(), dst.FooSlicePtrToSlice) 661 | 662 | assert.Equal(t, MethodTesterFoo{}.FooStringToStringPtr(), *dst.FooStringToStringPtr) 663 | assert.Equal(t, *MethodTesterFoo{}.FooStringPtrToString(), dst.FooStringPtrToString) 664 | 665 | assert.Equal(t, MethodTesterFoo{}.FooMapToMapPtr(), *dst.FooMapToMapPtr) 666 | assert.Equal(t, *MethodTesterFoo{}.FooMapPtrToMap(), dst.FooMapPtrToMap) 667 | } 668 | 669 | func TestAnonymousStruct(t *testing.T) { 670 | type ( 671 | Embedded struct{ Int int } 672 | EmbeddedRenamedField struct { 673 | MyInt int `deepcopier:"field:Int"` 674 | } 675 | 676 | Src struct{ Embedded } 677 | SrcRenamedField struct{ EmbeddedRenamedField } 678 | 679 | Dst struct{ Int int } 680 | DstRenamedField struct { 681 | MyInt int `deepcopier:"field:Int"` 682 | } 683 | ) 684 | 685 | var ( 686 | embedded = Embedded{Int: 1} 687 | embeddedRenamedField = EmbeddedRenamedField{MyInt: 1} 688 | src = &Src{Embedded: embedded} 689 | srcRenamedField = &SrcRenamedField{EmbeddedRenamedField: embeddedRenamedField} 690 | ) 691 | 692 | // 693 | // To() 694 | // 695 | 696 | dst := &Dst{} 697 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 698 | assert.Equal(t, src.Int, dst.Int) 699 | 700 | dstRenamedField := &DstRenamedField{} 701 | assert.Nil(t, deepcopier.Copy(src).To(dstRenamedField)) 702 | assert.Equal(t, src.Int, dstRenamedField.MyInt) 703 | 704 | // 705 | // From() 706 | // 707 | 708 | dst = &Dst{} 709 | assert.Nil(t, deepcopier.Copy(dst).From(src)) 710 | assert.Equal(t, src.Int, dst.Int) 711 | 712 | dst = &Dst{} 713 | assert.Nil(t, deepcopier.Copy(dst).From(srcRenamedField)) 714 | assert.Equal(t, srcRenamedField.MyInt, dst.Int) 715 | } 716 | 717 | func TestNullableType(t *testing.T) { 718 | type Value struct { 719 | UUID uuid.UUID 720 | } 721 | 722 | type Ptr struct { 723 | UUID *uuid.UUID 724 | } 725 | 726 | type ToString struct { 727 | UUID uuid.UUID `deepcopier:"force"` 728 | } 729 | 730 | type PtrToString struct { 731 | UUID *uuid.UUID `deepcopier:"force"` 732 | } 733 | 734 | type FromNullable struct { 735 | UUID string `deepcopier:"force"` 736 | } 737 | 738 | type PtrFromNullable struct { 739 | UUID *string `deepcopier:"force"` 740 | } 741 | 742 | // Same type: value -- copy to 743 | { 744 | src := &Value{UUID: uuid.NewV4()} 745 | dst := &Value{} 746 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 747 | assert.Equal(t, src.UUID, dst.UUID) 748 | } 749 | 750 | // Same type: value -- copy from 751 | { 752 | src := &Value{} 753 | from := &Value{UUID: uuid.NewV4()} 754 | assert.Nil(t, deepcopier.Copy(src).From(from)) 755 | assert.Equal(t, from.UUID, src.UUID) 756 | } 757 | 758 | // Same type: pointer -- copy to 759 | { 760 | uid := uuid.NewV4() 761 | src := &Ptr{UUID: &uid} 762 | dst := &Ptr{} 763 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 764 | assert.Equal(t, src.UUID, dst.UUID) 765 | } 766 | 767 | // Same type: pointer -- copy from 768 | { 769 | uid := uuid.NewV4() 770 | src := &Ptr{} 771 | from := &Ptr{UUID: &uid} 772 | assert.Nil(t, deepcopier.Copy(src).From(from)) 773 | assert.Equal(t, from.UUID, src.UUID) 774 | } 775 | 776 | // Value to value -- copy to 777 | { 778 | src := &Value{UUID: uuid.NewV4()} 779 | dst := &FromNullable{} 780 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 781 | assert.Equal(t, src.UUID.String(), dst.UUID) 782 | } 783 | 784 | // Value to value -- copy from 785 | { 786 | src := &FromNullable{} 787 | from := &ToString{UUID: uuid.NewV4()} 788 | assert.Nil(t, deepcopier.Copy(src).From(from)) 789 | assert.Equal(t, from.UUID.String(), src.UUID) 790 | } 791 | 792 | // Value to pointer -- copy to 793 | { 794 | src := &ToString{UUID: uuid.NewV4()} 795 | dst := &PtrFromNullable{} 796 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 797 | assert.Equal(t, src.UUID.String(), *dst.UUID) 798 | } 799 | 800 | // Value to pointer -- copy from 801 | { 802 | src := &PtrFromNullable{} 803 | from := &ToString{UUID: uuid.NewV4()} 804 | assert.Nil(t, deepcopier.Copy(src).From(from)) 805 | assert.Equal(t, from.UUID.String(), *src.UUID) 806 | } 807 | 808 | // Pointer to value -- copy to 809 | { 810 | uid := uuid.NewV4() 811 | src := &PtrToString{UUID: &uid} 812 | dst := &FromNullable{} 813 | assert.Nil(t, deepcopier.Copy(src).To(dst)) 814 | assert.Equal(t, src.UUID.String(), dst.UUID) 815 | } 816 | 817 | // Pointer to value -- copy from 818 | { 819 | uid := uuid.NewV4() 820 | src := &FromNullable{} 821 | from := &PtrToString{UUID: &uid} 822 | assert.Nil(t, deepcopier.Copy(src).From(from)) 823 | assert.Equal(t, from.UUID.String(), src.UUID) 824 | } 825 | } 826 | 827 | // ---------------------------------------------------------------------------- 828 | // Method testers 829 | // ---------------------------------------------------------------------------- 830 | 831 | type MethodTesterFoo struct { 832 | BarInteger int 833 | BarContext map[string]interface{} `deepcopier:"context"` 834 | BarSkipped string `deepcopier:"skip"` 835 | TagFirst string `deepcopier:"field:GetTagFirst"` 836 | } 837 | 838 | func (MethodTesterFoo) FooInteger() int { 839 | return 1 840 | } 841 | 842 | func (MethodTesterFoo) FooContext(c map[string]interface{}) map[string]interface{} { 843 | return c 844 | } 845 | 846 | func (MethodTesterFoo) FooSkipped() string { 847 | return "skipped" 848 | } 849 | 850 | func (MethodTesterFoo) GetTagFirst() string { 851 | return "method-value" 852 | } 853 | 854 | func (MethodTesterFoo) FooSliceToSlicePtr() []string { 855 | return []string{"hello"} 856 | } 857 | 858 | func (MethodTesterFoo) FooSlicePtrToSlice() *[]string { 859 | return &[]string{"hello"} 860 | } 861 | 862 | func (MethodTesterFoo) FooStringToStringPtr() string { 863 | return "hello" 864 | } 865 | 866 | func (MethodTesterFoo) FooStringPtrToString() *string { 867 | s := "hello" 868 | return &s 869 | } 870 | 871 | func (MethodTesterFoo) FooMapToMapPtr() map[string]interface{} { 872 | return map[string]interface{}{"one": 1} 873 | } 874 | 875 | func (MethodTesterFoo) FooMapPtrToMap() *map[string]interface{} { 876 | return &map[string]interface{}{"one": 1} 877 | } 878 | 879 | type MethodTesterBar struct { 880 | FooInteger int 881 | FooContext map[string]interface{} `deepcopier:"context"` 882 | FooSkipped string `deepcopier:"skip"` 883 | TagFirst string `deepcopier:"field:GetTagFirst"` 884 | FooSliceToSlicePtr *[]string `deepcopier:"force"` 885 | FooSlicePtrToSlice []string `deepcopier:"force"` 886 | FooStringToStringPtr *string `deepcopier:"force"` 887 | FooStringPtrToString string `deepcopier:"force"` 888 | FooMapToMapPtr *map[string]interface{} `deepcopier:"force"` 889 | FooMapPtrToMap map[string]interface{} `deepcopier:"force"` 890 | } 891 | 892 | func (MethodTesterBar) BarInteger() int { 893 | return 1 894 | } 895 | 896 | func (MethodTesterBar) BarContext(c map[string]interface{}) map[string]interface{} { 897 | return c 898 | } 899 | 900 | func (MethodTesterBar) BarSkipped() string { 901 | return "skipped" 902 | } 903 | 904 | func (MethodTesterBar) GetTagFirst() string { 905 | return "method-value" 906 | } 907 | -------------------------------------------------------------------------------- /tests/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ulule/deepcopier/tests 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/guregu/null v4.0.0+incompatible 7 | github.com/lib/pq v1.4.0 8 | github.com/satori/go.uuid v1.2.0 9 | github.com/stretchr/testify v1.5.1 10 | github.com/ulule/deepcopier v0.0.0 11 | ) 12 | 13 | replace github.com/ulule/deepcopier => ../ 14 | -------------------------------------------------------------------------------- /tests/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw= 4 | github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= 5 | github.com/lib/pq v1.4.0 h1:TmtCFbH+Aw0AixwyttznSMQDgbR5Yed/Gg6S8Funrhc= 6 | github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 10 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 13 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 17 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 18 | --------------------------------------------------------------------------------