193 lines
5.5 KiB
Go
193 lines
5.5 KiB
Go
package copyx
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
)
|
|
|
|
// ErrDynamicType means a source interface requires a concrete runtime value.
|
|
// An exact converter can explicitly own that dynamic boundary.
|
|
var ErrDynamicType = errors.New("copyx: dynamic source type requires runtime validation or an exact converter")
|
|
|
|
// Check validates a source/target type pair using the same options as Copy.
|
|
// It never executes converters and requires no example values. Nil policies,
|
|
// converter failures, map-key collisions and deep reference isolation still
|
|
// require runtime checks. Options remain local; no mapping files or registration.
|
|
func Check[S, T any](options ...Option) error {
|
|
c, err := New(options...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.Check(reflect.TypeFor[S](), reflect.TypeFor[T]())
|
|
}
|
|
|
|
// Check validates types with an existing Copier's immutable settings. The target
|
|
// is the result type (not Copy's extra output pointer). Safe for concurrent use.
|
|
func (c *Copier) Check(source, target reflect.Type) error {
|
|
if c == nil {
|
|
return errors.New("copyx: nil Copier")
|
|
}
|
|
if source == nil || target == nil {
|
|
return errors.New("copyx: source and target types are required")
|
|
}
|
|
return newTypeChecker(&c.options, false).check(source, target, "$", 0)
|
|
}
|
|
|
|
type typeChecker struct {
|
|
options *settings
|
|
allowDynamic bool
|
|
seen map[pair]bool
|
|
nodes int
|
|
}
|
|
|
|
func newTypeChecker(options *settings, allowDynamic bool) *typeChecker {
|
|
return &typeChecker{options: options, allowDynamic: allowDynamic, seen: map[pair]bool{}}
|
|
}
|
|
|
|
// Absent elements can reveal type/field mismatches, but contain no actual
|
|
// resource handles or dynamic interface values to validate. Runtime checks stay
|
|
// responsible for those value-dependent cases.
|
|
func (s *state) checkAbsent(from, to reflect.Type, path string) error {
|
|
if s.checkedTypes == nil {
|
|
s.checkedTypes = map[pair]bool{}
|
|
}
|
|
checker := typeChecker{options: s.options, allowDynamic: true, seen: s.checkedTypes, nodes: s.checkedNodes}
|
|
err := checker.check(from, to, path, 0)
|
|
s.checkedNodes = checker.nodes
|
|
return err
|
|
}
|
|
func (c *typeChecker) check(from, to reflect.Type, path string, depth int) error {
|
|
c.nodes++
|
|
if depth > maxDepth || c.nodes > maxNodes {
|
|
return at(path, ErrLimit)
|
|
}
|
|
key := pair{from, to}
|
|
if c.seen[key] {
|
|
return nil
|
|
}
|
|
c.seen[key] = true
|
|
if !c.allowDynamic && !shallowSafe(to) {
|
|
return at(path, ErrUnsupported)
|
|
}
|
|
if _, ok := c.options.converters[key]; ok {
|
|
return nil
|
|
}
|
|
if from == to && len(c.options.converters) == 0 && len(c.options.fields) == 0 && !c.options.skipZero && c.options.nilPolicy != NilSkip {
|
|
if !c.options.deep || simpleValue(to) {
|
|
return nil
|
|
}
|
|
}
|
|
if from.Kind() == reflect.Interface {
|
|
if c.allowDynamic {
|
|
return nil
|
|
}
|
|
return at(path, ErrDynamicType)
|
|
}
|
|
if to.Kind() == reflect.Interface {
|
|
if !from.Implements(to) {
|
|
return at(path, ErrType)
|
|
}
|
|
return c.check(from, from, path, depth+1)
|
|
}
|
|
if !c.allowDynamic && c.options.deep && forbidden(to) {
|
|
return at(path, ErrUnsupported)
|
|
}
|
|
if from.Kind() == reflect.Pointer && to.Kind() != reflect.Pointer {
|
|
return c.check(from.Elem(), to, path, depth+1)
|
|
}
|
|
if to.Kind() == reflect.Pointer {
|
|
if !c.options.deep && from == to {
|
|
return nil
|
|
}
|
|
if from.Kind() == reflect.Pointer {
|
|
from = from.Elem()
|
|
}
|
|
return c.check(from, to.Elem(), path, depth+1)
|
|
}
|
|
if from.Kind() != to.Kind() {
|
|
return at(path, fmt.Errorf("%w: %s -> %s", ErrType, from, to))
|
|
}
|
|
if to == timeType && from == to {
|
|
return nil
|
|
}
|
|
switch to.Kind() {
|
|
case reflect.Struct:
|
|
for i := 0; i < to.NumField(); i++ {
|
|
f := to.Field(i)
|
|
if !c.allowDynamic && f.PkgPath != "" && (from != to || c.options.deep && privateMutable(f.Type)) {
|
|
return at(path+"."+f.Name, ErrUnsupported)
|
|
}
|
|
}
|
|
sources, targets := exportedFields(from), exportedFields(to)
|
|
rules := c.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 := range sortedFieldNames(targets) {
|
|
if rules.ignoreTarget[name] {
|
|
continue
|
|
}
|
|
sourceName := name
|
|
if n, ok := reverse[name]; ok {
|
|
sourceName = n
|
|
}
|
|
source, ok := sources[sourceName]
|
|
if !ok || rules.ignoreSource[sourceName] {
|
|
return at(path+"."+name, ErrField)
|
|
}
|
|
used[sourceName] = true
|
|
if err := c.check(source.Type, targets[name].Type, path+"."+name, depth+1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if c.options.strict {
|
|
for _, name := range sortedFieldNames(sources) {
|
|
if !used[name] && !rules.ignoreSource[name] {
|
|
return at(path+"."+name, ErrField)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
case reflect.Slice:
|
|
if !c.options.deep && from == to {
|
|
return nil
|
|
}
|
|
return c.check(from.Elem(), to.Elem(), path+"[]", depth+1)
|
|
case reflect.Array:
|
|
if from.Len() != to.Len() {
|
|
return at(path, ErrType)
|
|
}
|
|
return c.check(from.Elem(), to.Elem(), path+"[]", depth+1)
|
|
case reflect.Map:
|
|
if !c.options.deep && from == to {
|
|
return nil
|
|
}
|
|
if err := c.check(from.Key(), to.Key(), path+"[key]", depth+1); err != nil {
|
|
return err
|
|
}
|
|
return c.check(from.Elem(), to.Elem(), path+"[value]", depth+1)
|
|
case reflect.Func, reflect.Chan, reflect.UnsafePointer:
|
|
if c.options.deep && !c.allowDynamic {
|
|
return at(path, ErrUnsupported)
|
|
}
|
|
}
|
|
if from != to {
|
|
return at(path, fmt.Errorf("%w: %s -> %s", ErrType, from, to))
|
|
}
|
|
return nil
|
|
}
|
|
func sortedFieldNames(fields map[string]reflect.StructField) []string {
|
|
names := make([]string, 0, len(fields))
|
|
for name := range fields {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|