github.com/hedzr/evendeep@v0.4.8/README.md (about) 1 # even-deep 2 3 ![Go](https://github.com/hedzr/evendeep/workflows/Go/badge.svg) 4 [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/hedzr/evendeep.svg?label=release)](https://github.com/hedzr/evendeep/releases) 5 [![go.dev](https://img.shields.io/badge/go.dev-reference-green)](https://pkg.go.dev/github.com/hedzr/evendeep) 6 [![Go Report Card](https://goreportcard.com/badge/github.com/hedzr/evendeep)](https://goreportcard.com/report/github.com/hedzr/evendeep) 7 [![codecov](https://codecov.io/gh/hedzr/evendeep/branch/master/graph/badge.svg)](https://codecov.io/gh/hedzr/evendeep) 8 [![Coverage Status](https://coveralls.io/repos/github/hedzr/evendeep/badge.svg?branch=master)](https://coveralls.io/github/hedzr/evendeep?branch=master) 9 10 Per-field copying deeply, and comparing deeply abilities. 11 12 This library is designed for making everything customizable. 13 14 ## Features 15 16 - loosely and reasonable data-types conversions, acrossing primitives, composites and functions, with customizable 17 converters/transformers 18 - unexported values (optional), ... 19 - circular references immunization 20 - fully customizable 21 - user-defined value/type converters/transformers 22 - user-defined field to field name converting rule via struct Tag 23 - easily apply different strategies 24 - basic strategies are: copy-n-merge, clone, 25 - strategies per struct field: 26 `slicecopy`, `slicemerge`, `mapcopy`, `mapmerge`, 27 `omitempty` (keep if source is zero or nil), `omitnil`, `omitzero`, 28 `keepneq` (keep if not equal), `cleareq` (clear if equal), ... 29 - copy fields by name or ordinal 30 - field to field 31 - field to method, method to field 32 - value to function (as input), function result to value 33 - slice[0] to struct, struct to slice[0] 34 - struct to map, map to struct 35 - User-defined extractor/getter on various source 36 - User-defined setter for struct or map target (if mapkey is string) 37 - ... 38 39 - The deep series 40 - deepcopy: [`DeepCopy()`](https://github.com/hedzr/evendeep/blob/master/deepcopy.go#L20), 41 or [`New()`](https://github.com/hedzr/evendeep/blob/master/deepcopy.go#L110) 42 - deepclone:[`MakeClone()`](https://github.com/hedzr/evendeep/blob/master/deepcopy.go#L36) 43 - deepequal: [`DeepEqual()`](https://github.com/hedzr/evendeep/blob/master/equal.go#L13) 44 - deepdiff: [`DeepDiff()`](https://github.com/hedzr/evendeep/blob/master/diff.go#L13) 45 46 - Compatibilities 47 - Run for Go Modules enabled (go1.11+) 48 49 ## History 50 51 - v0.4.8 52 - fixed: check unexported field recursively now 53 - improved some lines for better context logging in debugging 54 - little changes 55 56 - v0.4.7 57 - upgrade deps 58 59 - v0.4.3 60 - fixed sometimes a ptr to new slice has not been cleaned in time 61 62 - v0.4.1 63 - public `dbglog` subpackage, added Err/Wrn/Colored 64 - added ability to disable dbglog.Log at runtime 65 - improved internal functions (tool.Valfmt, cl.SetUnexportedFieldIfMap, ...) 66 - improved dbglog.Log outputting 67 - fixed bugs 68 69 - v0.4.0 70 - fixed autonew when copying to nil member 71 - improved diff on chan 72 - better logging (verbose) with colors 73 74 - v0.3.1 75 - changed: `dbglog.LogValid` is constant now 76 - improved code style 77 - DeepCopy: 78 - passing nil parameters can return safely without panic any more 79 - DeepDiff: 80 - imp/fea: `diff.WithStripPointerAtFirst` - locate the final objects and compare them 81 - imp/fea: `diff.WithTreatEmptyStructPtrAsNilPtr` - when comparing two pointers in struct field loop, assume nil and pointer to an empty struct is identical 82 - imp/fea: `diff.WithCompareDifferentTypeStructs` - you can compare two struct with different type, their fields could be `diff` by its name 83 - imp/fea: `diff.WithIgnoreUnmatchedFields` - this is default option for `diff.WithCompareDifferentTypeStructs(true)` mode, the field names unmatched couldn't take effects to comparing result 84 - imp/fea: `diff.WithCompareDifferentSizeArrays` - `[2]string{"1","2"}` and `[3]string{"1","2",<empty>}` can be assumed as identity 85 - By default, 86 - they're assumed as identity: nil and zero array/map. 87 - they're not identical: nil ptr to struct, and ptr to empty struct (can be overridden by `WithTreatEmptyStructPtrAsNilPtr`). 88 - the slice elements' order is sensitive, except you're `diff` with `WithSliceOrderedComparison(true)`. 89 90 - More in [CHANGELOG](https://github.com/hedzr/evendeep/blob/master/CHANGELOG) 91 92 ## Usages 93 94 ### deepcopy 95 96 `eventdeep.New`, `eventdeep.MakeClone` and `eventdeep.DeepCopy` are main entries. 97 98 By default, `DeepCopy()` will copy and **merge** source into destination object. That means, a map or a slice will be merged 99 deeply, same to a struct. 100 101 [`New(opts...)`](https://github.com/hedzr/evendeep/blob/master/deepcopy.go#L110) gives a most even scalable interface 102 than `DeepCopy`, it returns a new `DeepCopier` different to `DefaultCopyController` and you can make call 103 to `DeepCopier.DeepCopy(old, new, opts...)`. 104 105 In copy-n-merge mode, copying `[2, 3]` to `[3, 7]` will get `[3, 7, 2]`. 106 107 #### Getting Started 108 109 Here is a basic sample code: 110 111 ```go 112 func TestExample1(t *testing.T) { 113 timeZone, _ := time.LoadLocation("America/Phoenix") 114 tm := time.Date(1999, 3, 13, 5, 57, 11, 1901, timeZone) 115 src := eventdeep.Employee2{ 116 Base: eventdeep.Base{ 117 Name: "Bob", 118 Birthday: &tm, 119 Age: 24, 120 EmployeID: 7, 121 }, 122 Avatar: "https://tse4-mm.cn.bing.net/th/id/OIP-C.SAy__OKoxrIqrXWAb7Tj1wHaEC?pid=ImgDet&rs=1", 123 Image: []byte{95, 27, 43, 66, 0, 21, 210}, 124 Attr: &eventdeep.Attr{Attrs: []string{"hello", "world"}}, 125 Valid: true, 126 } 127 var dst eventdeep.User 128 129 // direct way but no error report: eventdeep.DeepCopy(src, &dst) 130 c := eventdeep.New() 131 if err := c.CopyTo(src, &dst); err != nil { 132 t.Fatal(err) 133 } 134 if !reflect.DeepEqual(dst, eventdeep.User{ 135 Name: "Bob", 136 Birthday: &tm, 137 Age: 24, 138 EmployeID: 7, 139 Avatar: "https://tse4-mm.cn.bing.net/th/id/OIP-C.SAy__OKoxrIqrXWAb7Tj1wHaEC?pid=ImgDet&rs=1", 140 Image: []byte{95, 27, 43, 66, 0, 21, 210}, 141 Attr: &eventdeep.Attr{Attrs: []string{"hello", "world"}}, 142 Valid: true, 143 }) { 144 t.Fatalf("bad, got %v", dst) 145 } 146 } 147 ``` 148 149 #### Customizing The Field Extractor 150 151 For the unconventional deep copy, we can copy field to field via a source extractor. 152 153 You need a target struct at first. 154 155 ```go 156 func TestStructWithSourceExtractor(t *testing.T) { 157 c := context.WithValue(context.TODO(), "Data", map[string]typ.Any{ 158 "A": 12, 159 }) 160 161 tgt := struct { 162 A int 163 }{} 164 165 evendeep.DeepCopy(c, &tgt, evendeep.WithSourceValueExtractor(func(name string) typ.Any { 166 if m, ok := c.Value("Data").(map[string]typ.Any); ok { 167 return m[name] 168 } 169 return nil 170 })) 171 172 if tgt.A != 12 { 173 t.FailNow() 174 } 175 } 176 ``` 177 178 #### Customizing The Target Setter 179 180 As a contrary, you might specify a setter to handle the setting action on copying struct and/or map. 181 182 ```go 183 func TestStructWithTargetSetter(t *testing.T) { 184 type srcS struct { 185 A int 186 B bool 187 C string 188 } 189 190 src := &srcS{ 191 A: 5, 192 B: true, 193 C: "helloString", 194 } 195 tgt := map[string]typ.Any{ 196 "Z": "str", 197 } 198 199 err := evendeep.New().CopyTo(src, &tgt, 200 evendeep.WithTargetValueSetter(func(value *reflect.Value, sourceNames ...string) (err error) { 201 if value != nil { 202 name := "Mo" + strings.Join(sourceNames, ".") 203 tgt[name] = value.Interface() 204 } 205 return // ErrShouldFallback to call the evendeep standard processing 206 }), 207 ) 208 209 if err != nil || tgt["MoA"] != 5 || tgt["MoB"] != true || tgt["MoC"] != "helloString" || tgt["Z"] != "str" { 210 t.Errorf("err: %v, tgt: %v", err, tgt) 211 t.FailNow() 212 } 213 } 214 ``` 215 216 NOTE that the feature is only fit for copying on/between struct and/or map. 217 218 If you really wanna customize the setter for primitives or others, concern to implement a ValueCopier or ValueConverter. 219 220 #### `ByOrdinal` or `ByName` 221 222 `evendeep` enumerates fields in struct/map/slice with two strategies: `ByOrdinal` and `ByName`. 223 224 1. Default `ByOrdinal` assumes the copier loops all source fields and copy them to the corresponding destination with 225 the ordinal order. 226 2. `ByName` strategy assumes the copier loops all target fields, and try copying value from the coressponding source 227 field by its name. 228 229 When a name conversion rule is defined in a struct field tag, the copier will look for the name and copy value to, even 230 if it's in `ByOrdinal` mode. 231 232 #### Customizing A Converter 233 234 The customized Type/Value Converter can be applied on transforming the data from source. For more information take a 235 look [`ValueConverter`](https://github.com/hedzr/evendeep/blob/master/cvts.go#L127) 236 and [`ValueCopier`](https://github.com/hedzr/evendeep/blob/master/cvts.go#L133). Its take effects on checking the value 237 type of target or source, or both of them. 238 239 ```go 240 type MyType struct { 241 I int 242 } 243 244 type MyTypeToStringConverter struct{} 245 246 // Uncomment this line if you wanna implment a ValueCopier implementation too: 247 // func (c *MyTypeToStringConverter) CopyTo(ctx *eventdeep.ValueConverterContext, source, target reflect.Value) (err error) { return } 248 249 func (c *MyTypeToStringConverter) Transform(ctx *eventdeep.ValueConverterContext, source reflect.Value, targetType reflect.Type) (target reflect.Value, err error) { 250 if source.IsValid() && targetType.Kind() == reflect.String { 251 var str string 252 if str, err = eventdeep.FallbackToBuiltinStringMarshalling(source); err == nil { 253 target = reflect.ValueOf(str) 254 } 255 } 256 return 257 } 258 259 func (c *MyTypeToStringConverter) Match(params *eventdeep.Params, source, target reflect.Type) (ctx *eventdeep.ValueConverterContext, yes bool) { 260 sn, sp := source.Name(), source.PkgPath() 261 sk, tk := source.Kind(), target.Kind() 262 if yes = sk == reflect.Struct && tk == reflect.String && 263 sn == "MyType" && sp == "github.com/hedzr/eventdeep_test"; yes { 264 ctx = &eventdeep.ValueConverterContext{Params: params} 265 } 266 return 267 } 268 269 func TestExample2(t *testing.T) { 270 var myData = MyType{I: 9} 271 var dst string 272 eventdeep.DeepCopy(myData, &dst, eventdeep.WithValueConverters(&MyTypeToStringConverter{})) 273 if dst != `{ 274 "I": 9 275 }` { 276 t.Fatalf("bad, got %v", dst) 277 } 278 } 279 ``` 280 281 Instead of `WithValueConverters` / `WithValueCopiers` for each times invoking `New()`, you might register yours once by 282 calling `RegisterDefaultConverters` / `RegisterDefaultCopiers` into global registry. 283 284 ```go 285 // a stub call for coverage 286 eventdeep.RegisterDefaultCopiers() 287 288 var dst1 string 289 eventdeep.RegisterDefaultConverters(&MyTypeToStringConverter{}) 290 eventdeep.DeepCopy(myData, &dst1) 291 if dst1 != `{ 292 "I": 9 293 }` { 294 t.Fatalf("bad, got %v", dst) 295 } 296 ``` 297 298 #### Zero Target Fields If Equals To Source 299 300 When we compare two Struct, the target one can be clear to zero except a field value is not equal to source field. This 301 feature can be used for your ORM codes: someone loads a record as a golang struct variable, and make some changes, and 302 invoking `eventdeep.DeepCopy(originRec, &newRecord, eventdeep.WithORMDiffOpt)`, the changes will be kept in `newRecord` 303 and the others unchanged fields be cleanup at last. 304 305 The codes are: 306 307 ```go 308 func TestExample3(t *testing.T) { 309 timeZone, _ := time.LoadLocation("America/Phoenix") 310 tm := time.Date(1999, 3, 13, 5, 57, 11, 1901, timeZone) 311 var originRec = eventdeep.User{ ... } 312 var newRecord eventdeep.User 313 var t0 = time.Unix(0, 0) 314 var expectRec = eventdeep.User{Name: "Barbara", Birthday: &t0, Attr: &eventdeep.Attr{}} 315 316 eventdeep.DeepCopy(originRec, &newRecord) 317 t.Logf("newRecord: %v", newRecord) 318 319 newRecord.Name = "Barbara" 320 eventdeep.DeepCopy(originRec, &newRecord, eventdeep.WithORMDiffOpt) 321 ... 322 if !reflect.DeepEqual(newRecord, expectRec) { 323 t.Fatalf("bad, got %v | %v", newRecord, newRecord.Birthday.Nanosecond()) 324 } 325 } 326 ``` 327 328 #### Keep The Target Value If Source Is Empty 329 330 Sometimes we would look for a do-not-modify copier, it'll keep the value of target fields while the corresponding source 331 field is empty (zero or nil). Use `eventdeep.WithOmitEmptyOpt` in the case. 332 333 ```go 334 func TestExample4(t *testing.T) { 335 timeZone, _ := time.LoadLocation("America/Phoenix") 336 tm := time.Date(1999, 3, 13, 5, 57, 11, 1901, timeZone) 337 var originRec = eventdeep.User{ 338 Name: "Bob", 339 Birthday: &tm, 340 Age: 24, 341 EmployeID: 7, 342 Avatar: "https://tse4-mm.cn.bing.net/th/id/OIP-C.SAy__OKoxrIqrXWAb7Tj1wHaEC?pid=ImgDet&rs=1", 343 Image: []byte{95, 27, 43, 66, 0, 21, 210}, 344 Attr: &eventdeep.Attr{Attrs: []string{"hello", "world"}}, 345 Valid: true, 346 } 347 var dstRecord eventdeep.User 348 var t0 = time.Unix(0, 0) 349 var emptyRecord = eventdeep.User{Name: "Barbara", Birthday: &t0} 350 var expectRecord = eventdeep.User{Name: "Barbara", Birthday: &t0, 351 Image: []byte{95, 27, 43, 66, 0, 21, 210}, 352 Attr: &eventdeep.Attr{Attrs: []string{"hello", "world"}}, 353 Valid: true, 354 } 355 356 // prepare a hard copy at first 357 eventdeep.DeepCopy(originRec, &dstRecord) 358 t.Logf("dstRecord: %v", dstRecord) 359 360 // now update dstRecord with the non-empty fields. 361 eventdeep.DeepCopy(emptyRecord, &dstRecord, eventdeep.WithOmitEmptyOpt) 362 t.Logf("dstRecord: %v", dstRecord) 363 if !reflect.DeepEqual(dstRecord, expectRecord) { 364 t.Fatalf("bad, got %v\nexpect: %v", dstRecord, expectRecord) 365 } 366 } 367 ``` 368 369 #### String Marshalling 370 371 While copying struct, map, slice, or other source to target string, the builtin `toStringConverter` will be launched. 372 And the default logic includes marshaling the structural source to string, typically `json.Marshal`. 373 374 This marshaller can be customized: `RegisterStringMarshaller` and `WithStringMarshaller` enable it: 375 376 ```go 377 eventdeep.RegisterStringMarshaller(yaml.Marshal) 378 eventdeep.RegisterStringMarshaller(json.Marshal) 379 ``` 380 381 The default marshaler is a wraper to `json.MarshalIndent`. 382 383 #### Specify CopyMergeStrategy via struct Tag 384 385 Sample struct is (use `copy` as key): 386 387 ```go 388 type AFT struct { 389 flags flags.Flags `copy:",cleareq"` 390 converter *ValueConverter 391 wouldbe int `copy:",must,keepneq,omitzero,mapmerge"` 392 ignored1 int `copy:"-"` 393 ignored2 int `copy:",-"` 394 } 395 ``` 396 397 ##### Name conversions 398 399 `copy` tag has form: `nameConversion[,strategies...]`. `nameConversion` gives a target field Name to define a name 400 conversion strategy, or `-` to ignore the field. 401 402 > `nameConversion` has form: 403 > 404 > - `-`: field is ignored 405 > - `targetName` 406 > - `->targetName` 407 > - `sourceName->targetName` 408 > 409 > Spaces besides of `->` are allowed. 410 411 Copier will check target field tag at first, and following by a source field tag checking. 412 413 You may specify converting rule at either target or source side, Copier assume the target one is prior. 414 415 **NOTE**: `nameConversion` is fully functional only for `cms.ByName` mode. It get partial work in `cms.ByOrdinal` mode ( 416 default mode). 417 418 *TODO*: In `cms.ByOrdinal` (`*`) mode, a name converter can be applied in copying field to field. 419 420 ##### Sample codes 421 422 The test gives a sample to show you how the name-conversion and member function work together: 423 424 ```go 425 func TestStructWithNameConversions(t *testing.T) { 426 type srcS struct { 427 A int `copy:"A1"` 428 B bool `copy:"B1,std"` 429 C string `copy:"C1,"` 430 } 431 432 type dstS struct { 433 A1 int 434 B1 bool 435 C1 string 436 } 437 438 src := &srcS{A: 6, B: true, C: "hello"} 439 var tgt = dstS{A1: 1} 440 441 // use ByName strategy, 442 err := evendeep.New().CopyTo(src, &tgt, evendeep.WithByNameStrategyOpt) 443 444 if tgt.A1 != 6 || !tgt.B1 || tgt.C1 != "hello" || err != nil { 445 t.Fatalf("BAD COPY, tgt: %+v", tgt) 446 } 447 } 448 ``` 449 450 #### Strategy Names 451 452 The available tag names are (Almost newest, see its 453 in [flags/cms/copymergestrategy.go](https://github.com/hedzr/evendeep/blob/master/flags/cms/copymergestrategy.go#L23)): 454 455 | Tag name | Flags | Detail | 456 | ------------------ | ----------------------- | ------------------------------------------------ | 457 | `-` | `cms.Ignore` | field will be ignored | 458 | `std` (*) | `cms.Default` | reserved | 459 | `must` | `cms.Must` | reserved | 460 | `cleareq` | `cms.ClearIfEqual` | set zero if target equal to source | 461 | `keepneq` | `cms.KeepIfNotEq` | don't copy source if target not equal to source | 462 | `clearinvalid` | `cms.ClearIfInvalid` | if target field is invalid, set to zero value | 463 | `noomit` (*) | `cms.NoOmit` | | 464 | `omitempty` | `cms.OmitIfEmpty` | if source field is empty, keep destination value | 465 | `omitnil` | `cms.OmitIfNil` | | 466 | `omitzero` | `cms.OmitIfZero` | | 467 | `noomittarget` (*) | `cms.NoOmitTarget` | | 468 | `omitemptytarget` | `cms.OmitIfTargetEmpty` | if target field is empty, don't copy from source | 469 | `omitniltarget` | `cms.OmitIfTargetNil` | | 470 | `omitzerotarget` | `cms.OmitIfTargetZero` | | 471 | `slicecopy` | `cms.SliceCopy` | copy elem by subscription | 472 | `slicecopyappend` | `cms.SliceCopyAppend` | and append more | 473 | `slicemerge` | `cms.SliceMerge` | merge with order-insensitive | 474 | `mapcopy` | `cms.MapCopy` | copy elem by key | 475 | `mapmerge` | `cms.MapMerge` | merge map deeply | 476 | ... | | | 477 478 > `*`: the flag is on by default. 479 480 #### Notes About `DeepCopy()` 481 482 Many settings are accumulated in multiple calling on `DeepCopy()`, such as `converters`, `ignoreNames`, and so on. The 483 underlying object is `DefaultCopyController`. 484 485 To get a fresh clean copier, `New()` or `NewFlatDeepCopier()` are the choices. BTW, 486 sometimes `evendeep.ResetDefaultCopyController()` might be helpful. 487 488 The only exception is copy-n-merge strategies. There flags are saved and restored on each calling on `DeepCopy()`. 489 490 #### Notes About Global Settings 491 492 Some settings are global and available to both of `DeepCopy()` and `New().CopyTo()`, such as: 493 494 1. `WithStringMarshaller` or `RegisterDefaultStringMarshaller()` 495 2. `RegisterDefaultConverters` 496 3. `RegisterDefaultCopiers` 497 498 And so on. 499 500 ### deepdiff 501 502 `DeepDiff` can deeply print the differences about two objects. 503 504 ```go 505 delta, equal := evendeep.DeepDiff([]int{3, 0, 9}, []int{9, 3, 0}, diff.WithSliceOrderedComparison(true)) 506 t.Logf("delta: %v", delta) // "" 507 508 delta, equal := evendeep.DeepDiff([]int{3, 0}, []int{9, 3, 0}, diff.WithSliceOrderedComparison(true)) 509 t.Logf("delta: %v", delta) // "added: [0] = 9\n" 510 511 delta, equal := evendeep.DeepDiff([]int{3, 0}, []int{9, 3, 0}) 512 t.Logf("delta: %v", delta) 513 // Outputs: 514 // added: [2] = <zero> 515 // modified: [0] = 9 (int) (Old: 3) 516 // modified: [1] = 3 (int) (Old: <zero>) 517 518 ``` 519 520 `DeepDiff` is a rewrote version 521 upon [d4l3k/messagediff]([d4l3k/messagediff at v1.2.1 (github.com)](https://github.com/d4l3k/messagediff)). This new 522 code enables user-defined comparer for you. 523 524 #### Ignored Names 525 526 [`diff.WithIgnoredFields(names...)`](https://github.com/hedzr/evendeep/blob/master/diff/diff.go#L41) can give a list of 527 names which should be ignored when comparing. 528 529 #### Slice-Order Insensitive 530 531 In normal mode, `diff` is slice-order-sensitive, that means, `[1, 2] != [2, 1]` 532 . [`WithSliceOrderedComparison(b bool)`](https://github.com/hedzr/evendeep/blob/master/diff/diff.go#L41) can unmind the 533 differences of order and as an equal. 534 535 #### Customizing Comparer 536 537 For example, `evendeep` ships a `timeComparer`: 538 539 ```go 540 type timeComparer struct{} 541 542 func (c *timeComparer) Match(typ reflect.Type) bool { 543 return typ.String() == "time.Time" 544 } 545 546 func (c *timeComparer) Equal(ctx Context, lhs, rhs reflect.Value, path Path) (equal bool) { 547 aTime := lhs.Interface().(time.Time) 548 bTime := rhs.Interface().(time.Time) 549 if equal = aTime.Equal(bTime); !equal { 550 ctx.PutModified(ctx.PutPath(path), Update{Old: aTime.String(), New: bTime.String(), Typ: typfmtlite(&lhs)}) 551 } 552 return 553 } 554 ``` 555 556 And it has been initialized into diff info struct. `timeComparer` provides a semantic comparing for `time.Time` objects. 557 558 To enable your comparer, 559 use [`diff.WithComparer(comparer)`](https://github.com/hedzr/evendeep/blob/master/diff/diff.go#L65). 560 561 ### deepequal 562 563 Our `DeepEqual` is shortcut to `DeepDiff`: 564 565 ```go 566 equal := evendeep.DeepEqual([]int{3, 0, 9}, []int{9, 3, 0}, diff.WithSliceOrderedComparison(true)) 567 if !equal { 568 t.Errorf("expecting equal = true but got false") 569 } 570 ``` 571 572 For the unhandled types and objects, DeepEqual and DeepDiff will fallback to `reflect.DeepEqual()`. It's no need to 573 call `reflect.DeepEqual` explicitly. 574 575 ## Roadmap 576 577 These features had been planning but still on ice. 578 579 - [x] Name converting and mapping for `cms.ByOrdinal` (`*`) mode: a universal `name converter` can be applied in copying 580 field to field. 581 - [ ] *Use SourceExtractor and TargetSetter together (might be impossible)* 582 - [ ] More builtin converters (*might not be a requisite*) 583 - [x] Handle circular pointer (DONE) 584 585 Issue me if you wanna put it or them on the table. 586 587 ## LICENSE 588 589 Under Apache 2.0.