170 lines
5.2 KiB
Go
170 lines
5.2 KiB
Go
// 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
|
|
}
|