-
Notifications
You must be signed in to change notification settings - Fork 0
/
conversions.go
197 lines (162 loc) · 4.88 KB
/
conversions.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package conversions
import (
"container/heap"
"fmt"
"sync"
)
// Conversions contains converters between different types.
type Conversions[T comparable, D any] struct {
converters sync.Map
typeExtractor func(D) (T, error)
}
// New creates a new Conversions instance.
func New[T comparable, D any](opts ...Option[T, D]) (*Conversions[T, D], error) {
o := &options[T, D]{}
for _, opt := range opts {
if err := opt(o); err != nil {
return nil, err
}
}
if o.typeExtractor == nil {
return nil, fmt.Errorf("type extractor is required")
}
c := &Conversions[T, D]{
typeExtractor: o.typeExtractor,
}
// Add the conversions specified via options
for _, conv := range o.converters {
c.AddConversion(conv.From, conv.To, conv.Converter)
}
return c, nil
}
// AddConversion adds a new converter to the conversions.
func (c *Conversions[T, D]) AddConversion(from T, to T, conv Converter[D]) {
m, _ := c.converters.LoadOrStore(from, &sync.Map{})
m.(*sync.Map).Store(to, conv)
}
// Convert converts a value from one type to another.
func (c *Conversions[T, D]) Convert(value D, to T) (any, error) {
from, err := c.typeExtractor(value)
if err != nil {
return nil, err
}
if from == to {
// No conversion needed
return value, nil
}
conv, err := c.findConverter(from, to)
if err != nil {
return nil, err
}
return conv(value)
}
// findConverter will lookup the converter to use between two types. If no
// direct converter is found it will try to find a converter that can convert
// from the source type to an intermediate type and then from that intermediate
// type to the destination type. It does this using a priority queue search,
// where the we always try to find the shortest path first.
func (c *Conversions[T, D]) findConverter(from T, to T) (Converter[D], error) {
// Get if there is a direct converter
if value, ok := c.converters.Load(from); ok {
m := value.(*sync.Map)
conv, ok := m.Load(to)
if ok {
return conv.(Converter[D]), nil
}
}
// Get all converters that can convert from the source type
converters, ok := c.converters.Load(from)
if !ok {
return nil, fmt.Errorf("no converter found from %v to %v", from, to)
}
// Create a priority queue to find the shortest path
queue := &priorityQueue[T, D]{}
heap.Init(queue)
// Add all converters that can convert from the source type
converters.(*sync.Map).Range(func(key, value any) bool {
convertsTo := key.(T)
conv := value.(Converter[D])
heap.Push(queue, &queueItem[T, D]{
conv: conv,
to: convertsTo,
depth: 1,
})
return true
})
// Keep track of previously visited types to avoid loops
visited := make(map[T]bool)
visited[from] = true
// Search for a converter that can convert to the destination type
for queue.Len() > 0 {
item := heap.Pop(queue).(*queueItem[T, D])
intermediateConverter := item.conv
// Check if the converter can convert to the destination type
if item.to == to {
// We can convert to the destination type, so cache the converter
// for future use
c.AddConversion(from, to, item.conv)
return item.conv, nil
}
// No direct converter found, so check if there is a converter that
// can convert from the intermediate type to the destination type
if converters, ok := c.converters.Load(item.to); ok {
// Check if we have already visited this type
if visited[item.to] {
continue
}
// Mark the type as visited
visited[item.to] = true
// Add all converters that can convert from the intermediate type
converters.(*sync.Map).Range(func(key, value any) bool {
convertsTo := key.(T)
nextConverter := value.(Converter[D])
// Create a new converter that will first convert to the
// intermediate type and then to the destination type
conv := func(previousConverter, nextConverter Converter[D]) Converter[D] {
return func(value D) (D, error) {
value, err := previousConverter(value)
if err != nil {
return *new(D), err
}
return nextConverter(value)
}
}(intermediateConverter, nextConverter)
heap.Push(queue, &queueItem[T, D]{
conv: conv,
to: convertsTo,
depth: item.depth + 1,
})
return true
})
}
}
return nil, fmt.Errorf("no converter found from %v to %v", from, to)
}
type priorityQueue[T any, D any] []*queueItem[T, D]
type queueItem[T any, D any] struct {
// The converter to use
conv Converter[D]
// The type that the converter converts to
to T
// The number of converters that have been used to get to this point
depth int
}
func (q priorityQueue[T, D]) Len() int {
return len(q)
}
func (q priorityQueue[T, D]) Less(i, j int) bool {
return q[i].depth < q[j].depth
}
func (q priorityQueue[T, D]) Swap(i, j int) {
q[i], q[j] = q[j], q[i]
}
func (q *priorityQueue[T, D]) Push(x any) {
*q = append(*q, x.(*queueItem[T, D]))
}
func (q *priorityQueue[T, D]) Pop() any {
old := *q
n := len(old)
item := old[n-1]
*q = old[0 : n-1]
return item
}