Skip to content

Commit 0cabf70

Browse files
committed
feat: chain with independent convert functions
Signed-off-by: Keith Zantow <[email protected]>
1 parent c72ef88 commit 0cabf70

5 files changed

+563
-0
lines changed

convert_func.go

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package converter
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
)
7+
8+
type FuncChain interface {
9+
Convert(from interface{}, to interface{}) error
10+
}
11+
12+
type funcChain struct {
13+
funcs map[reflect.Type]map[reflect.Type]func(from reflect.Value, to reflect.Value) error
14+
}
15+
16+
func NewFuncChain(converters ...interface{}) FuncChain {
17+
out := funcChain{
18+
funcs: map[reflect.Type]map[reflect.Type]func(from reflect.Value, to reflect.Value) error{},
19+
}
20+
for _, converter := range converters {
21+
out.AddConverter(converter)
22+
}
23+
return out
24+
}
25+
26+
var chainType = reflect.TypeOf((*FuncChain)(nil)).Elem()
27+
var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
28+
29+
func (c funcChain) AddConverter(converter interface{}) {
30+
convertFunc := reflect.ValueOf(converter)
31+
convertFuncType := convertFunc.Type()
32+
if validationError := validateConvertFunc(convertFuncType); validationError != nil {
33+
panic(fmt.Errorf(`converter must be a function of one of the following forms:
34+
func(from *Type1, to *Type2)
35+
func(from *Type1, to *Type2) error
36+
func(chain %v, from *Type1, to *Type2)
37+
func(chain %v, from *Type1, to *Type2) error
38+
39+
got: %+v
40+
err: %v
41+
`, chainType, chainType, convertFuncType, validationError))
42+
}
43+
44+
// seems to be a valid function, create a handler function for it
45+
46+
returnsError := false
47+
if convertFuncType.NumOut() > 0 {
48+
returnsError = true
49+
}
50+
51+
hasChainParam := false
52+
fromType := convertFuncType.In(0)
53+
toType := convertFuncType.In(1)
54+
55+
if convertFuncType.NumIn() > 2 {
56+
hasChainParam = true
57+
fromType = convertFuncType.In(1)
58+
toType = convertFuncType.In(2)
59+
}
60+
61+
baseFromType := baseType(fromType)
62+
baseToType := baseType(toType)
63+
64+
convertFuncs := c.funcs[baseFromType]
65+
if convertFuncs == nil {
66+
convertFuncs = map[reflect.Type]func(from reflect.Value, to reflect.Value) error{}
67+
c.funcs[baseFromType] = convertFuncs
68+
}
69+
70+
if convertFuncs[baseToType] != nil {
71+
panic(fmt.Errorf("convert from: %s -> %s defined multiple times; %+v", typeName(baseFromType), typeName(baseToType), reflect.TypeOf(convertFuncs[baseToType])))
72+
}
73+
74+
convertFuncs[baseToType] = func(from reflect.Value, to reflect.Value) error {
75+
// setup matching args, from and to should already be set up properly
76+
var args []reflect.Value
77+
if hasChainParam {
78+
args = []reflect.Value{reflect.ValueOf(c), from, to}
79+
} else {
80+
args = []reflect.Value{from, to}
81+
}
82+
83+
// invoke the function
84+
out := convertFunc.Call(args)
85+
86+
// return errors if the function does
87+
if returnsError && !out[0].IsNil() {
88+
return out[0].Interface().(error)
89+
}
90+
return nil
91+
}
92+
}
93+
94+
func typeName(t reflect.Type) string {
95+
return fmt.Sprintf("<%s>.%s", t.PkgPath(), t.Name())
96+
}
97+
98+
func validateConvertFunc(t reflect.Type) error {
99+
if t.Kind() != reflect.Func {
100+
return fmt.Errorf("not a function")
101+
}
102+
// need to have 2 or 3 args, optionally with funcChain as the first one
103+
if t.NumIn() < 2 || t.NumIn() > 3 {
104+
return fmt.Errorf("must have 2 or 3 arguments")
105+
}
106+
fromType := t.In(0)
107+
toType := t.In(1)
108+
if t.NumIn() > 2 {
109+
if t.In(0) != chainType {
110+
return fmt.Errorf("when using 3 arguments, %+v must the first", chainType)
111+
}
112+
fromType = t.In(1)
113+
toType = t.In(2)
114+
} else if t.In(0) == chainType {
115+
return fmt.Errorf("if %+v is the first argument, there must be 2 more arguments to convert", chainType)
116+
}
117+
118+
// it doesn't make sense to convert from a type to the same type
119+
if baseType(fromType) == baseType(toType) {
120+
return fmt.Errorf("convert should be between different types")
121+
}
122+
// toType must be a pointer, which will be provided by the convert function
123+
if !isPtr(toType) {
124+
return fmt.Errorf("second convert argument, the target/destination must be a pointer; got: %+v", toType)
125+
}
126+
// return type is either error or nothing
127+
if t.NumOut() > 1 {
128+
return fmt.Errorf("too many return values, must return error or have no return value")
129+
}
130+
if t.NumOut() > 0 && !t.Out(0).Implements(errorInterface) {
131+
return fmt.Errorf("must return error or have no return value")
132+
}
133+
134+
return nil
135+
}
136+
137+
func (c funcChain) Convert(from interface{}, to interface{}) error {
138+
fromValue := reflect.ValueOf(from)
139+
fromType := fromValue.Type()
140+
baseFromType := baseType(fromType)
141+
142+
toValue := reflect.ValueOf(to)
143+
toType := toValue.Type()
144+
baseToType := baseType(toType)
145+
146+
// build the shortest path between types
147+
chain := c.shortestChain(baseFromType, baseToType)
148+
149+
// iterate, creating any intermediary structs for the migration
150+
last := fromValue
151+
for i, step := range chain {
152+
var next reflect.Value
153+
if i == len(chain)-1 {
154+
next = toValue
155+
} else {
156+
next = reflect.New(step.targetType)
157+
}
158+
159+
if err := c.convert(last, next); err != nil {
160+
return err
161+
}
162+
163+
last = next
164+
}
165+
166+
return nil
167+
}
168+
169+
func baseType(t reflect.Type) reflect.Type {
170+
for isPtr(t) {
171+
t = t.Elem()
172+
}
173+
return t
174+
}
175+
176+
type reflectConvertFunc func(from reflect.Value, to reflect.Value) error
177+
type reflectConvertStep struct {
178+
targetType reflect.Type
179+
convertFunc reflectConvertFunc
180+
}
181+
182+
func (c funcChain) shortestChain(fromType reflect.Type, targetType reflect.Type) []reflectConvertStep {
183+
var shortest []reflectConvertStep
184+
for toType, convertFunc := range c.funcs[fromType] {
185+
if toType == targetType {
186+
return []reflectConvertStep{{toType, convertFunc}}
187+
}
188+
chain := c.shortestChain(toType, targetType)
189+
if chain != nil {
190+
chain = append([]reflectConvertStep{{toType, convertFunc}}, chain...)
191+
}
192+
if shortest == nil || len(chain) < len(shortest) {
193+
shortest = chain
194+
}
195+
}
196+
return shortest
197+
}

0 commit comments

Comments
 (0)