From 7d83d70c21f3eb48a8cb504045da34534210bb3a Mon Sep 17 00:00:00 2001 From: shockerli Date: Sun, 28 Nov 2021 17:52:53 +0800 Subject: [PATCH] Add func `StringMapE` #2 * Support JSON string of map * Support any `map` type * Support any `struct` type --- README.md | 24 ++++++++++++++ README_ZH.md | 24 ++++++++++++++ cvte.go | 9 ++++++ cvte_test.go | 3 -- map.go | 56 +++++++++++++++++++++++++++++++++ map_test.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 map.go create mode 100644 map_test.go diff --git a/README.md b/README.md index 09f12d0..2b98eb3 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,30 @@ cvt.SliceStringE(map[int]string{2: "222", 1: "11.1"}) // []string{"11.1", "222 > more case see [slice_test.go](slice_test.go) +### map +- StringMapE + +```go +// JSON +// expect: map[string]interface{}{"name": "cvt", "age": 3.21} +cvt.StringMapE(`{"name":"cvt","age":3.21}`) + +// Map +// expect: map[string]interface{}{"111": "cvt", "222": 3.21} +cvt.StringMapE(map[interface{}]interface{}{111: "cvt", "222": 3.21}) + +// Struct +// expect: map[string]interface{}{"Name": "cvt", "Age": 3} +cvt.StringMapE(struct { + Name string + Age int +}{"cvt", 3}) +``` + +> more case see [map_test.go](map_test.go) + + + ## License This project is under the terms of the [MIT](LICENSE) license. diff --git a/README_ZH.md b/README_ZH.md index 5df4ad8..bac8a7e 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -320,6 +320,30 @@ cvt.SliceStringE(map[int]string{2: "222", 1: "11.1"}) // []string{"11.1", "222 > 更多示例: [slice_test.go](slice_test.go) +### map +- StringMapE + +```go +// JSON +// expect: map[string]interface{}{"name": "cvt", "age": 3.21} +cvt.StringMapE(`{"name":"cvt","age":3.21}`) + +// Map +// expect: map[string]interface{}{"111": "cvt", "222": 3.21} +cvt.StringMapE(map[interface{}]interface{}{111: "cvt", "222": 3.21}) + +// Struct +// expect: map[string]interface{}{"Name": "cvt", "Age": 3} +cvt.StringMapE(struct { + Name string + Age int +}{"cvt", 3}) +``` + +> 更多示例: [map_test.go](map_test.go) + + + ## 开源协议 本项目基于 [MIT](LICENSE) 协议开放源代码。 diff --git a/cvte.go b/cvte.go index c654272..8c157ef 100644 --- a/cvte.go +++ b/cvte.go @@ -109,6 +109,15 @@ func ptrType(rt reflect.Type) reflect.Type { return rt } +func ptrValue(rv reflect.Value) reflect.Value { + if rv.Kind() == reflect.Ptr { + for rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + } + return rv +} + // returns the value with base type func indirect(a interface{}) (val interface{}, rv reflect.Value) { if a == nil { diff --git a/cvte_test.go b/cvte_test.go index a436c3e..c323a37 100644 --- a/cvte_test.go +++ b/cvte_test.go @@ -191,7 +191,6 @@ func assertError(t *testing.T, err error, msgAndArgs ...interface{}) { fail(t, "An error is expected but got nil", msgAndArgs...) return } - return } // assert no error @@ -200,7 +199,6 @@ func assertNoError(t *testing.T, err error, msgAndArgs ...interface{}) { fail(t, fmt.Sprintf("Received unexpected error:\n\t\t\t\t%+v", err), msgAndArgs...) return } - return } // assert equal @@ -217,7 +215,6 @@ func assertEqual(t *testing.T, expected, actual interface{}, msgAndArgs ...inter "actual : %s", expected, actual), msgAndArgs...) return } - return } func fail(t *testing.T, failureMessage string, msgAndArgs ...interface{}) { diff --git a/map.go b/map.go new file mode 100644 index 0000000..1eafb81 --- /dev/null +++ b/map.go @@ -0,0 +1,56 @@ +package cvt + +import ( + "encoding/json" + "reflect" +) + +// StringMapE convert an interface to `map[string]interface{}` +// * Support JSON string of map +// * Support any `map` type +// * Support any `struct` type +func StringMapE(val interface{}) (m map[string]interface{}, err error) { + m = make(map[string]interface{}) + if val == nil { + return nil, errUnsupportedTypeNil + } + + _, rv := indirect(val) + switch rv.Kind() { + case reflect.Map: + for _, key := range rv.MapKeys() { + m[String(key.Interface())] = rv.MapIndex(key).Interface() + } + case reflect.Struct: + m = struct2map(rv) + case reflect.String: + // JSON string of map + err = json.Unmarshal([]byte(rv.String()), &m) + } + + return +} + +func struct2map(rv reflect.Value) map[string]interface{} { + var m = make(map[string]interface{}) + if !rv.IsValid() { + return m + } + + for j := 0; j < rv.NumField(); j++ { + f := rv.Type().Field(j) + t := ptrType(f.Type) + vv := ptrValue(rv.Field(j)) + if f.Anonymous && t.Kind() == reflect.Struct { + for k, v := range struct2map(vv) { + // anonymous sub-field has a low priority + if _, ok := m[k]; !ok { + m[k] = v + } + } + } else if vv.IsValid() && vv.CanInterface() { + m[f.Name] = vv.Interface() + } + } + return m +} diff --git a/map_test.go b/map_test.go new file mode 100644 index 0000000..2e669dc --- /dev/null +++ b/map_test.go @@ -0,0 +1,88 @@ +package cvt_test + +import ( + "fmt" + "github.com/shockerli/cvt" + "testing" +) + +func TestStringMapE(t *testing.T) { + tests := []struct { + input interface{} + expect map[string]interface{} + isErr bool + }{ + // JSON String + {`{"name":"cvt","age":3.21}`, map[string]interface{}{"name": "cvt", "age": 3.21}, false}, + {`{"name":"cvt","tag":"convert"}`, map[string]interface{}{"name": "cvt", "tag": "convert"}, false}, + {`{"name":"cvt","build":true}`, map[string]interface{}{"name": "cvt", "build": true}, false}, + + // Map + {map[string]interface{}{}, map[string]interface{}{}, false}, + {map[string]interface{}{"name": "cvt", "age": 3.21}, map[string]interface{}{"name": "cvt", "age": 3.21}, false}, + {map[interface{}]interface{}{"name": "cvt", "age": 3.21}, map[string]interface{}{"name": "cvt", "age": 3.21}, false}, + {map[interface{}]interface{}{111: "cvt", "222": 3.21}, map[string]interface{}{"111": "cvt", "222": 3.21}, false}, + + // Struct + {struct { + Name string + Age int + }{"cvt", 3}, map[string]interface{}{"Name": "cvt", "Age": 3}, false}, + {&struct { + Name string + Age int + }{"cvt", 3}, map[string]interface{}{"Name": "cvt", "Age": 3}, false}, + {struct { + A1 string + TestStructC + }{"a1", TestStructC{"c1"}}, map[string]interface{}{"A1": "a1", "C1": "c1"}, false}, + {struct { + A1 string + TestStructC + C1 string + }{"a1", TestStructC{"c1-1"}, "c1-2"}, map[string]interface{}{"A1": "a1", "C1": "c1-2"}, false}, + {struct { + A1 string + *TestStructC + C1 string + }{"a1", &TestStructC{"c1-1"}, "c1-2"}, map[string]interface{}{"A1": "a1", "C1": "c1-2"}, false}, + {struct { + C1 string + *TestStructC + A1 string + }{"c1-1", &TestStructC{"c1-2"}, "a1"}, map[string]interface{}{"A1": "a1", "C1": "c1-1"}, false}, + {struct { + AliasTypeInt8 + }{5}, map[string]interface{}{"AliasTypeInt8": AliasTypeInt8(5)}, false}, + {struct { + *AliasTypeInt + }{&aliasTypeInt0}, map[string]interface{}{"AliasTypeInt": aliasTypeInt0}, false}, + {struct { + *AliasTypeInt + }{}, map[string]interface{}{}, false}, + {struct { + *TestStructC + }{}, map[string]interface{}{}, false}, + + // errors + {nil, nil, true}, + {"", nil, true}, + {"hello", nil, true}, + } + + for i, tt := range tests { + msg := fmt.Sprintf( + "i = %d, input[%+v], expect[%+v], isErr[%v]", + i, tt.input, tt.expect, tt.isErr, + ) + + v, err := cvt.StringMapE(tt.input) + if tt.isErr { + assertError(t, err, "[HasErr] "+msg) + continue + } + + assertNoError(t, err, "[NoErr] "+msg) + assertEqual(t, tt.expect, v, "[WithE] "+msg) + } +}