-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
reflect.go
331 lines (270 loc) · 8.84 KB
/
reflect.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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
package converter
import (
"database/sql"
"database/sql/driver"
"reflect"
"github.com/pkg/errors"
"github.com/infevocorp/goflexstore/store"
)
// NewReflect creates a new reflection-based converter.
//
// It converts between DTO and Entity using reflection, mapping fields from one to the other.
// The `overridesMapping` argument allows specifying custom field name mappings between the Entity and DTO.
// If nil or empty, the Entity's field names are used as DTO's field names.
//
// Type parameters:
// - Entity: The Entity type implementing store.Entity interface.
// - DTO: The DTO type implementing store.Entity interface.
// - ID: The type of the identifier for Entity and DTO, which must be comparable.
//
// Parameters:
// - overridesMapping: A map where the key is the Entity's field name and the value is the DTO's field name.
//
// Returns:
// A new instance of Reflect converter with the specified field mappings.
func NewReflect[
Entity store.Entity[ID],
DTO store.Entity[ID],
ID comparable,
](
overridesMapping map[string]string,
) Converter[Entity, DTO, ID] {
return Reflect[Entity, DTO, ID]{
dtoFieldsMapping: overridesMapping,
entityFieldMapping: reverseMapping(overridesMapping),
}
}
// Reflect is a converter that uses reflection to convert between DTO and Entity.
// It implements the Converter interface and allows for automated conversion based on field names.
//
// Type parameters:
// - Entity: The Entity type.
// - DTO: The DTO type.
// - ID: The type of the identifier for Entity and DTO.
//
// Fields:
// - dtoFieldsMapping: Map where the key is Entity's field name and the value is DTO's field name.
// - entityFieldMapping: Map where the key is DTO's field name and the value is Entity's field name.
type Reflect[Entity store.Entity[ID], DTO store.Entity[ID], ID comparable] struct {
// fieldMapping key is Entity's field name. value is DTO's field name.
dtoFieldsMapping map[string]string
// fieldMapping key is DTO's field name. value is Entity's field name.
entityFieldMapping map[string]string
}
// ToEntity converts a DTO to an Entity using reflection.
// It creates a new instance of Entity and copies values from the DTO to the Entity based on field mappings.
//
// Parameters:
// - dto: The DTO to be converted to Entity.
//
// Returns:
// The converted Entity.
func (c Reflect[Entity, DTO, ID]) ToEntity(dto DTO) Entity {
entity := *new(Entity)
reflectCopy(dto, &entity, c.entityFieldMapping)
return entity
}
// ToDTO converts an Entity to a DTO using reflection.
// It creates a new instance of DTO and copies values from the Entity to the DTO based on field mappings.
//
// Parameters:
// - entity: The Entity to be converted to DTO.
//
// Returns:
// The converted DTO.
func (c Reflect[Entity, DTO, ID]) ToDTO(entity Entity) DTO {
dto := *new(DTO)
reflectCopy(entity, &dto, c.dtoFieldsMapping)
return dto
}
// reflectCopy performs the actual copying of values from the source to the destination.
// It iterates over the fields of the destination and sets values from the source based on the provided field mapping.
//
// Parameters:
// - src: The source object.
// - dst: The destination object.
// - fieldMapping: Map where the key is the destination field name and the value is the source field name.
func reflectCopy(src any, dst any, fieldMapping map[string]string) {
// Obtain a reflection Value of the source object.
srcVal := reflect.ValueOf(src)
// Unwrap the source value if it's a pointer.
// This is to handle cases where the source is a pointer type.
for srcVal.Kind() == reflect.Ptr {
// If the source value is a zero value (nil), return early.
if srcVal.IsZero() {
return
}
// Get the actual value that the pointer points to.
srcVal = srcVal.Elem()
}
// Obtain a reflection Value of the destination object.
dstVal := reflect.ValueOf(dst)
// Ensure the destination is a pointer, as we need to modify it.
if dstVal.Kind() != reflect.Ptr {
panic("dst must be reference type (pointer)")
}
// Unwrap the destination value if it's a pointer.
// This also ensures that we're dealing with the actual value.
for dstVal.Kind() == reflect.Ptr {
// If the destination is a nil pointer, initialize it with a new value.
if dstVal.IsNil() {
dstVal.Set(reflect.New(dstVal.Type().Elem()))
}
// Get the actual value that the pointer points to.
dstVal = dstVal.Elem()
}
// Get the type information of the destination.
dstType := dstVal.Type()
// Iterate over each field of the destination.
numField := dstVal.NumField()
for i := 0; i < numField; i++ {
// Get the i-th field of the destination.
dstField := dstVal.Field(i)
// Skip if the field cannot be set (unexported private field).
if !dstField.CanSet() {
continue
}
// Get the name of the i-th field.
dstFieldName := dstType.Field(i).Name
// If a field mapping exists, use it to find the corresponding source field.
if fieldMapping != nil {
if f, ok := fieldMapping[dstFieldName]; ok && f != "" {
dstFieldName = f
}
}
// Find the field in the source object that matches the destination field.
srcField := srcVal.FieldByName(dstFieldName)
// Skip if the source field is not valid (doesn't exist).
if !srcField.IsValid() {
continue
}
// If the source field is a pointer but nil, skip copying.
if (srcField.Kind() == reflect.Ptr || srcField.Kind() == reflect.Slice) && srcField.IsNil() {
continue
}
// Attempt to set the destination field with the value of the source field.
// Panic with a detailed error message if the assignment is not possible.
if !setValue(srcField, dstField) {
panic(errors.Errorf(
"cannot assign src.%s(%s) to dst.%s(%s)",
dstFieldName,
srcField.Type().String(),
dstFieldName,
dstField.Type().String(),
))
}
}
}
func reverseMapping[K comparable, V comparable](m map[K]V) map[V]K {
reversed := make(map[V]K, len(m))
for k, v := range m {
reversed[v] = k
}
return reversed
}
func setValue(srcVal, dstVal reflect.Value) bool {
// same type
if srcVal.Type() == dstVal.Type() {
dstVal.Set(srcVal)
return true
}
if ok := tryIfTargetTypeIsScanner(srcVal, dstVal); ok {
return true
}
if ok := tryIfTargetTypeIsValuer(srcVal, dstVal); ok {
return true
}
if ok := tryIfStruct(srcVal, dstVal); ok {
return true
}
if ok := tryIfSlice(srcVal, dstVal); ok {
return true
}
return false
}
func tryIfTargetTypeIsScanner(src reflect.Value, dst reflect.Value) bool {
// check if dst is struct so we should use pointer to dst
// because all sql.Null* types implement sql.Scanner as pointer receiver
if dst.Kind() == reflect.Struct && dst.CanAddr() {
dst = dst.Addr()
}
// check if dst implements sql.Scanner interface
if !dst.Type().Implements(reflect.TypeOf((*sql.Scanner)(nil)).Elem()) {
return false
}
// check if dst is nil
// so need to init it first
if dst.Kind() == reflect.Ptr && dst.IsNil() {
dst.Set(reflect.New(dst.Type().Elem()))
}
if results := dst.MethodByName("Scan").Call([]reflect.Value{src}); !results[0].IsNil() {
err := results[0].Interface().(error)
panic(errors.Errorf("cannot assign %s to %s: %v", src.String(), dst.String(), err))
}
return true
}
func tryIfTargetTypeIsValuer(src reflect.Value, dst reflect.Value) bool {
// unwrap pointer
// because all driver.Valuer types implement driver.Valuer as value receiver
for src.Kind() == reflect.Ptr {
if src.IsNil() {
return false
}
src = src.Elem()
}
// check if src implements driver.Valuer interface
if !src.Type().Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem()) {
return false
}
// execute Value() method
results := src.MethodByName("Value").Call([]reflect.Value{})
// check if Value() method returns nil
value := results[0].Interface()
if value == nil {
return true
}
// set value if src and dst have the same type
if valueOf := reflect.ValueOf(value); valueOf.Type() == dst.Type() {
dst.Set(valueOf)
}
return true
}
func tryIfStruct(src, dst reflect.Value) bool {
srcType := src.Type()
dstType := dst.Type()
if getStructType(srcType).Kind() != reflect.Struct || getStructType(dstType).Kind() != reflect.Struct {
return false
}
if dst.IsNil() {
dst.Set(reflect.New(getStructType(dstType)))
}
reflectCopy(src.Interface(), dst.Interface(), nil)
return true
}
func tryIfSlice(src, dst reflect.Value) bool {
srcType := src.Type()
dstType := dst.Type()
if srcType.Kind() != reflect.Slice || dstType.Kind() != reflect.Slice {
return false
}
n := src.Len()
tmpArr := reflect.MakeSlice(dstType, n, n)
for i := 0; i < n; i++ {
srcElem := src.Index(i)
dstEl := tmpArr.Index(i)
if dstEl.Type().Kind() != reflect.Ptr {
dstEl = dstEl.Addr()
} else {
dstEl.Set(reflect.New(dstEl.Type().Elem()))
}
reflectCopy(srcElem.Interface(), dstEl.Interface(), nil)
}
dst.Set(tmpArr)
return true
}
func getStructType(src reflect.Type) reflect.Type {
if src.Kind() == reflect.Ptr {
src = src.Elem()
}
return src
}