// 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 }