From 94e87cb6f48af6e362061a7d439bc25157044d14 Mon Sep 17 00:00:00 2001 From: Jacek Olszak Date: Sat, 19 Mar 2022 09:34:24 +0100 Subject: [PATCH] Add support for map conversion Mapper.MapAny now supports map conversion. `map[string]any` instances are converted to map[string]interface{}. During conversion usual callbacks are executed: Filter, Rename and MapValue. --- README.md | 12 +- _examples/maps/main.go | 32 +++ mapify.go | 45 +++- mapify_test.go | 521 ++++++++++++++++++++++++++--------------- 4 files changed, 416 insertions(+), 194 deletions(-) create mode 100644 _examples/maps/main.go diff --git a/README.md b/README.md index 163e80c..d1687ab 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/elgopher/mapify)](https://goreportcard.com/report/github.com/elgopher/mapify) [![codecov](https://codecov.io/gh/elgopher/mapify/branch/master/graph/badge.svg)](https://codecov.io/gh/elgopher/mapify) -**Highly configurable** struct to map converter. _Will convert maps into other maps as well (work in progress)._ +**Highly configurable** struct to map converter. Converts `map[string]any` into other maps as well. ## Features @@ -55,4 +55,12 @@ func main() { type SomeStruct struct { Field string } -``` \ No newline at end of file +``` + +## MapAny algorithm + +1. Take an object which is a struct, map or slice. +2. Traverse entire object looking for nested structs or maps. +3. **Filter** elements (struct fields, map keys) +4. **Rename** field names or map keys +5. **Map** (struct field or map values) \ No newline at end of file diff --git a/_examples/maps/main.go b/_examples/maps/main.go new file mode 100644 index 0000000..821571d --- /dev/null +++ b/_examples/maps/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/elgopher/mapify" +) + +// This example shows how to filter maps and rename keys +func main() { + s := map[string]interface{}{ + "key": "value", + "another": "another value", + } + + mapper := mapify.Mapper{ + Filter: func(path string, e mapify.Element) (bool, error) { + return path == ".key", nil + }, + Rename: func(path string, e mapify.Element) (string, error) { + return strings.ToUpper(e.Name()), nil + }, + } + + result, err := mapper.MapAny(s) + if err != nil { + panic(err) + } + + fmt.Printf("%+v", result) // map[KEY:value] +} diff --git a/mapify.go b/mapify.go index a3972ee..31d4306 100644 --- a/mapify.go +++ b/mapify.go @@ -66,6 +66,8 @@ func (i Mapper) mapAny(path string, v interface{}) (interface{}, error) { return i.mapAny(path, reflectValue.Elem().Interface()) case reflectValue.Kind() == reflect.Struct: return i.mapStruct(path, reflectValue) + case reflectValue.Kind() == reflect.Map && reflectValue.Type().Key().Kind() == reflect.String: + return i.mapStringMap(path, reflectValue) case reflectValue.Kind() == reflect.Slice: return i.mapSlice(path, reflectValue) default: @@ -106,7 +108,7 @@ func (i Mapper) mapStruct(path string, reflectValue reflect.Value) (map[string]i value := reflectValue.Field(j) element := Element{name: fieldName, Value: value, field: &field} - if err := i.mapStructField(fieldPath, element, result); err != nil { + if err := i.mapElement(fieldPath, element, result); err != nil { return nil, err } } @@ -114,7 +116,25 @@ func (i Mapper) mapStruct(path string, reflectValue reflect.Value) (map[string]i return result, nil } -func (i Mapper) mapStructField(fieldPath string, element Element, result map[string]interface{}) error { +func (i Mapper) mapStringMap(path string, reflectValue reflect.Value) (map[string]interface{}, error) { + result := map[string]interface{}{} + + keys := reflectValue.MapKeys() + for _, key := range keys { + fieldName := key.String() + fieldPath := path + "." + fieldName + value := reflectValue.MapIndex(key) + element := Element{name: fieldName, Value: value} + + if err := i.mapElement(fieldPath, element, result); err != nil { + return nil, err + } + } + + return result, nil +} + +func (i Mapper) mapElement(fieldPath string, element Element, result map[string]interface{}) error { accepted, filterErr := i.Filter(fieldPath, element) if filterErr != nil { return fmt.Errorf("Filter failed: %w", filterErr) @@ -156,9 +176,28 @@ func (i Mapper) mapSlice(path string, reflectValue reflect.Value) (_ interface{} } } + return slice, nil + case reflect.Map: + if reflectValue.Type().Elem().Key().Kind() != reflect.String { + return reflectValue.Interface(), nil + } + + slice := make([]map[string]interface{}, reflectValue.Len()) + + for j := 0; j < reflectValue.Len(); j++ { + slice[j], err = i.mapStringMap(slicePath(path, j), reflectValue.Index(j)) + if err != nil { + return nil, err + } + } + return slice, nil case reflect.Slice: - if reflectValue.Type().Elem().Elem().Kind() == reflect.Struct { + sliceElem := reflectValue.Type().Elem().Elem() + + if sliceElem.Kind() == reflect.Struct || + (sliceElem.Kind() == reflect.Map && sliceElem.Key().Kind() == reflect.String) { + var slice [][]map[string]interface{} for j := 0; j < reflectValue.Len(); j++ { diff --git a/mapify_test.go b/mapify_test.go index 61afc40..a45073f 100644 --- a/mapify_test.go +++ b/mapify_test.go @@ -257,267 +257,410 @@ func TestMapper_MapAny(t *testing.T) { } assert.Equal(t, expected, actual) }) + }) + + t.Run("should convert map[string]any to map[string]interface{}", func(t *testing.T) { + m := map[string]string{"key1": "value1", "key2": "value2"} + mapper := mapify.Mapper{} + result, err := mapper.MapAny(m) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"key1": "value1", "key2": "value2"}, result) + }) + t.Run("should not convert map which does not have a string key", func(t *testing.T) { + tests := map[string]interface{}{ + "map[struct]": map[struct{ A string }]string{}, + "[]map[struct]": []map[struct{ A string }]string{{}}, + "[][]map[struct]": [][]map[struct{ A string }]string{{}}, + } + + for name, theMap := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{} + result, err := mapper.MapAny(theMap) + require.NoError(t, err) + assert.Equal(t, theMap, result) + }) + } }) } func TestFilter(t *testing.T) { - t.Run("should filter out all struct fields", func(t *testing.T) { - s := struct{ A, B string }{} - mapper := mapify.Mapper{ - Filter: func(path string, e mapify.Element) (bool, error) { - return false, nil - }, + t.Run("should filter out all elements", func(t *testing.T) { + tests := map[string]interface{}{ + "struct": struct{ A, B string }{}, + "map": map[string]string{"A": "", "B": ""}, + } + + for name, object := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{ + Filter: func(path string, e mapify.Element) (bool, error) { + return false, nil + }, + } + // when + v, err := mapper.MapAny(object) + // then + require.NoError(t, err) + assert.Empty(t, v) + }) } - // when - v, err := mapper.MapAny(s) - // then - require.NoError(t, err) - assert.Empty(t, v) }) - t.Run("should filter by struct field path", func(t *testing.T) { - s := struct{ A, B string }{} - mapper := mapify.Mapper{ - Filter: func(path string, e mapify.Element) (bool, error) { - return path == ".A", nil - }, + t.Run("should filter by element path", func(t *testing.T) { + tests := map[string]interface{}{ + "struct": struct{ A, B string }{}, + "map": map[string]string{"A": "", "B": ""}, } - // when - v, err := mapper.MapAny(s) - // then - require.NoError(t, err) - expected := map[string]interface{}{ - "A": "", + + for name, object := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{ + Filter: func(path string, e mapify.Element) (bool, error) { + return path == ".A", nil + }, + } + // when + v, err := mapper.MapAny(object) + // then + require.NoError(t, err) + expected := map[string]interface{}{ + "A": "", + } + assert.Equal(t, expected, v) + }) } - assert.Equal(t, expected, v) }) - t.Run("should filter by nested struct field path", func(t *testing.T) { - s := struct { - Nested struct{ A string } - }{} - mapper := mapify.Mapper{ - Filter: func(path string, e mapify.Element) (bool, error) { - return path == ".Nested" || path == ".Nested.A", nil + t.Run("should filter by nested element path", func(t *testing.T) { + tests := map[string]interface{}{ + "struct": struct { + Nested struct{ A string } + }{}, + "map": map[string]map[string]string{ + "Nested": {"A": ""}, }, } - // when - v, err := mapper.MapAny(s) - // then - require.NoError(t, err) - assert.Equal(t, map[string]interface{}{ - "Nested": map[string]interface{}{"A": ""}, - }, v) + + for name, object := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{ + Filter: func(path string, e mapify.Element) (bool, error) { + return path == ".Nested" || path == ".Nested.A", nil + }, + } + // when + v, err := mapper.MapAny(object) + // then + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{ + "Nested": map[string]interface{}{"A": ""}, + }, v) + }) + } }) t.Run("should filter by slice element path", func(t *testing.T) { - s := []struct{ Field string }{ - {Field: "0"}, - {Field: "1"}, - } - mapper := mapify.Mapper{ - Filter: func(path string, e mapify.Element) (bool, error) { - return path == "[1].Field", nil + tests := map[string]interface{}{ + "struct": []struct{ Field string }{ + {Field: "0"}, + {Field: "1"}, + }, + "map": []map[string]string{ + {"Field": "0"}, + {"Field": "1"}, }, } - // when - v, err := mapper.MapAny(s) - // then - require.NoError(t, err) - expected := []map[string]interface{}{ - {}, - {"Field": s[1].Field}, + + for name, object := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{ + Filter: func(path string, e mapify.Element) (bool, error) { + return path == "[1].Field", nil + }, + } + // when + v, err := mapper.MapAny(object) + // then + require.NoError(t, err) + expected := []map[string]interface{}{ + {}, + {"Field": "1"}, + } + assert.Equal(t, expected, v) + }) } - assert.Equal(t, expected, v) }) t.Run("should filter by 2d slice element path", func(t *testing.T) { - s := [][]struct{ Field string }{ - { - {Field: "A0"}, - }, - { - {Field: "B0"}, - {Field: "B1"}, + tests := map[string]interface{}{ + "struct": [][]struct{ Field string }{ + { + {Field: "A0"}, + }, + { + {Field: "B0"}, + {Field: "B1"}, + }, }, - } - mapper := mapify.Mapper{ - Filter: func(path string, e mapify.Element) (bool, error) { - return path == "[1][1].Field", nil + "map": [][]map[string]string{ + { + {"Field": "A0"}, + }, + { + {"Field": "B0"}, + {"Field": "B1"}, + }, }, } - // when - v, err := mapper.MapAny(s) - // then - require.NoError(t, err) - expected := [][]map[string]interface{}{ - { - {}, - }, - { - {}, - {"Field": s[1][1].Field}, - }, + + for name, object := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{ + Filter: func(path string, e mapify.Element) (bool, error) { + return path == "[1][1].Field", nil + }, + } + // when + v, err := mapper.MapAny(object) + // then + require.NoError(t, err) + expected := [][]map[string]interface{}{ + { + {}, + }, + { + {}, + {"Field": "B1"}, + }, + } + assert.Equal(t, expected, v) + }) } - assert.Equal(t, expected, v) }) - t.Run("should filter by field name", func(t *testing.T) { - mapper := mapify.Mapper{ - Filter: func(path string, e mapify.Element) (bool, error) { - return e.Name() == "Field", nil - }, - } - // when - v, err := mapper.MapAny( - struct{ Field string }{ + t.Run("should filter by element name", func(t *testing.T) { + tests := map[string]interface{}{ + "struct": struct{ Field string }{ Field: "v", }, - ) - // then - require.NoError(t, err) - expected := map[string]interface{}{ - "Field": "v", + "map": map[string]string{ + "Field": "v", + }, + } + + for name, object := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{ + Filter: func(path string, e mapify.Element) (bool, error) { + return e.Name() == "Field", nil + }, + } + // when + v, err := mapper.MapAny(object) + // then + require.NoError(t, err) + expected := map[string]interface{}{ + "Field": "v", + } + assert.Equal(t, expected, v) + }) } - assert.Equal(t, expected, v) }) t.Run("should filter by value", func(t *testing.T) { - mapper := mapify.Mapper{ - Filter: func(path string, e mapify.Element) (bool, error) { - return e.String() == "keep it", nil - }, - } - // when - v, err := mapper.MapAny( - struct{ Field1, Field2 string }{ + tests := map[string]interface{}{ + "struct": struct{ Field1, Field2 string }{ Field1: "keep it", Field2: "omit this", }, - ) - // then - require.NoError(t, err) - expected := map[string]interface{}{ - "Field1": "keep it", + "map": map[string]string{ + "Field1": "keep it", + "Field2": "omit this", + }, + } + + for name, object := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{ + Filter: func(path string, e mapify.Element) (bool, error) { + return e.String() == "keep it", nil + }, + } + // when + v, err := mapper.MapAny(object) + // then + require.NoError(t, err) + expected := map[string]interface{}{ + "Field1": "keep it", + } + assert.Equal(t, expected, v) + }) } - assert.Equal(t, expected, v) }) t.Run("should return error when Filter returned error", func(t *testing.T) { - givenError := stringError("err") - mapper := mapify.Mapper{ - Filter: func(path string, e mapify.Element) (bool, error) { - return false, givenError - }, + tests := map[string]interface{}{ + "struct": struct{ Field string }{}, + "map": map[string]string{"Field": ""}, + } + + for name, object := range tests { + t.Run(name, func(t *testing.T) { + givenError := stringError("err") + mapper := mapify.Mapper{ + Filter: func(path string, e mapify.Element) (bool, error) { + return false, givenError + }, + } + // when + result, actualErr := mapper.MapAny(object) + // then + assert.Nil(t, result) + assert.ErrorIs(t, actualErr, givenError) + }) } - // when - result, actualErr := mapper.MapAny(struct{ Field string }{}) - // then - assert.Nil(t, result) - assert.ErrorIs(t, actualErr, givenError) }) } func TestRename(t *testing.T) { - t.Run("should rename struct field", func(t *testing.T) { - mapper := mapify.Mapper{ - Rename: func(path string, e mapify.Element) (string, error) { - return "newName", nil - }, - } - // when - v, err := mapper.MapAny( - struct{ OldName string }{ + t.Run("should rename element", func(t *testing.T) { + tests := map[string]interface{}{ + "struct": struct{ OldName string }{ OldName: "v", }, - ) - // then - require.NoError(t, err) - expected := map[string]interface{}{ - "newName": "v", + "map": map[string]string{ + "OldName": "v", + }, + } + for name, object := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{ + Rename: func(path string, e mapify.Element) (string, error) { + return "newName", nil + }, + } + // when + v, err := mapper.MapAny(object) + // then + require.NoError(t, err) + expected := map[string]interface{}{ + "newName": "v", + } + assert.Equal(t, expected, v) + }) } - assert.Equal(t, expected, v) }) t.Run("should return error when Rename returned error", func(t *testing.T) { - givenError := stringError("err") - mapper := mapify.Mapper{ - Rename: func(path string, e mapify.Element) (string, error) { - return e.Name(), givenError - }, + tests := map[string]interface{}{ + "struct": struct{ Field string }{}, + "map": map[string]string{"Field": ""}, + } + + for name, object := range tests { + t.Run(name, func(t *testing.T) { + givenError := stringError("err") + mapper := mapify.Mapper{ + Rename: func(path string, e mapify.Element) (string, error) { + return e.Name(), givenError + }, + } + // when + result, actualErr := mapper.MapAny(object) + // then + assert.Nil(t, result) + assert.ErrorIs(t, actualErr, givenError) + }) } - // when - result, actualErr := mapper.MapAny(struct{ Field string }{}) - // then - assert.Nil(t, result) - assert.ErrorIs(t, actualErr, givenError) }) } func TestMapValue(t *testing.T) { mappedValue := "str" + field2Value := 2 + + tests := map[string]interface{}{ + "struct": struct{ Field1, Field2 int }{ + Field1: 1, Field2: field2Value, + }, + "map": map[string]int{ + "Field1": 1, "Field2": field2Value, + }, + } t.Run("should map struct field", func(t *testing.T) { - mapper := mapify.Mapper{ - MapValue: func(path string, e mapify.Element) (interface{}, error) { - if e.Name() == "Field1" { - return mappedValue, nil + for name, object := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{ + MapValue: func(path string, e mapify.Element) (interface{}, error) { + if e.Name() == "Field1" { + return mappedValue, nil + } + + return e.Interface(), nil + }, } - - return e.Interface(), nil - }, - } - s := struct{ Field1, Field2 int }{ - Field1: 1, Field2: 2, - } - // when - v, err := mapper.MapAny(s) - // then - require.NoError(t, err) - expected := map[string]interface{}{ - "Field1": mappedValue, - "Field2": s.Field2, + // when + v, err := mapper.MapAny(object) + // then + require.NoError(t, err) + expected := map[string]interface{}{ + "Field1": mappedValue, + "Field2": field2Value, + } + assert.Equal(t, expected, v) + }) } - assert.Equal(t, expected, v) }) t.Run("should map struct field by path", func(t *testing.T) { - mapper := mapify.Mapper{ - MapValue: func(path string, e mapify.Element) (interface{}, error) { - if path == ".Field1" { - return mappedValue, nil + for name, object := range tests { + t.Run(name, func(t *testing.T) { + mapper := mapify.Mapper{ + MapValue: func(path string, e mapify.Element) (interface{}, error) { + if path == ".Field1" { + return mappedValue, nil + } + + return e.Interface(), nil + }, } - - return e.Interface(), nil - }, - } - s := struct{ Field1, Field2 int }{ - Field1: 1, Field2: 2, - } - // when - v, err := mapper.MapAny(s) - // then - require.NoError(t, err) - expected := map[string]interface{}{ - "Field1": mappedValue, - "Field2": s.Field2, + // when + v, err := mapper.MapAny(object) + // then + require.NoError(t, err) + expected := map[string]interface{}{ + "Field1": mappedValue, + "Field2": field2Value, + } + assert.Equal(t, expected, v) + }) } - assert.Equal(t, expected, v) }) t.Run("should return error when MapValue returned error", func(t *testing.T) { - givenError := stringError("err") - mapper := mapify.Mapper{ - MapValue: func(path string, e mapify.Element) (interface{}, error) { - return nil, givenError - }, + tests := map[string]interface{}{ + "struct": struct{ Field string }{}, + "map": map[string]string{"Field": ""}, + } + + for name, object := range tests { + t.Run(name, func(t *testing.T) { + givenError := stringError("err") + mapper := mapify.Mapper{ + MapValue: func(path string, e mapify.Element) (interface{}, error) { + return nil, givenError + }, + } + // when + result, actualErr := mapper.MapAny(object) + // then + assert.Nil(t, result) + assert.ErrorIs(t, actualErr, givenError) + }) } - // when - result, actualErr := mapper.MapAny(struct{ Field string }{}) - // then - assert.Nil(t, result) - assert.ErrorIs(t, actualErr, givenError) }) }