feat: replace mapping generation with checked copy utilities
This commit is contained in:
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