feat: replace mapping generation with checked copy utilities
This commit is contained in:
117
copyx/benchmark_test.go
Normal file
117
copyx/benchmark_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package copyx_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/copyx"
|
||||
)
|
||||
|
||||
func BenchmarkScenarios(b *testing.B) {
|
||||
type request struct {
|
||||
Name string
|
||||
Count int64
|
||||
Active bool
|
||||
}
|
||||
type result struct {
|
||||
Label string
|
||||
Count int64
|
||||
Active bool
|
||||
}
|
||||
source := request{"sample", 9007199254740993, true}
|
||||
option := copyx.WithFieldNames[request, result](map[string]string{"Name": "Label"})
|
||||
copier, err := copyx.New(option)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for _, reuse := range []bool{false, true} {
|
||||
b.Run(fmt.Sprintf("struct/reuse%t", reuse), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var target result
|
||||
var err error
|
||||
if reuse {
|
||||
err = copier.Copy(&target, source)
|
||||
} else {
|
||||
err = copyx.Copy(&target, source, option)
|
||||
}
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
deep, _ := copyx.New(copyx.WithDeepCopy())
|
||||
b.Run("bytes64KiB", func(b *testing.B) {
|
||||
src := make([]byte, 64<<10)
|
||||
b.SetBytes(int64(len(src)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var dst []byte
|
||||
if err := deep.Copy(&dst, src); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
src := map[string][]int{}
|
||||
for i := 0; i < 100; i++ {
|
||||
src[fmt.Sprint(i)] = []int{1, 2, 3}
|
||||
}
|
||||
b.Run("map100", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var dst map[string][]int
|
||||
if err := deep.Copy(&dst, src); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
cycle := &detail{Values: []int{1, 2, 3}}
|
||||
cycle.Next = cycle
|
||||
b.Run("cycle", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var dst *detail
|
||||
if err := deep.Copy(&dst, cycle); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
b.Run("parallelCycle", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
var dst *detail
|
||||
if err := deep.Copy(&dst, cycle); err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
for _, fresh := range []bool{false, true} {
|
||||
b.Run(fmt.Sprintf("converter/fresh%t", fresh), func(b *testing.B) {
|
||||
c, err := copyx.New(copyx.WithDeepCopy(), copyx.WithConverter(func(v []int) ([]int, error) {
|
||||
if fresh {
|
||||
return append([]int(nil), v...), nil
|
||||
}
|
||||
return v, nil
|
||||
}))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
src := []int{1, 2, 3}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var dst []int
|
||||
err := c.Copy(&dst, src)
|
||||
if fresh && err != nil || !fresh && !errors.Is(err, copyx.ErrSharedReference) {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
537
copyx/copy.go
Normal file
537
copyx/copy.go
Normal file
@@ -0,0 +1,537 @@
|
||||
package copyx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrType = errors.New("copyx: incompatible types")
|
||||
ErrNil = errors.New("copyx: nil cannot fill a non-nullable target")
|
||||
ErrField = errors.New("copyx: unmatched field")
|
||||
ErrUnsupported = errors.New("copyx: unsupported value; provide an explicit data converter")
|
||||
ErrSharedReference = errors.New("copyx: deep copy retains a mutable source reference")
|
||||
ErrLimit = errors.New("copyx: traversal limit exceeded")
|
||||
)
|
||||
|
||||
// Error describes a field/index path without recording the source value.
|
||||
type Error struct {
|
||||
Path string
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string { return e.Path + ": " + e.Cause.Error() }
|
||||
func (e *Error) Unwrap() error { return e.Cause }
|
||||
func at(path string, err error) error { return &Error{Path: path, Cause: err} }
|
||||
|
||||
func Copy(target, source any, options ...Option) error {
|
||||
c, err := New(options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Copy(target, source)
|
||||
}
|
||||
|
||||
// Clone always makes a deep copy. Nil and empty collections stay distinct.
|
||||
func Clone[T any](source T, options ...Option) (T, error) {
|
||||
var target T
|
||||
options = append(append([]Option(nil), options...), WithDeepCopy())
|
||||
err := Copy(&target, source, options...)
|
||||
return target, err
|
||||
}
|
||||
|
||||
// Copy stages the complete result and only assigns target on success. User
|
||||
// converter side effects (including mutation of source) cannot be rolled back.
|
||||
// Caller must synchronize concurrent access to source and target themselves.
|
||||
func (c *Copier) Copy(target, source any) error {
|
||||
// Alias indexes contain addresses only. Keep the original graph alive until
|
||||
// validation and commit finish so the GC cannot recycle any indexed address.
|
||||
defer func() { runtime.KeepAlive(source) }()
|
||||
if c == nil {
|
||||
return errors.New("copyx: nil Copier")
|
||||
}
|
||||
dst := reflect.ValueOf(target)
|
||||
if !dst.IsValid() || dst.Kind() != reflect.Pointer || dst.IsNil() {
|
||||
return errors.New("copyx: target must be a non-nil pointer")
|
||||
}
|
||||
src := reflect.ValueOf(source)
|
||||
s := state{options: &c.options, seen: map[visit]reflect.Value{}}
|
||||
if c.options.deep {
|
||||
var err error
|
||||
s.graph, err = sourceGraph(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if src.IsValid() && src.Kind() == reflect.Pointer && !src.IsNil() && dst.Elem().Kind() == reflect.Struct {
|
||||
id := identity(src)
|
||||
s.valueRoot = &id
|
||||
}
|
||||
}
|
||||
out, err := s.walk(src, dst.Elem(), dst.Elem().Type(), "$", 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.graph != nil {
|
||||
if err = s.graph.audit(out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
dst.Elem().Set(out)
|
||||
return nil
|
||||
}
|
||||
|
||||
type visit struct {
|
||||
reference
|
||||
target reflect.Type
|
||||
}
|
||||
type state struct {
|
||||
options *settings
|
||||
seen map[visit]reflect.Value
|
||||
graph *graph
|
||||
nodes int
|
||||
allocated uint64
|
||||
valueRoot *reference
|
||||
}
|
||||
|
||||
func (s *state) reserve(count int, size uintptr, path string) error {
|
||||
if count < 0 || (count > 0 && uint64(size) > maxCopyBytes/uint64(count)) {
|
||||
return at(path, ErrLimit)
|
||||
}
|
||||
s.allocated += uint64(count) * uint64(size)
|
||||
if s.allocated > maxCopyBytes {
|
||||
return at(path, ErrLimit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nullable(k reflect.Kind) bool {
|
||||
switch k {
|
||||
case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Interface, reflect.Func, reflect.Chan:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
func baseValue(base reflect.Value, t reflect.Type) reflect.Value {
|
||||
if base.IsValid() && base.Type() == t {
|
||||
return base
|
||||
}
|
||||
return reflect.Zero(t)
|
||||
}
|
||||
func (s *state) walk(src, base reflect.Value, t reflect.Type, path string, depth int) (reflect.Value, error) {
|
||||
s.nodes++
|
||||
if depth > maxDepth || s.nodes > maxNodes {
|
||||
return reflect.Value{}, at(path, ErrLimit)
|
||||
}
|
||||
base = baseValue(base, t)
|
||||
if !shallowSafe(t) {
|
||||
return reflect.Value{}, at(path, ErrUnsupported)
|
||||
}
|
||||
if src.IsValid() && !src.CanInterface() {
|
||||
return reflect.Value{}, at(path, ErrUnsupported)
|
||||
}
|
||||
// Exact converters also receive typed nils and zeros. No method calls,
|
||||
// Scanner/Valuer invocation or implicit numeric conversion is attempted.
|
||||
if src.IsValid() {
|
||||
if convert, ok := s.options.converters[pair{src.Type(), t}]; ok {
|
||||
out, err := convert(src)
|
||||
if err != nil {
|
||||
return reflect.Value{}, at(path, err)
|
||||
}
|
||||
if s.graph != nil {
|
||||
if err = s.graph.audit(out); err != nil {
|
||||
return reflect.Value{}, at(path, err)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
if !src.IsValid() || (nullable(src.Kind()) && src.IsNil()) {
|
||||
if src.IsValid() && !s.nilCompatible(src.Type(), t, map[pair]bool{}) {
|
||||
return reflect.Value{}, at(path, ErrType)
|
||||
}
|
||||
if s.options.nilPolicy == NilSkip {
|
||||
return base, nil
|
||||
}
|
||||
if nullable(t.Kind()) || s.options.nilPolicy == NilZero {
|
||||
return reflect.Zero(t), nil
|
||||
}
|
||||
return reflect.Value{}, at(path, ErrNil)
|
||||
}
|
||||
if s.options.skipZero && src.IsZero() {
|
||||
if !s.nilCompatible(src.Type(), t, map[pair]bool{}) {
|
||||
return reflect.Value{}, at(path, ErrType)
|
||||
}
|
||||
return base, nil
|
||||
}
|
||||
if src.Type() == t && len(s.options.converters) == 0 && len(s.options.fields) == 0 && !s.options.skipZero && s.options.nilPolicy != NilSkip {
|
||||
if !s.options.deep || simpleValue(t) {
|
||||
return src, nil
|
||||
}
|
||||
}
|
||||
if src.Kind() == reflect.Interface {
|
||||
return s.walk(src.Elem(), base, t, path, depth+1)
|
||||
}
|
||||
if t.Kind() == reflect.Interface {
|
||||
if !src.Type().Implements(t) {
|
||||
return reflect.Value{}, at(path, ErrType)
|
||||
}
|
||||
out, err := s.walk(src, reflect.Value{}, src.Type(), path, depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
v := reflect.New(t).Elem()
|
||||
v.Set(out)
|
||||
return v, nil
|
||||
}
|
||||
if s.options.deep && forbidden(t) {
|
||||
return reflect.Value{}, at(path, ErrUnsupported)
|
||||
}
|
||||
if src.Kind() == reflect.Pointer && t.Kind() != reflect.Pointer {
|
||||
return s.walk(src.Elem(), base, t, path, depth+1)
|
||||
}
|
||||
if t.Kind() == reflect.Pointer {
|
||||
if !s.options.deep && src.Type() == t {
|
||||
return src, nil
|
||||
}
|
||||
if src.Kind() == reflect.Pointer {
|
||||
key := visit{identity(src), t}
|
||||
if s.valueRoot != nil && key.reference == *s.valueRoot {
|
||||
return reflect.Value{}, at(path, fmt.Errorf("%w: cyclic root must retain its pointer; use Clone(source) or Copy(&targetPointer, source)", ErrUnsupported))
|
||||
}
|
||||
if prior, ok := s.seen[key]; ok {
|
||||
return prior, nil
|
||||
}
|
||||
}
|
||||
if err := s.reserve(1, t.Elem().Size(), path); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out := reflect.New(t.Elem())
|
||||
if out.Type() != t {
|
||||
out = out.Convert(t)
|
||||
}
|
||||
if src.Kind() == reflect.Pointer {
|
||||
key := visit{identity(src), t}
|
||||
s.seen[key] = out
|
||||
src = src.Elem()
|
||||
}
|
||||
var childBase reflect.Value
|
||||
if !base.IsNil() {
|
||||
childBase = base.Elem()
|
||||
}
|
||||
value, err := s.walk(src, childBase, t.Elem(), path, depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out.Elem().Set(value)
|
||||
return out, nil
|
||||
}
|
||||
if src.Kind() != t.Kind() {
|
||||
return reflect.Value{}, at(path, fmt.Errorf("%w: %s -> %s", ErrType, src.Type(), t))
|
||||
}
|
||||
if t == timeType && src.Type() == t {
|
||||
return src, nil
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
return s.structure(src, base, t, path, depth)
|
||||
case reflect.Slice:
|
||||
if !s.options.deep && src.Type() == t {
|
||||
return src, nil
|
||||
}
|
||||
key := visit{identity(src), t}
|
||||
if prior, ok := s.seen[key]; ok {
|
||||
return prior, nil
|
||||
}
|
||||
if err := s.reserve(src.Len(), t.Elem().Size(), path); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out := reflect.MakeSlice(t, src.Len(), src.Len())
|
||||
s.seen[key] = out
|
||||
if src.Type() == t && len(s.options.converters) == 0 && len(s.options.fields) == 0 && simpleValue(t.Elem()) {
|
||||
reflect.Copy(out, src)
|
||||
return out, nil
|
||||
}
|
||||
for i := 0; i < src.Len(); i++ {
|
||||
v, err := s.walk(src.Index(i), reflect.Value{}, t.Elem(), fmt.Sprintf("%s[%d]", path, i), depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out.Index(i).Set(v)
|
||||
}
|
||||
return out, nil
|
||||
case reflect.Array:
|
||||
if src.Len() != t.Len() {
|
||||
return reflect.Value{}, at(path, ErrType)
|
||||
}
|
||||
if err := s.reserve(1, t.Size(), path); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out := reflect.New(t).Elem()
|
||||
for i := 0; i < src.Len(); i++ {
|
||||
v, err := s.walk(src.Index(i), base.Index(i), t.Elem(), fmt.Sprintf("%s[%d]", path, i), depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out.Index(i).Set(v)
|
||||
}
|
||||
return out, nil
|
||||
case reflect.Map:
|
||||
if !s.options.deep && src.Type() == t {
|
||||
return src, nil
|
||||
}
|
||||
key := visit{identity(src), t}
|
||||
if prior, ok := s.seen[key]; ok {
|
||||
return prior, nil
|
||||
}
|
||||
if err := s.reserve(src.Len(), t.Key().Size()+t.Elem().Size()+32, path); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out := reflect.MakeMapWithSize(t, src.Len())
|
||||
s.seen[key] = out
|
||||
it := src.MapRange()
|
||||
for it.Next() {
|
||||
k, err := s.walk(it.Key(), reflect.Value{}, t.Key(), path+"[key]", depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
if !k.Comparable() {
|
||||
return reflect.Value{}, at(path+"[key]", ErrType)
|
||||
}
|
||||
if out.MapIndex(k).IsValid() {
|
||||
return reflect.Value{}, at(path+"[key]", errors.New("copyx: key conversion collision"))
|
||||
}
|
||||
v, err := s.walk(it.Value(), reflect.Value{}, t.Elem(), path+"[value]", depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out.SetMapIndex(k, v)
|
||||
}
|
||||
return out, nil
|
||||
case reflect.Func, reflect.Chan, reflect.UnsafePointer:
|
||||
if s.options.deep {
|
||||
return reflect.Value{}, at(path, ErrUnsupported)
|
||||
}
|
||||
}
|
||||
if src.Type() != t {
|
||||
return reflect.Value{}, at(path, fmt.Errorf("%w: %s -> %s", ErrType, src.Type(), t))
|
||||
}
|
||||
return src, nil
|
||||
}
|
||||
|
||||
var fieldTypes sync.Map
|
||||
|
||||
func exportedFields(t reflect.Type) map[string]reflect.StructField {
|
||||
if known, ok := fieldTypes.Load(t); ok {
|
||||
return known.(map[string]reflect.StructField)
|
||||
}
|
||||
out := map[string]reflect.StructField{}
|
||||
for _, f := range reflect.VisibleFields(t) {
|
||||
if f.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
kind := f.Type.Kind()
|
||||
if kind == reflect.Pointer {
|
||||
kind = f.Type.Elem().Kind()
|
||||
}
|
||||
if f.Anonymous && kind == reflect.Struct {
|
||||
continue
|
||||
}
|
||||
out[f.Name] = f
|
||||
}
|
||||
actual, _ := fieldTypes.LoadOrStore(t, out)
|
||||
return actual.(map[string]reflect.StructField)
|
||||
}
|
||||
|
||||
func (s *state) nilCompatible(from, to reflect.Type, seen map[pair]bool) bool {
|
||||
key := pair{from, to}
|
||||
if seen[key] {
|
||||
return true
|
||||
}
|
||||
seen[key] = true
|
||||
if from == to {
|
||||
return true
|
||||
}
|
||||
if _, ok := s.options.converters[key]; ok {
|
||||
return true
|
||||
}
|
||||
if to.Kind() == reflect.Interface {
|
||||
return from.Implements(to)
|
||||
}
|
||||
if from.Kind() == reflect.Pointer {
|
||||
return s.nilCompatible(from.Elem(), to, seen)
|
||||
}
|
||||
if to.Kind() == reflect.Pointer {
|
||||
return s.nilCompatible(from, to.Elem(), seen)
|
||||
}
|
||||
if from.Kind() != to.Kind() {
|
||||
return false
|
||||
}
|
||||
switch from.Kind() {
|
||||
case reflect.Struct:
|
||||
// Nil must not hide a structurally incompatible type pair. There are no
|
||||
// field values to visit, but coverage and conversion types still apply.
|
||||
for i := 0; i < to.NumField(); i++ {
|
||||
if to.Field(i).PkgPath != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
sources, targets := exportedFields(from), exportedFields(to)
|
||||
rules := s.options.fields[key]
|
||||
if rules == nil {
|
||||
rules = &fields{}
|
||||
}
|
||||
reverse, used := map[string]string{}, map[string]bool{}
|
||||
for a, b := range rules.names {
|
||||
reverse[b] = a
|
||||
}
|
||||
for name, target := range targets {
|
||||
if rules.ignoreTarget[name] {
|
||||
continue
|
||||
}
|
||||
srcName := name
|
||||
if renamed, ok := reverse[name]; ok {
|
||||
srcName = renamed
|
||||
}
|
||||
source, ok := sources[srcName]
|
||||
if !ok || rules.ignoreSource[srcName] || !s.nilCompatible(source.Type, target.Type, seen) {
|
||||
return false
|
||||
}
|
||||
used[srcName] = true
|
||||
}
|
||||
if s.options.strict {
|
||||
for name := range sources {
|
||||
if !used[name] && !rules.ignoreSource[name] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Slice:
|
||||
return s.nilCompatible(from.Elem(), to.Elem(), seen)
|
||||
case reflect.Map:
|
||||
return s.nilCompatible(from.Key(), to.Key(), seen) && s.nilCompatible(from.Elem(), to.Elem(), seen)
|
||||
case reflect.Array:
|
||||
return from.Len() == to.Len() && s.nilCompatible(from.Elem(), to.Elem(), seen)
|
||||
}
|
||||
return false
|
||||
}
|
||||
func fieldValue(v reflect.Value, index []int) (reflect.Value, bool) {
|
||||
for _, i := range index {
|
||||
if v.Kind() == reflect.Pointer {
|
||||
if v.IsNil() {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
v = v.Field(i)
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
func setField(v reflect.Value, index []int, value reflect.Value) error {
|
||||
for j, i := range index {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
return ErrUnsupported
|
||||
}
|
||||
if j == len(index)-1 {
|
||||
field.Set(value)
|
||||
return nil
|
||||
}
|
||||
if field.Kind() == reflect.Pointer {
|
||||
// Detach embedded destination pointers before editing promoted fields.
|
||||
fresh := reflect.New(field.Type().Elem())
|
||||
if !field.IsNil() {
|
||||
fresh.Elem().Set(field.Elem())
|
||||
}
|
||||
field.Set(fresh)
|
||||
v = fresh.Elem()
|
||||
} else {
|
||||
v = field
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *state) structure(src, base reflect.Value, t reflect.Type, path string, depth int) (reflect.Value, error) {
|
||||
if err := s.reserve(1, t.Size(), path); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out := reflect.New(t).Elem()
|
||||
out.Set(base)
|
||||
if src.Type() == t {
|
||||
// Private scalar state may be assigned as part of its whole value; mutable
|
||||
// private state requires a converter, never unsafe writes or silent loss.
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
if f.PkgPath != "" && s.options.deep && privateMutable(f.Type) {
|
||||
return reflect.Value{}, at(path+"."+f.Name, ErrUnsupported)
|
||||
}
|
||||
}
|
||||
out.Set(src)
|
||||
} else {
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if t.Field(i).PkgPath != "" {
|
||||
return reflect.Value{}, at(path, ErrUnsupported)
|
||||
}
|
||||
}
|
||||
}
|
||||
from, to := exportedFields(src.Type()), exportedFields(t)
|
||||
rules := s.options.fields[pair{src.Type(), t}]
|
||||
if rules == nil {
|
||||
rules = &fields{map[string]string{}, map[string]bool{}, map[string]bool{}}
|
||||
}
|
||||
reverse := map[string]string{}
|
||||
for a, b := range rules.names {
|
||||
reverse[b] = a
|
||||
}
|
||||
names := make([]string, 0, len(to))
|
||||
for n := range to {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
used := map[string]bool{}
|
||||
for _, name := range names {
|
||||
target := to[name]
|
||||
old, _ := fieldValue(base, target.Index)
|
||||
if rules.ignoreTarget[name] {
|
||||
if !old.IsValid() {
|
||||
old = reflect.Zero(target.Type)
|
||||
}
|
||||
if err := setField(out, target.Index, old); err != nil {
|
||||
return reflect.Value{}, at(path+"."+name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
srcName := name
|
||||
if renamed, ok := reverse[name]; ok {
|
||||
srcName = renamed
|
||||
}
|
||||
source, ok := from[srcName]
|
||||
if !ok || rules.ignoreSource[srcName] {
|
||||
return reflect.Value{}, at(path+"."+name, ErrField)
|
||||
}
|
||||
used[srcName] = true
|
||||
v, ok := fieldValue(src, source.Index)
|
||||
if !ok {
|
||||
return reflect.Value{}, at(path+"."+name, ErrNil)
|
||||
}
|
||||
value, err := s.walk(v, old, target.Type, path+"."+name, depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
if err = setField(out, target.Index, value); err != nil {
|
||||
return reflect.Value{}, at(path+"."+name, err)
|
||||
}
|
||||
}
|
||||
if s.options.strict {
|
||||
for name := range from {
|
||||
if !used[name] && !rules.ignoreSource[name] {
|
||||
return reflect.Value{}, at(path+"."+name, ErrField)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
448
copyx/copy_test.go
Normal file
448
copyx/copy_test.go
Normal file
@@ -0,0 +1,448 @@
|
||||
package copyx_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/copyx"
|
||||
)
|
||||
|
||||
type detail struct {
|
||||
Values []int
|
||||
Index map[string][]int
|
||||
Next *detail
|
||||
}
|
||||
|
||||
func TestDeepGraph(t *testing.T) {
|
||||
src := &detail{Values: []int{1, 2}, Index: map[string][]int{"a": {3}}}
|
||||
src.Next = src
|
||||
dst, err := copyx.Clone(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if dst == src || dst.Next != dst {
|
||||
t.Fatal("pointer cycle not preserved")
|
||||
}
|
||||
dst.Values[0] = 9
|
||||
dst.Index["a"][0] = 8
|
||||
if src.Values[0] != 1 || src.Index["a"][0] != 3 {
|
||||
t.Fatal("deep alias")
|
||||
}
|
||||
m := map[string]any{}
|
||||
m["self"] = m
|
||||
n, err := copyx.Clone(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n["self"].(map[string]any)["new"] = 1
|
||||
if n["new"] != 1 || m["new"] != nil {
|
||||
t.Fatal("map cycle changed")
|
||||
}
|
||||
x := []any{nil}
|
||||
x[0] = x
|
||||
y, err := copyx.Clone(x)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
y[0].([]any)[0] = "changed"
|
||||
if y[0] != "changed" {
|
||||
t.Fatal("slice cycle changed")
|
||||
}
|
||||
if _, ok := x[0].([]any); !ok {
|
||||
t.Fatal("source slice changed")
|
||||
}
|
||||
}
|
||||
func TestShallowAndNil(t *testing.T) {
|
||||
src := detail{Values: []int{1}}
|
||||
var dst detail
|
||||
if err := copyx.Copy(&dst, src); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dst.Values[0] = 2
|
||||
if src.Values[0] != 2 {
|
||||
t.Fatal("shallow unexpectedly detached")
|
||||
}
|
||||
for _, slice := range [][]int{nil, {}} {
|
||||
out, err := copyx.Clone(slice)
|
||||
if err != nil || (out == nil) != (slice == nil) {
|
||||
t.Fatal(out, err)
|
||||
}
|
||||
}
|
||||
for _, m := range []map[string]int{nil, {}} {
|
||||
out, err := copyx.Clone(m)
|
||||
if err != nil || (out == nil) != (m == nil) {
|
||||
t.Fatal(out, err)
|
||||
}
|
||||
}
|
||||
var p *int
|
||||
n := 7
|
||||
if err := copyx.Copy(&n, p); !errors.Is(err, copyx.ErrNil) || n != 7 {
|
||||
t.Fatal(n, err)
|
||||
}
|
||||
if err := copyx.Copy(&n, p, copyx.WithNilPolicy(copyx.NilSkip)); err != nil || n != 7 {
|
||||
t.Fatal(n, err)
|
||||
}
|
||||
if err := copyx.Copy(&n, p, copyx.WithNilPolicy(copyx.NilZero)); err != nil || n != 0 {
|
||||
t.Fatal(n, err)
|
||||
}
|
||||
x := 3
|
||||
out := &x
|
||||
if err := copyx.Copy(&out, p); err != nil || out != nil {
|
||||
t.Fatal(out, err)
|
||||
}
|
||||
flag := true
|
||||
if err := copyx.Copy(&flag, false); err != nil || flag {
|
||||
t.Fatal(flag, err)
|
||||
}
|
||||
flag = true
|
||||
if err := copyx.Copy(&flag, false, copyx.WithSkipZero()); err != nil || !flag {
|
||||
t.Fatal(flag, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConverterAliasesRejected(t *testing.T) {
|
||||
for _, kind := range []string{"same", "nested", "subslice", "capacity", "fieldPointer", "private", "map", "sibling"} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
src := &detail{Values: []int{1, 2, 3}, Index: map[string][]int{"x": {1}}}
|
||||
var err error
|
||||
switch kind {
|
||||
case "same":
|
||||
_, err = copyx.Clone(src, copyx.WithConverter(func(v *detail) (*detail, error) { return v, nil }))
|
||||
case "nested":
|
||||
_, err = copyx.Clone(src, copyx.WithConverter(func(v *detail) (*detail, error) { return &detail{Values: v.Values}, nil }))
|
||||
case "subslice":
|
||||
_, err = copyx.Clone(src.Values, copyx.WithConverter(func(v []int) ([]int, error) { return v[1:], nil }))
|
||||
case "capacity":
|
||||
_, err = copyx.Clone(src.Values, copyx.WithConverter(func(v []int) ([]int, error) { return v[:0], nil }))
|
||||
case "map":
|
||||
_, err = copyx.Clone(src.Index, copyx.WithConverter(func(v map[string][]int) (map[string][]int, error) { return v, nil }))
|
||||
case "fieldPointer":
|
||||
var dst *int
|
||||
err = copyx.Copy(&dst, src.Values, copyx.WithDeepCopy(), copyx.WithConverter(func(v []int) (*int, error) { return &v[1], nil }))
|
||||
case "private":
|
||||
type opaque struct{ data []int }
|
||||
v := opaque{src.Values}
|
||||
_, err = copyx.Clone(v, copyx.WithConverter(func(v opaque) (opaque, error) { return v, nil }))
|
||||
case "sibling":
|
||||
type object struct{ A, B []int }
|
||||
v := object{[]int{1}, []int{2}}
|
||||
_, err = copyx.Clone(v, copyx.WithConverter(func(_ []int) ([]int, error) { return v.B, nil }))
|
||||
}
|
||||
if !errors.Is(err, copyx.ErrSharedReference) {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
_, err := copyx.Clone(4, copyx.WithConverter(func(v int) (int, error) { return v, nil }))
|
||||
if err != nil {
|
||||
t.Fatal("equal primitive rejected", err)
|
||||
}
|
||||
_, err = copyx.Clone("same", copyx.WithConverter(func(v string) (string, error) { return v, nil }))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
good, err := copyx.Clone([]int{1}, copyx.WithConverter(func(v []int) ([]int, error) { return append([]int(nil), v...), nil }))
|
||||
if err != nil || good[0] != 1 {
|
||||
t.Fatal(good, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateAndUnsupported(t *testing.T) {
|
||||
type money struct{ cents *int64 }
|
||||
n := int64(9007199254740991)
|
||||
src := money{&n}
|
||||
if _, err := copyx.Clone(src); !errors.Is(err, copyx.ErrUnsupported) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := copyx.Clone(src, copyx.WithConverter(func(v money) (money, error) { n := *v.cents; return money{&n}, nil }))
|
||||
if err != nil || *out.cents != n || out.cents == src.cents {
|
||||
t.Fatal(out, err)
|
||||
}
|
||||
tm := time.Now()
|
||||
cloned, err := copyx.Clone(tm)
|
||||
if err != nil || !cloned.Equal(tm) {
|
||||
t.Fatal(cloned, err)
|
||||
}
|
||||
for _, v := range []any{make(chan int), func() {}, &sync.Mutex{}, sync.Mutex{}} {
|
||||
if _, err := copyx.Clone(v); !errors.Is(err, copyx.ErrUnsupported) {
|
||||
t.Fatalf("%T: %v", v, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilTypeMismatchAndInterfaceConverter(t *testing.T) {
|
||||
var source *string
|
||||
target := 7
|
||||
if err := copyx.Copy(&target, source, copyx.WithNilPolicy(copyx.NilZero)); !errors.Is(err, copyx.ErrType) || target != 7 {
|
||||
t.Fatal(target, err)
|
||||
}
|
||||
var list []int
|
||||
var targetMap map[string]int
|
||||
if err := copyx.Copy(&targetMap, list); !errors.Is(err, copyx.ErrType) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
type input struct{ Value any }
|
||||
type output struct{ Value string }
|
||||
var out output
|
||||
err := copyx.Copy(&out, input{}, copyx.WithConverter(func(v any) (string, error) {
|
||||
if v == nil {
|
||||
return "nil handled", nil
|
||||
}
|
||||
return "value", nil
|
||||
}))
|
||||
if err != nil || out.Value != "nil handled" {
|
||||
t.Fatal(out, err)
|
||||
}
|
||||
var lock sync.Mutex
|
||||
if err := copyx.Copy(&lock, sync.Mutex{}); !errors.Is(err, copyx.ErrUnsupported) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
type locked struct {
|
||||
mu sync.Mutex
|
||||
Value int
|
||||
}
|
||||
var l locked
|
||||
if err := copyx.Copy(&l, locked{}); !errors.Is(err, copyx.ErrUnsupported) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocationLimitAndPointerRoot(t *testing.T) {
|
||||
src := make([]byte, (64<<20)+1)
|
||||
if _, err := copyx.Clone(src); !errors.Is(err, copyx.ErrLimit) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n := &detail{}
|
||||
n.Next = n
|
||||
var value detail
|
||||
if err := copyx.Copy(&value, n, copyx.WithDeepCopy()); !errors.Is(err, copyx.ErrUnsupported) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var p *detail
|
||||
if err := copyx.Copy(&p, n, copyx.WithDeepCopy()); err != nil || p.Next != p || p == n {
|
||||
t.Fatal(p, err)
|
||||
}
|
||||
}
|
||||
func TestFieldsConvertersAndAtomicity(t *testing.T) {
|
||||
type input struct {
|
||||
Name string
|
||||
Age int
|
||||
Extra string
|
||||
}
|
||||
type output struct {
|
||||
Label string
|
||||
Age int
|
||||
Local string
|
||||
}
|
||||
rename := map[string]string{"Name": "Label"}
|
||||
c, err := copyx.New(copyx.WithFieldNames[input, output](rename), copyx.WithIgnoreTarget[input, output]("Local"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rename["Name"] = "Missing"
|
||||
dst := output{Local: "preserved"}
|
||||
if err = c.Copy(&dst, input{Name: "hello", Age: 2}); err != nil || dst.Label != "hello" || dst.Local != "preserved" {
|
||||
t.Fatal(dst, err)
|
||||
}
|
||||
if err = copyx.Copy(&dst, input{}, copyx.WithStrictFields(), copyx.WithFieldNames[input, output](map[string]string{"Name": "Label"}), copyx.WithIgnoreTarget[input, output]("Local")); !errors.Is(err, copyx.ErrField) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = copyx.Copy(&dst, input{}, copyx.WithStrictFields(), copyx.WithFieldNames[input, output](map[string]string{"Name": "Label"}), copyx.WithIgnoreTarget[input, output]("Local"), copyx.WithIgnoreSource[input, output]("Extra")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = copyx.Copy(&dst, input{}); !errors.Is(err, copyx.ErrField) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = copyx.New(copyx.WithFieldNames[input, output](map[string]string{"Nope": "Age"})); err == nil {
|
||||
t.Fatal("invalid rule accepted")
|
||||
}
|
||||
if _, err = copyx.New(copyx.WithConverter(func(v int) (string, error) { return "", nil }), copyx.WithConverter(func(v int) (string, error) { return "", nil })); err == nil {
|
||||
t.Fatal("duplicate converter accepted")
|
||||
}
|
||||
|
||||
sentinel := errors.New("conversion failed")
|
||||
type node struct {
|
||||
A []int
|
||||
Z string
|
||||
}
|
||||
old := node{[]int{8}, "old"}
|
||||
before := old.A
|
||||
err = copyx.Copy(&old, node{[]int{1}, "bad"}, copyx.WithDeepCopy(), copyx.WithConverter(func(v string) (string, error) { return "", sentinel }))
|
||||
if !errors.Is(err, sentinel) || !strings.Contains(err.Error(), "Z") || old.Z != "old" || old.A[0] != 8 || &old.A[0] != &before[0] {
|
||||
t.Fatal(old, err)
|
||||
}
|
||||
// Exact typed nil converter must get control before the global nil policy.
|
||||
var ptr *int
|
||||
var value int
|
||||
err = copyx.Copy(&value, ptr, copyx.WithConverter(func(v *int) (int, error) {
|
||||
if v == nil {
|
||||
return 0, sentinel
|
||||
}
|
||||
return *v, nil
|
||||
}))
|
||||
if !errors.Is(err, sentinel) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var small int8 = 3
|
||||
if err = copyx.Copy(&small, int64(300)); !errors.Is(err, copyx.ErrType) || small != 3 {
|
||||
t.Fatal(small, err)
|
||||
}
|
||||
}
|
||||
|
||||
type Embedded struct {
|
||||
A int
|
||||
Z string
|
||||
}
|
||||
|
||||
func TestEmbeddedDestinationDetached(t *testing.T) {
|
||||
type target struct{ *Embedded }
|
||||
type source struct {
|
||||
A int
|
||||
Z int
|
||||
}
|
||||
ptr := &Embedded{5, "keep"}
|
||||
dst := target{ptr}
|
||||
if err := copyx.Copy(&dst, source{6, 7}); !errors.Is(err, copyx.ErrType) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ptr.A != 5 || dst.Embedded != ptr {
|
||||
t.Fatal("failure mutated embedded target")
|
||||
}
|
||||
if err := copyx.Copy(&dst, Embedded{6, "new"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ptr.A != 5 || dst.A != 6 {
|
||||
t.Fatal("success mutated old embedded object")
|
||||
}
|
||||
}
|
||||
func TestConcurrentCopier(t *testing.T) {
|
||||
c, err := copyx.New(copyx.WithDeepCopy())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src := &detail{Values: []int{7}, Index: map[string][]int{"a": {9}}}
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 64; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for n := 0; n < 20; n++ {
|
||||
var dst *detail
|
||||
if err := c.Copy(&dst, src); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
dst.Values[0] = n
|
||||
if src.Values[0] != 7 {
|
||||
t.Error("shared")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
func TestBoundAndCollections(t *testing.T) {
|
||||
src := &detail{}
|
||||
p := src
|
||||
for i := 0; i < 300; i++ {
|
||||
p.Next = &detail{}
|
||||
p = p.Next
|
||||
}
|
||||
if _, err := copyx.Clone(src); !errors.Is(err, copyx.ErrLimit) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
type from struct{ Value string }
|
||||
type to struct{ Value string }
|
||||
var out []to
|
||||
if err := copyx.Copy(&out, []from{{"a"}, {"b"}}); err != nil || len(out) != 2 || out[1].Value != "b" {
|
||||
t.Fatal(out, err)
|
||||
}
|
||||
var array [2]to
|
||||
if err := copyx.Copy(&array, [2]from{{"a"}, {"b"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := copyx.Copy(&array, [1]from{{"a"}}); !errors.Is(err, copyx.ErrType) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var outMap map[string]int
|
||||
err := copyx.Copy(&outMap, map[int]int{1: 1, 2: 2}, copyx.WithConverter(func(int) (string, error) { return "same", nil }))
|
||||
if err == nil || outMap != nil {
|
||||
t.Fatal(outMap, err)
|
||||
}
|
||||
}
|
||||
func FuzzClone(f *testing.F) {
|
||||
f.Add([]byte{1, 2, 3})
|
||||
f.Add([]byte{})
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
if len(data) > 1024 {
|
||||
data = data[:1024]
|
||||
}
|
||||
src := map[string]any{"bytes": data, "other": []string{"x"}}
|
||||
out, err := copyx.Clone(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(src, out) {
|
||||
t.Fatal("changed value")
|
||||
}
|
||||
if len(data) > 0 {
|
||||
out["bytes"].([]byte)[0] ^= 255
|
||||
if data[0] == out["bytes"].([]byte)[0] {
|
||||
t.Fatal("alias")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraversalNodeLimit(t *testing.T) {
|
||||
source := make([]any, 200001)
|
||||
if _, err := copyx.Clone(source); !errors.Is(err, copyx.ErrLimit) {
|
||||
t.Fatal("expected bounded traversal", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilCannotHideFieldMismatch(t *testing.T) {
|
||||
type source struct{ Value string }
|
||||
type target struct{ Value int }
|
||||
var src *source
|
||||
var dst *target
|
||||
for _, policy := range []copyx.NilPolicy{copyx.NilError, copyx.NilZero, copyx.NilSkip} {
|
||||
if err := copyx.Copy(&dst, src, copyx.WithNilPolicy(policy)); !errors.Is(err, copyx.ErrType) {
|
||||
t.Fatal("nil concealed incompatible fields", err)
|
||||
}
|
||||
}
|
||||
if err := copyx.Copy(&dst, src, copyx.WithConverter(func(string) (int, error) { return 0, nil })); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text := "keep"
|
||||
if err := copyx.Copy(&text, 0, copyx.WithSkipZero()); !errors.Is(err, copyx.ErrType) || text != "keep" {
|
||||
t.Fatal("zero concealed incompatible types", text, err)
|
||||
}
|
||||
}
|
||||
func BenchmarkCopy(b *testing.B) {
|
||||
for _, n := range []int{1, 100, 1000} {
|
||||
for _, deep := range []bool{false, true} {
|
||||
b.Run(fmt.Sprintf("fields%d/deep%t", n, deep), func(b *testing.B) {
|
||||
src := detail{Values: make([]int, n), Index: map[string][]int{"one": {1, 2}}}
|
||||
var opts []copyx.Option
|
||||
if deep {
|
||||
opts = append(opts, copyx.WithDeepCopy())
|
||||
}
|
||||
c, _ := copyx.New(opts...)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var dst detail
|
||||
if err := c.Copy(&dst, src); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
270
copyx/graph.go
Normal file
270
copyx/graph.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package copyx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const maxDepth = 256
|
||||
const maxNodes = 200000
|
||||
const maxCopyBytes = 64 << 20
|
||||
|
||||
var timeType = reflect.TypeFor[time.Time]()
|
||||
|
||||
// Immutable type metadata is shared, never values or converter registrations.
|
||||
var valueTypes sync.Map
|
||||
|
||||
func simpleValue(t reflect.Type) bool {
|
||||
if known, ok := valueTypes.Load(t); ok {
|
||||
return known.(bool)
|
||||
}
|
||||
result := t == timeType
|
||||
if !result {
|
||||
switch t.Kind() {
|
||||
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
|
||||
reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String:
|
||||
result = true
|
||||
case reflect.Array:
|
||||
result = simpleValue(t.Elem())
|
||||
case reflect.Struct:
|
||||
result = !forbidden(t)
|
||||
for i := 0; result && i < t.NumField(); i++ {
|
||||
result = simpleValue(t.Field(i).Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
valueTypes.Store(t, result)
|
||||
return result
|
||||
}
|
||||
|
||||
type reference struct {
|
||||
kind reflect.Kind
|
||||
typ reflect.Type
|
||||
ptr uintptr
|
||||
length, capacity int
|
||||
}
|
||||
|
||||
func identity(v reflect.Value) reference {
|
||||
r := reference{kind: v.Kind(), typ: v.Type(), ptr: uintptr(v.UnsafePointer())}
|
||||
if v.Kind() == reflect.Slice {
|
||||
r.length = v.Len()
|
||||
r.capacity = v.Cap()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type span struct{ start, end uintptr }
|
||||
type graph struct {
|
||||
maps map[uintptr]bool
|
||||
spans []span
|
||||
maxEnd []uintptr
|
||||
}
|
||||
|
||||
// References are inspected without reading private values through Interface or
|
||||
// using unsafe writes. Slice capacity matters: returning source[:0] is an alias.
|
||||
func scanGraph(v reflect.Value, visit func(reflect.Value, string) error) error {
|
||||
seen := map[reference]bool{}
|
||||
nodes := 0
|
||||
var walk func(reflect.Value, string, int) error
|
||||
walk = func(v reflect.Value, path string, depth int) error {
|
||||
nodes++
|
||||
if depth > maxDepth || nodes > maxNodes {
|
||||
return at(path, ErrLimit)
|
||||
}
|
||||
if !v.IsValid() {
|
||||
return nil
|
||||
}
|
||||
if err := visit(v, path); err != nil {
|
||||
return err
|
||||
}
|
||||
if simpleValue(v.Type()) {
|
||||
return nil
|
||||
} // time.Time is a documented immutable value.
|
||||
switch v.Kind() {
|
||||
case reflect.Interface:
|
||||
if !v.IsNil() {
|
||||
return walk(v.Elem(), path, depth+1)
|
||||
}
|
||||
case reflect.Pointer, reflect.Map, reflect.Slice:
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
id := identity(v)
|
||||
if seen[id] {
|
||||
return nil
|
||||
}
|
||||
seen[id] = true
|
||||
switch v.Kind() {
|
||||
case reflect.Pointer:
|
||||
return walk(v.Elem(), path, depth+1)
|
||||
case reflect.Map:
|
||||
it := v.MapRange()
|
||||
for it.Next() {
|
||||
if err := walk(it.Key(), path+"[key]", depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := walk(it.Value(), path+"[value]", depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
if simpleValue(v.Type().Elem()) {
|
||||
return nil
|
||||
}
|
||||
all := v.Slice(0, v.Cap())
|
||||
for i := 0; i < all.Len(); i++ {
|
||||
if err := walk(all.Index(i), fmt.Sprintf("%s[%d]", path, i), depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if err := walk(v.Field(i), path+"."+v.Type().Field(i).Name, depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case reflect.Array:
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if err := walk(v.Index(i), fmt.Sprintf("%s[%d]", path, i), depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return walk(v, "$", 0)
|
||||
}
|
||||
func mutableSpan(v reflect.Value) (span, bool) {
|
||||
var n uintptr
|
||||
switch v.Kind() {
|
||||
case reflect.Pointer:
|
||||
if v.IsNil() {
|
||||
return span{}, false
|
||||
}
|
||||
n = v.Type().Elem().Size()
|
||||
case reflect.Slice:
|
||||
if v.IsNil() {
|
||||
return span{}, false
|
||||
}
|
||||
n = uintptr(v.Cap()) * v.Type().Elem().Size()
|
||||
default:
|
||||
return span{}, false
|
||||
}
|
||||
if n == 0 {
|
||||
return span{}, false
|
||||
}
|
||||
start := uintptr(v.UnsafePointer())
|
||||
return span{start, start + n}, true
|
||||
}
|
||||
func sourceGraph(v reflect.Value) (*graph, error) {
|
||||
g := &graph{maps: map[uintptr]bool{}}
|
||||
err := scanGraph(v, func(v reflect.Value, _ string) error {
|
||||
if r, ok := mutableSpan(v); ok {
|
||||
g.spans = append(g.spans, r)
|
||||
}
|
||||
if v.Kind() == reflect.Map && !v.IsNil() {
|
||||
g.maps[uintptr(v.UnsafePointer())] = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Slice(g.spans, func(i, j int) bool { return g.spans[i].start < g.spans[j].start })
|
||||
var end uintptr
|
||||
for _, r := range g.spans {
|
||||
if r.end > end {
|
||||
end = r.end
|
||||
}
|
||||
g.maxEnd = append(g.maxEnd, end)
|
||||
}
|
||||
return g, err
|
||||
}
|
||||
func (g *graph) audit(v reflect.Value) error {
|
||||
return scanGraph(v, func(v reflect.Value, path string) error {
|
||||
if forbidden(v.Type()) {
|
||||
return at(path, ErrUnsupported)
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Func, reflect.Chan:
|
||||
if !v.IsNil() {
|
||||
return at(path, ErrUnsupported)
|
||||
}
|
||||
case reflect.UnsafePointer:
|
||||
if v.Pointer() != 0 {
|
||||
return at(path, ErrUnsupported)
|
||||
}
|
||||
}
|
||||
if v.Kind() == reflect.Map && !v.IsNil() && g.maps[uintptr(v.UnsafePointer())] {
|
||||
return at(path, ErrSharedReference)
|
||||
}
|
||||
if r, ok := mutableSpan(v); ok {
|
||||
i := sort.Search(len(g.spans), func(i int) bool { return g.spans[i].start >= r.end })
|
||||
if i > 0 && g.maxEnd[i-1] > r.start {
|
||||
return at(path, ErrSharedReference)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
func forbidden(t reflect.Type) bool {
|
||||
// Lock/resource implementations are not data. A custom converter may turn
|
||||
// them into a different data type but may not manufacture cloned handles.
|
||||
switch t.PkgPath() {
|
||||
case "sync", "sync/atomic", "os", "net", "database/sql":
|
||||
return t.Kind() == reflect.Struct
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Shallow copies may share handles through pointers, but must not copy lock or
|
||||
// resource state by value, including state inside a containing struct/array.
|
||||
var safeValueTypes sync.Map
|
||||
|
||||
func shallowSafe(t reflect.Type) bool {
|
||||
if known, ok := safeValueTypes.Load(t); ok {
|
||||
return known.(bool)
|
||||
}
|
||||
result := true
|
||||
if forbidden(t) {
|
||||
safeValueTypes.Store(t, false)
|
||||
return false
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if !shallowSafe(t.Field(i).Type) {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
case reflect.Array:
|
||||
result = shallowSafe(t.Elem())
|
||||
}
|
||||
safeValueTypes.Store(t, result)
|
||||
return result
|
||||
}
|
||||
|
||||
func privateMutable(t reflect.Type) bool {
|
||||
if t == timeType {
|
||||
return false
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Interface, reflect.Func, reflect.Chan, reflect.UnsafePointer:
|
||||
return true
|
||||
case reflect.Array:
|
||||
return privateMutable(t.Elem())
|
||||
case reflect.Struct:
|
||||
if forbidden(t) {
|
||||
return true
|
||||
}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if privateMutable(t.Field(i).Type) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
169
copyx/options.go
Normal file
169
copyx/options.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Package copyx copies Go values without JSON, generated code or global registration.
|
||||
package copyx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// NilPolicy controls nil input. NilError preserves nil for nullable targets and
|
||||
// rejects nil to non-nullable targets. NilSkip retains the existing target.
|
||||
type NilPolicy uint8
|
||||
|
||||
const (
|
||||
NilError NilPolicy = iota
|
||||
NilZero
|
||||
NilSkip
|
||||
)
|
||||
|
||||
type pair struct{ from, to reflect.Type }
|
||||
type converter func(reflect.Value) (reflect.Value, error)
|
||||
type fields struct {
|
||||
names map[string]string
|
||||
ignoreSource, ignoreTarget map[string]bool
|
||||
}
|
||||
type settings struct {
|
||||
deep, skipZero, strict bool
|
||||
nilPolicy NilPolicy
|
||||
converters map[pair]converter
|
||||
fields map[pair]*fields
|
||||
}
|
||||
|
||||
// Option configures one immutable Copier. Options never change global behavior.
|
||||
type Option func(*settings) error
|
||||
|
||||
func WithDeepCopy() Option { return func(s *settings) error { s.deep = true; return nil } }
|
||||
|
||||
// WithSkipZero explicitly skips Go zero values, including false, 0 and "".
|
||||
func WithSkipZero() Option { return func(s *settings) error { s.skipZero = true; return nil } }
|
||||
|
||||
// WithStrictFields additionally rejects unconsumed exported source fields.
|
||||
// Target fields must have a source by default, unless explicitly ignored.
|
||||
func WithStrictFields() Option { return func(s *settings) error { s.strict = true; return nil } }
|
||||
func WithNilPolicy(p NilPolicy) Option {
|
||||
return func(s *settings) error {
|
||||
if p > NilSkip {
|
||||
return errors.New("copyx: invalid nil policy")
|
||||
}
|
||||
s.nilPolicy = p
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithConverter registers an exact source/target type pair. Typed nils are
|
||||
// delivered to converters before default nil handling. The function must not
|
||||
// mutate its input and must be safe for concurrent calls when Copier is shared.
|
||||
func WithConverter[S, T any](fn func(S) (T, error)) Option {
|
||||
return func(s *settings) error {
|
||||
if fn == nil {
|
||||
return errors.New("copyx: nil converter")
|
||||
}
|
||||
key := pair{reflect.TypeFor[S](), reflect.TypeFor[T]()}
|
||||
if _, exists := s.converters[key]; exists {
|
||||
return fmt.Errorf("copyx: duplicate converter %s -> %s", key.from, key.to)
|
||||
}
|
||||
s.converters[key] = func(v reflect.Value) (reflect.Value, error) {
|
||||
var input S
|
||||
reflect.ValueOf(&input).Elem().Set(v)
|
||||
out, err := fn(input)
|
||||
return reflect.ValueOf(&out).Elem(), err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithFieldNames declares source -> target Go field names for one struct pair.
|
||||
// It is not a JSON tag mapping. Only actual, unambiguous exported fields qualify.
|
||||
func WithFieldNames[S, T any](names map[string]string) Option {
|
||||
copyNames := make(map[string]string, len(names))
|
||||
for k, v := range names {
|
||||
copyNames[k] = v
|
||||
}
|
||||
return fieldOption[S, T](func(f *fields) error {
|
||||
for from, to := range copyNames {
|
||||
if _, exists := f.names[from]; exists {
|
||||
return fmt.Errorf("copyx: duplicate field rule %s", from)
|
||||
}
|
||||
f.names[from] = to
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
func WithIgnoreSource[S, T any](names ...string) Option {
|
||||
names = append([]string(nil), names...)
|
||||
return fieldOption[S, T](func(f *fields) error {
|
||||
for _, n := range names {
|
||||
f.ignoreSource[n] = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// WithIgnoreTarget preserves the corresponding existing target fields in Copy.
|
||||
// Deep mode rejects that preservation if it would retain a source reference.
|
||||
func WithIgnoreTarget[S, T any](names ...string) Option {
|
||||
names = append([]string(nil), names...)
|
||||
return fieldOption[S, T](func(f *fields) error {
|
||||
for _, n := range names {
|
||||
f.ignoreTarget[n] = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
func fieldOption[S, T any](apply func(*fields) error) Option {
|
||||
return func(s *settings) error {
|
||||
key := pair{reflect.TypeFor[S](), reflect.TypeFor[T]()}
|
||||
if key.from.Kind() != reflect.Struct || key.to.Kind() != reflect.Struct {
|
||||
return errors.New("copyx: field options require struct types")
|
||||
}
|
||||
f := s.fields[key]
|
||||
if f == nil {
|
||||
f = &fields{map[string]string{}, map[string]bool{}, map[string]bool{}}
|
||||
s.fields[key] = f
|
||||
}
|
||||
return apply(f)
|
||||
}
|
||||
}
|
||||
|
||||
// Copier is immutable after construction. Each Copy owns its traversal state.
|
||||
type Copier struct{ options settings }
|
||||
|
||||
func New(options ...Option) (*Copier, error) {
|
||||
s := settings{converters: map[pair]converter{}, fields: map[pair]*fields{}}
|
||||
for _, option := range options {
|
||||
if option == nil {
|
||||
return nil, errors.New("copyx: nil option")
|
||||
}
|
||||
if err := option(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for key, f := range s.fields {
|
||||
from, to := exportedFields(key.from), exportedFields(key.to)
|
||||
used := map[string]bool{}
|
||||
for src, dst := range f.names {
|
||||
if _, ok := from[src]; !ok {
|
||||
return nil, fmt.Errorf("copyx: unknown source field %s.%s", key.from, src)
|
||||
}
|
||||
if _, ok := to[dst]; !ok {
|
||||
return nil, fmt.Errorf("copyx: unknown target field %s.%s", key.to, dst)
|
||||
}
|
||||
if used[dst] || f.ignoreSource[src] || f.ignoreTarget[dst] {
|
||||
return nil, fmt.Errorf("copyx: conflicting field rule %s", dst)
|
||||
}
|
||||
used[dst] = true
|
||||
}
|
||||
for n := range f.ignoreSource {
|
||||
if _, ok := from[n]; !ok {
|
||||
return nil, fmt.Errorf("copyx: unknown source field %s", n)
|
||||
}
|
||||
}
|
||||
for n := range f.ignoreTarget {
|
||||
if _, ok := to[n]; !ok {
|
||||
return nil, fmt.Errorf("copyx: unknown target field %s", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &Copier{options: s}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user