mirror of
https://github.com/retailcrm/mg-transport-core.git
synced 2025-04-04 22:03:33 +03:00
Merge pull request #94 from Neur0toxine/map-extract
map value extractor for testutil
This commit is contained in:
commit
da40710ae2
5 changed files with 249 additions and 3 deletions
|
@ -140,7 +140,7 @@ linters-settings:
|
|||
default-signifies-exhaustive: false
|
||||
funlen:
|
||||
lines: 65
|
||||
statements: 40
|
||||
statements: 50
|
||||
gocognit:
|
||||
min-complexity: 25
|
||||
gocyclo:
|
||||
|
|
138
core/util/testutil/map_extract.go
Normal file
138
core/util/testutil/map_extract.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// MapValue extracts nested map values using dot notation. Keys are separated by dots, slices and arrays can be
|
||||
// accessed by integer indexes.
|
||||
// Example:
|
||||
//
|
||||
// MapValue(m, "key1") // Access value with key "key1"
|
||||
// MapValue(m, "key1.key2") // Access nested value with key "key2"
|
||||
// MapValue(m, "key1.key2.key3") // Access nested value with key "key3"
|
||||
// MapValue(m, "key1.key2.key3.0") // Access the first slice / array element in the nested map.
|
||||
func MapValue(data interface{}, path string) (interface{}, error) {
|
||||
if path == "" {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(path, ".")
|
||||
current := data
|
||||
|
||||
for i, part := range parts {
|
||||
v := reflect.ValueOf(current)
|
||||
|
||||
if v.Kind() == reflect.Map {
|
||||
converted, err := convertKeyToKind(part, v.Type().Key().Kind())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyValue := reflect.ValueOf(converted)
|
||||
valueValue := v.MapIndex(keyValue)
|
||||
|
||||
if !valueValue.IsValid() {
|
||||
return nil, fmt.Errorf("key '%s' not found at path '%s'",
|
||||
part, strings.Join(parts[:i], "."))
|
||||
}
|
||||
|
||||
current = valueValue.Interface()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
|
||||
index, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("'%s' is not a valid slice / array index", part)
|
||||
}
|
||||
|
||||
if index < 0 || index >= v.Len() {
|
||||
return nil, fmt.Errorf("index %d out of bounds for %s of length %d at path '%s'",
|
||||
index, v.Kind().String(), v.Len(), strings.Join(parts[:i], "."))
|
||||
}
|
||||
|
||||
current = v.Index(index).Interface()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("value at path '%s' is not a map, slice or array",
|
||||
strings.Join(parts[:i], "."))
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
// AssertMapValue is the MapValue variant useful in tests.
|
||||
func AssertMapValue(t *testing.T, data interface{}, path string) interface{} {
|
||||
val, err := MapValue(data, path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// MustMapValue is the same as MapValue but it panics in case of error.
|
||||
func MustMapValue(data interface{}, path string) interface{} {
|
||||
val, err := MapValue(data, path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// convertKeyToKind converts a string to the given kind.
|
||||
func convertKeyToKind(part string, kind reflect.Kind) (interface{}, error) {
|
||||
switch kind { //nolint:exhaustive
|
||||
case reflect.String:
|
||||
return part, nil
|
||||
case reflect.Bool:
|
||||
return part == "true" || part == "1", nil
|
||||
case reflect.Int:
|
||||
return strconv.Atoi(part)
|
||||
case reflect.Int8:
|
||||
val, err := strconv.ParseInt(part, 10, 8)
|
||||
return int8(val), err
|
||||
case reflect.Int16:
|
||||
val, err := strconv.ParseInt(part, 10, 16)
|
||||
return int16(val), err
|
||||
case reflect.Int32:
|
||||
val, err := strconv.ParseInt(part, 10, 32)
|
||||
return int32(val), err
|
||||
case reflect.Int64:
|
||||
val, err := strconv.ParseInt(part, 10, 64)
|
||||
return val, err
|
||||
case reflect.Uint:
|
||||
val, err := strconv.ParseUint(part, 10, 32)
|
||||
return uint(val), err
|
||||
case reflect.Uint8:
|
||||
val, err := strconv.ParseUint(part, 10, 8)
|
||||
return uint8(val), err
|
||||
case reflect.Uint16:
|
||||
val, err := strconv.ParseUint(part, 10, 16)
|
||||
return uint16(val), err
|
||||
case reflect.Uint32:
|
||||
val, err := strconv.ParseUint(part, 10, 32)
|
||||
return uint32(val), err
|
||||
case reflect.Uint64:
|
||||
val, err := strconv.ParseUint(part, 10, 64)
|
||||
return val, err
|
||||
case reflect.Uintptr:
|
||||
val, err := strconv.ParseUint(part, 10, 64)
|
||||
return uintptr(val), err
|
||||
case reflect.Float32:
|
||||
val, err := strconv.ParseFloat(strings.Replace(part, ",", ".", 1), 32)
|
||||
return float32(val), err
|
||||
case reflect.Float64:
|
||||
val, err := strconv.ParseFloat(strings.Replace(part, ",", ".", 1), 64)
|
||||
return val, err
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported reflect.Kind: %s", kind)
|
||||
}
|
||||
}
|
108
core/util/testutil/map_extract_test.go
Normal file
108
core/util/testutil/map_extract_test.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMapValue_DifferentKeyTypes(t *testing.T) {
|
||||
mString := map[string]interface{}{"key": 1}
|
||||
mInt := map[int]interface{}{2: 1}
|
||||
mInt8 := map[int8]int{2: 1}
|
||||
mInt16 := map[int16]int{2: 1}
|
||||
mInt32 := map[int32]int{2: 1}
|
||||
mInt64 := map[int64]int{2: 1}
|
||||
mUInt := map[uint]interface{}{2: 1}
|
||||
mUInt8 := map[uint8]int{2: 1}
|
||||
mUInt16 := map[uint16]int{2: 1}
|
||||
mUInt32 := map[uint32]int{2: 1}
|
||||
mUInt64 := map[uint64]int{2: 1}
|
||||
mUIntptr := map[uintptr]int{2: 1}
|
||||
mFloat32 := map[float32]int{2.5: 1}
|
||||
mFloat64 := map[float64]int{2.5: 1}
|
||||
|
||||
assert.Equal(t, 1, MustMapValue(mString, "key").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mInt, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mInt8, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mInt16, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mInt32, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mInt64, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mUInt, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mUInt8, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mUInt16, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mUInt32, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mUInt64, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mUIntptr, "2").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mFloat32, "2,5").(int))
|
||||
assert.Equal(t, 1, MustMapValue(mFloat64, "2,5").(int))
|
||||
}
|
||||
|
||||
func TestMapValue_ErrorUnsupportedKeyType(t *testing.T) {
|
||||
_, err := MapValue(map[complex64]interface{}{}, "key")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "unsupported reflect.Kind: complex64", err.Error())
|
||||
}
|
||||
|
||||
func TestMapValue_Nested(t *testing.T) {
|
||||
assert.Equal(t, "value", MustMapValue(map[string]map[string]interface{}{
|
||||
"key1": {
|
||||
"key2": "value",
|
||||
},
|
||||
}, "key1.key2").(string))
|
||||
|
||||
m := map[string]interface{}{
|
||||
"key1": map[string]map[string]interface{}{
|
||||
"key2": {
|
||||
"key3": "value",
|
||||
},
|
||||
},
|
||||
"key4": []interface{}{
|
||||
"value5",
|
||||
map[string]interface{}{
|
||||
"key6": "value7",
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "value", MustMapValue(m, "key1.key2.key3").(string))
|
||||
assert.Equal(t, "value", MustMapValue(m, "key1.key2").(map[string]interface{})["key3"].(string))
|
||||
assert.Equal(t, "value5", MustMapValue(m, "key4.0"))
|
||||
assert.Equal(t, "value7", MustMapValue(m, "key4.1.key6"))
|
||||
assert.Equal(t, "value", MustMapValue([]map[string]string{
|
||||
{"key": "value"},
|
||||
}, "0.key"))
|
||||
}
|
||||
|
||||
func TestMapValue_ErrorNotAMap(t *testing.T) {
|
||||
_, err := MapValue(1, "key")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "value at path '' is not a map, slice or array", err.Error())
|
||||
|
||||
_, err = MapValue(map[string]int{"key": 1}, "key.key2")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "value at path 'key' is not a map, slice or array", err.Error())
|
||||
}
|
||||
|
||||
func TestMapValue_ErrorKeyNotFound(t *testing.T) {
|
||||
_, err := MapValue(map[string]int{"key": 1}, "key2")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "key 'key2' not found at path ''", err.Error())
|
||||
|
||||
_, err = MapValue(map[string]map[string]int{"key": {"key2": 1}}, "key.key3")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "key 'key3' not found at path 'key'", err.Error())
|
||||
}
|
||||
|
||||
func TestMapValue_ErrorOutOfBounds(t *testing.T) {
|
||||
_, err := MapValue(map[string][]int{"key": {1}}, "key.1")
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "index 1 out of bounds for slice of length 1 at path 'key'", err.Error())
|
||||
}
|
||||
|
||||
func TestMustMapValue_Panics(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
MustMapValue(map[string]int{"key": 1}, "key2")
|
||||
})
|
||||
}
|
2
go.mod
2
go.mod
|
@ -92,4 +92,4 @@ require (
|
|||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
)
|
2
go.sum
2
go.sum
|
@ -625,4 +625,4 @@ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYm
|
|||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
Loading…
Add table
Reference in a new issue