Skip to content

Commit 79ef8dc

Browse files
committed
Add ShouldConvert
With this new function user can specify which structs, maps or slices are converted. For example, user can skip conversion of standard struct types (such as time.Time) or can skip conversion of all maps with string key. Sometimes this functionality can be used for performances reasons too - user can reduce the amount of time spend on conversion significantly by skipping unnecessary conversion.
1 parent d53cf33 commit 79ef8dc

File tree

5 files changed

+258
-6
lines changed

5 files changed

+258
-6
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* **rename keys** during conversion
1919
* **omit keys** based on field name, value or tag etc.
2020
* **map elements** during conversion
21+
* specify which structs should be converted to maps
2122

2223
## Installation
2324

_examples/skip/main.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"time"
7+
8+
"github.com/elgopher/mapify"
9+
)
10+
11+
// This example shows how to skip conversion of some objects
12+
func main() {
13+
mapper := mapify.Mapper{
14+
ShouldConvert: shouldConvert,
15+
}
16+
17+
v := struct {
18+
Time time.Time // time.Time is a struct too
19+
DontConvertMe struct{ Field string }
20+
}{
21+
Time: time.Now(),
22+
DontConvertMe: struct{ Field string }{Field: "v"},
23+
}
24+
25+
value, err := mapper.MapAny(v)
26+
if err != nil {
27+
panic(err)
28+
}
29+
30+
fmt.Println(value)
31+
}
32+
33+
func shouldConvert(path string, v reflect.Value) (bool, error) {
34+
t := v.Type()
35+
36+
if t.PkgPath() == "time" && t.Name() == "Time" {
37+
// time.Time struct will not be converted to map
38+
return false, nil
39+
}
40+
41+
if path == ".DontConvertMe" {
42+
return false, nil
43+
}
44+
45+
return true, nil
46+
}

defaults.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
package mapify
55

6+
import "reflect"
7+
8+
func convertAll(string, reflect.Value) (bool, error) {
9+
return true, nil
10+
}
11+
612
func acceptAllFields(string, Element) (bool, error) {
713
return true, nil
814
}

