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 }