mapify.go

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@ import (
1212

1313
// Mapper represents instance of mapper
1414
type Mapper struct {
15-
Filter Filter
16-
Rename Rename
17-
MapValue MapValue
15+
ShouldConvert ShouldConvert
16+
Filter Filter
17+
Rename Rename
18+
MapValue MapValue
1819
}
1920

21+
// ShouldConvert returns true when value should be converted to map. The value can be a struct, map[string]any or slice.
22+
type ShouldConvert func(path string, value reflect.Value) (bool, error)
23+
2024
// Filter returns true when element should be included. If error is returned then the whole conversion is aborted
2125
// and wrapped error is returned from Mapper.MapAny method.
2226
type Filter func(path string, e Element) (bool, error)
@@ -62,11 +66,28 @@ func (i Mapper) mapAny(path string, v interface{}) (interface{}, error) {
6266
reflectValue := reflect.ValueOf(v)
6367

6468
switch {
65-
case reflectValue.Kind() == reflect.Ptr && reflectValue.Elem().Kind() == reflect.Struct:
66-
return i.mapAny(path, reflectValue.Elem().Interface())
67-
case reflectValue.Kind() == reflect.Struct:
69+
case reflectValue.Kind() == reflect.Struct ||
70+
(reflectValue.Kind() == reflect.Ptr && reflectValue.Elem().Kind() == reflect.Struct):
71+
shouldConvert, err := i.ShouldConvert(path, reflectValue)
72+
if err != nil {
73+
return nil, fmt.Errorf("ShouldConvert failed: %w", err)
74+
}
75+
76+
if !shouldConvert {
77+
return reflectValue.Interface(), nil
78+
}
79+
6880
return i.mapStruct(path, reflectValue)
6981
case reflectValue.Kind() == reflect.Map && reflectValue.Type().Key().Kind() == reflect.String:
82+
shouldConvert, err := i.ShouldConvert(path, reflectValue)
83+
if err != nil {
84+
return nil, fmt.Errorf("ShouldConvert failed: %w", err)
85+
}
86+
87+
if !shouldConvert {
88+
return reflectValue.Interface(), nil
89+
}
90+
7091
return i.mapStringMap(path, reflectValue)
7192
case reflectValue.Kind() == reflect.Slice:
7293
return i.mapSlice(path, reflectValue)
@@ -76,6 +97,10 @@ func (i Mapper) mapAny(path string, v interface{}) (interface{}, error) {
7697
}
7798

7899
func (i Mapper) newInstance() Mapper {
100+
if i.ShouldConvert == nil {
101+
i.ShouldConvert = convertAll
102+
}
103+
79104
if i.Filter == nil {
80105
i.Filter = acceptAllFields
81106
}
@@ -94,6 +119,8 @@ func (i Mapper) newInstance() Mapper {
94119
func (i Mapper) mapStruct(path string, reflectValue reflect.Value) (map[string]interface{}, error) {
95120
result := map[string]interface{}{}
96121

122+
reflectValue = dereference(reflectValue)
123+
97124
reflectType := reflectValue.Type()
98125

99126
for j := 0; j < reflectType.NumField(); j++ {
@@ -116,6 +143,14 @@ func (i Mapper) mapStruct(path string, reflectValue reflect.Value) (map[string]i
116143
return result, nil
117144
}
118145

146+
func dereference(value reflect.Value) reflect.Value {
147+
for value.Kind() == reflect.Ptr {
148+
value = value.Elem()
149+
}
150+
151+
return value
152+
}
153+
119154
func (i Mapper) mapStringMap(path string, reflectValue reflect.Value) (map[string]interface{}, error) {
120155
result := map[string]interface{}{}
121156

@@ -167,6 +202,15 @@ func (i Mapper) mapSlice(path string, reflectValue reflect.Value) (_ interface{}
167202

168203
switch kind {
169204
case reflect.Struct:
205+
shouldConvert, err := i.ShouldConvert(path, reflectValue)
206+
if err != nil {
207+
return nil, fmt.Errorf("ShouldConvert failed: %w", err)
208+
}
209+
210+
if !shouldConvert {
211+
return reflectValue.Interface(), nil
212+
}
213+
170214
slice := make([]map[string]interface{}, reflectValue.Len())
171215

172216
for j := 0; j < reflectValue.Len(); j++ {
@@ -182,6 +226,15 @@ func (i Mapper) mapSlice(path string, reflectValue reflect.Value) (_ interface{}
182226
return reflectValue.Interface(), nil
183227
}
184228

229+
shouldConvert, err := i.ShouldConvert(path, reflectValue)
230+
if err != nil {
231+
return nil, fmt.Errorf("ShouldConvert failed: %w", err)
232+
}
233+
234+
if !shouldConvert {
235+
return reflectValue.Interface(), nil
236+
}
237+
185238
slice := make([]map[string]interface{}, reflectValue.Len())
186239

187240
for j := 0; j < reflectValue.Len(); j++ {
@@ -198,6 +251,15 @@ func (i Mapper) mapSlice(path string, reflectValue reflect.Value) (_ interface{}
198251
if sliceElem.Kind() == reflect.Struct ||
199252
(sliceElem.Kind() == reflect.Map && sliceElem.Key().Kind() == reflect.String) {
200253

254+
shouldConvert, err := i.ShouldConvert(path, reflectValue)
255+
if err != nil {
256+
return nil, fmt.Errorf("ShouldConvert failed: %w", err)
257+
}
258+
259+
if !shouldConvert {
260+
return reflectValue.Interface(), nil
261+
}
262+
201263
var slice [][]map[string]interface{}
202264

203265
for j := 0; j < reflectValue.Len(); j++ {

mapify_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package mapify_test
55

66
import (
7+
"fmt"
8+
"reflect"
79
"testing"
810

911
"github.com/elgopher/mapify"
@@ -700,3 +702,138 @@ func assertStructField(t *testing.T, fieldName string, e mapify.Element) {
700702
require.True(t, ok)
701703
assert.Equal(t, fieldName, field.Name)
702704
}
705+
706+
func TestShouldConvert(t *testing.T) {
707+
t.Run("should not convert root element", func(t *testing.T) {
708+
mapper := mapify.Mapper{
709+
ShouldConvert: func(path string, value reflect.Value) (bool, error) {
710+
if path == "" {
711+
return false, nil
712+
}
713+
714+
return true, nil
715+
},
716+
}
717+
718+
tests := map[string]interface{}{
719+
"struct": struct{ Field string }{Field: "v"},
720+
"map": map[string]string{"key": "value"},
721+
"slice": []struct{ Field string }{{Field: "v"}},
722+
}
723+
724+
for name, instance := range tests {
725+
t.Run(name, func(t *testing.T) {
726+
// when
727+
mapped, err := mapper.MapAny(instance)
728+
// then
729+
require.NoError(t, err)
730+
assert.Equal(t, instance, mapped)
731+
})
732+
}
733+
})
734+
735+
t.Run("should not convert nested object", func(t *testing.T) {
736+
type nestedStruct struct{ Field string }
737+
738+
nestedStructInstance := nestedStruct{Field: "a"}
739+
nestedMap := map[string]string{"Field": "a"}
740+
741+
tests := map[string]struct {
742+
input, expected interface{}
743+
}{
744+
"struct in a struct field": {
745+
input: struct{ Nested nestedStruct }{Nested: nestedStructInstance},
746+
expected: map[string]interface{}{"Nested": nestedStructInstance},
747+
},
748+
"struct pointer in a struct field": {
749+
input: struct{ Nested *nestedStruct }{Nested: &nestedStructInstance},
750+
expected: map[string]interface{}{"Nested": &nestedStructInstance},
751+
},
752+
"struct in a map key": {
753+
input: map[string]nestedStruct{"Nested": nestedStructInstance},
754+
expected: map[string]interface{}{"Nested": nestedStructInstance},
755+
},
756+
"map in a struct field": {
757+
input: struct{ Nested map[string]string }{Nested: nestedMap},
758+
expected: map[string]interface{}{"Nested": nestedMap},
759+
},
760+
"map in a map key": {
761+
input: map[string]map[string]string{"Nested": nestedMap},
762+
expected: map[string]interface{}{"Nested": nestedMap},
763+
},
764+
"structs slice in a struct field": {
765+
input: struct{ Nested []nestedStruct }{Nested: []nestedStruct{nestedStructInstance}},
766+
expected: map[string]interface{}{"Nested": []nestedStruct{nestedStructInstance}},
767+
},
768+
"structs slice in a map key": {
769+
input: map[string][]nestedStruct{"Nested": {nestedStructInstance}},
770+
expected: map[string]interface{}{"Nested": []nestedStruct{nestedStructInstance}},
771+
},
772+
"map slice in a struct field": {
773+
input: struct{ Nested []map[string]string }{Nested: []map[string]string{nestedMap}},
774+
expected: map[string]interface{}{"Nested": []map[string]string{nestedMap}},
775+
},
776+
"2d structs slice in a map key": {
777+
input: map[string][][]nestedStruct{"Nested": {{nestedStructInstance}}},
778+
expected: map[string]interface{}{"Nested": [][]nestedStruct{{nestedStructInstance}}},
779+
},
780+
}
781+
782+
mapper := mapify.Mapper{
783+
ShouldConvert: func(path string, value reflect.Value) (bool, error) {
784+
if path == ".Nested" {
785+
return false, nil
786+
}
787+
788+
return true, nil
789+
},
790+
}
791+
792+
for name, test := range tests {
793+
t.Run(name, func(t *testing.T) {
794+
// when
795+
mapped, err := mapper.MapAny(test.input)
796+
// then
797+
require.NoError(t, err)
798+
assert.Equal(t, test.expected, mapped)
799+
})
800+
}
801+
})
802+
803+
t.Run("should not run ShouldConvert for inconvertible values", func(t *testing.T) {
804+
mapper := mapify.Mapper{
805+
ShouldConvert: func(path string, value reflect.Value) (bool, error) {
806+
panic("ShouldConvert run but should not")
807+
},
808+
}
809+
810+
tests := []interface{}{nil, 1, "str", map[int]interface{}{}, []int{1}}
811+
812+
for _, test := range tests {
813+
name := fmt.Sprintf("%+v", test)
814+
815+
t.Run(name, func(t *testing.T) {
816+
assert.NotPanics(t, func() {
817+
_, _ = mapper.MapAny(test)
818+
})
819+
})
820+
}
821+
})
822+
823+
t.Run("should run ShouldConvert only once for pointer to struct", func(t *testing.T) {
824+
s := &struct{ Field string }{Field: "v"}
825+
826+
timesRun := 0
827+
mapper := mapify.Mapper{
828+
ShouldConvert: func(path string, value reflect.Value) (bool, error) {
829+
assert.Equal(t, s, value.Interface())
830+
timesRun++
831+
return true, nil
832+
},
833+
}
834+
835+
_, err := mapper.MapAny(s)
836+
require.NoError(t, err)
837+
assert.Equal(t, 1, timesRun, "ShouldConvert must be executed only once")
838+
})
839+
}

0 commit comments

Comments
 (0)