Skip to content

Commit

Permalink
✨ feat: structs add new util func ToSMap(), TryToSMap(), TryToSMap()
Browse files Browse the repository at this point in the history
- add new map option MergeAnonymous, ExportPrivate
  • Loading branch information
inhere committed May 18, 2023
1 parent f8d29b2 commit 6886bd7
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 42 deletions.
98 changes: 77 additions & 21 deletions structs/convert.go
Expand Up @@ -6,6 +6,7 @@ import (
"reflect"

"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/reflects"
)

// ToMap quickly convert structs to map by reflect
Expand All @@ -28,7 +29,31 @@ func TryToMap(st any, optFns ...MapOptFunc) (map[string]any, error) {
return StructToMap(st, optFns...)
}

// ToString format
// ToSMap quickly and safe convert structs to map[string]string by reflect
func ToSMap(st any, optFns ...MapOptFunc) map[string]string {
mp, _ := StructToMap(st, optFns...)
return maputil.ToStringMap(mp)
}

// TryToSMap quickly convert structs to map[string]string by reflect
func TryToSMap(st any, optFns ...MapOptFunc) (map[string]string, error) {
mp, err := StructToMap(st, optFns...)
if err != nil {
return nil, err
}
return maputil.ToStringMap(mp), nil
}

// MustToSMap alias of ToStringMap(), but will panic on error
func MustToSMap(st any, optFns ...MapOptFunc) map[string]string {
mp, err := StructToMap(st, optFns...)
if err != nil {
panic(err)
}
return maputil.ToStringMap(mp)
}

// ToString quickly format struct to string
func ToString(st any, optFns ...MapOptFunc) string {
mp, err := StructToMap(st, optFns...)
if err == nil {
Expand All @@ -39,14 +64,38 @@ func ToString(st any, optFns ...MapOptFunc) string {

const defaultFieldTag = "json"

// MapOptions struct
// MapOptions for convert struct to map
type MapOptions struct {
// TagName for map filed. default is "json"
TagName string
// ParseDepth for parse. TODO support depth
ParseDepth int
// MergeAnonymous struct fields to parent map. default is true
MergeAnonymous bool
// ExportPrivate export private fields. default is false
ExportPrivate bool
}

// MapOptFunc define
type MapOptFunc func(opt *MapOptions)

// WithMapTagName set tag name for map field
func WithMapTagName(tagName string) MapOptFunc {
return func(opt *MapOptions) {
opt.TagName = tagName
}
}

// MergeAnonymous merge anonymous struct fields to parent map
func MergeAnonymous(opt *MapOptions) {
opt.MergeAnonymous = true
}

// ExportPrivate merge anonymous struct fields to parent map
func ExportPrivate(opt *MapOptions) {
opt.ExportPrivate = true
}

// StructToMap quickly convert structs to map[string]any by reflect.
// Can custom export field name by tag `json` or custom tag
func StructToMap(st any, optFns ...MapOptFunc) (map[string]any, error) {
Expand All @@ -55,11 +104,7 @@ func StructToMap(st any, optFns ...MapOptFunc) (map[string]any, error) {
return mp, nil
}

obj := reflect.ValueOf(st)
if obj.Kind() == reflect.Ptr {
obj = obj.Elem()
}

obj := reflect.Indirect(reflect.ValueOf(st))
if obj.Kind() != reflect.Struct {
return mp, errors.New("must be an struct value")
}
Expand All @@ -69,48 +114,59 @@ func StructToMap(st any, optFns ...MapOptFunc) (map[string]any, error) {
fn(opt)
}

mp, err := structToMap(obj, opt.TagName)
_, err := structToMap(obj, opt, mp)
return mp, err
}

func structToMap(obj reflect.Value, tagName string) (map[string]any, error) {
refType := obj.Type()
mp := make(map[string]any)
func structToMap(obj reflect.Value, opt *MapOptions, mp map[string]any) (map[string]any, error) {
if mp == nil {
mp = make(map[string]any)
}

refType := obj.Type()
for i := 0; i < obj.NumField(); i++ {
ft := refType.Field(i)
name := ft.Name
// skip don't exported field
if name[0] >= 'a' && name[0] <= 'z' {
// skip un-exported field
if !opt.ExportPrivate && IsUnexported(name) {
continue
}

tagVal, ok := ft.Tag.Lookup(tagName)
tagVal, ok := ft.Tag.Lookup(opt.TagName)
if ok && tagVal != "" {
sMap, err := ParseTagValueDefault(name, tagVal)
if err != nil {
return nil, err
}

name = sMap.Default("name", name)
// un-exported field
if name == "" {
if name == "" { // un-exported field
continue
}
}

field := obj.Field(i)
field := reflect.Indirect(obj.Field(i))
if field.Kind() == reflect.Struct {
sub, err := structToMap(field, tagName)
if err != nil {
return nil, err
// collect anonymous struct values to parent.
if ft.Anonymous && opt.MergeAnonymous {
_, err := structToMap(field, opt, mp)
if err != nil {
return nil, err
}
} else { // collect struct values to submap
sub, err := structToMap(field, opt, nil)
if err != nil {
return nil, err
}
mp[name] = sub
}
mp[name] = sub
continue
}

if field.CanInterface() {
mp[name] = field.Interface()
} else if field.CanAddr() { // for unexported field
mp[name] = reflects.UnexportedValue(field)
}
}

Expand Down
134 changes: 119 additions & 15 deletions structs/convert_test.go
Expand Up @@ -41,10 +41,32 @@ func TestTryToMap(t *testing.T) {
assert.NotEmpty(t, mp)
// dump.P(mp)

// test to string map
smp := structs.MustToSMap(&u)
assert.NotEmpty(t, smp)
assert.ContainsKeys(t, smp, []string{"Name", "Age"})

smp = structs.ToSMap(&u)
assert.NotEmpty(t, smp)
assert.ContainsKeys(t, smp, []string{"Name", "Age"})

_, err = structs.TryToSMap("invalid")
assert.Err(t, err)

smp, err = structs.TryToSMap(&u)
assert.NoErr(t, err)
assert.NotEmpty(t, smp)
assert.ContainsKeys(t, smp, []string{"Name", "Age"})

assert.NotEmpty(t, structs.ToString(&u))

// test error
assert.Panics(t, func() {
structs.MustToMap("abc")
})

assert.Panics(t, func() {
structs.MustToSMap("abc")
})
}

func TestToMap_useTag(t *testing.T) {
Expand All @@ -64,20 +86,84 @@ func TestToMap_useTag(t *testing.T) {
dump.P(mp)
assert.ContainsKeys(t, mp, []string{"name", "age"})
assert.NotContains(t, mp, "city")

// export unexported field
mp = structs.MustToMap(u1, structs.ExportPrivate)
dump.P(mp)
assert.ContainsKeys(t, mp, []string{"name", "age", "city"})
}

type Extra struct {
City string `json:"city"`
Github string `json:"github"`
}

type Extra1 struct {
ExtraSub
City string `json:"city"`
Github string `json:"github"`
}

type ExtraSub struct {
ESubKey string `json:"e_sub_key"`
}

type User struct {
Extra `json:"extra"`
Name string `json:"name"`
Age int `json:"age"`
}

type User1 struct {
Extra
Name string `json:"name"`
Age int `json:"age"`
}

type User2 struct {
Extra1
Name string `json:"name"`
Age int `json:"age"`
}

func TestToMap_nestStruct(t *testing.T) {
type Extra struct {
City string `json:"city"`
Github string `json:"github"`
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Extra Extra `json:"extra"`
e := Extra{
City: "chengdu",
Github: "https://github.com/inhere",
}

u := &User{
Name: "inhere",
Age: 30,
Extra: e,
}

mp := structs.MustToMap(u)
dump.P(mp)
assert.ContainsKeys(t, mp, []string{"name", "age", "extra"})
assert.ContainsKeys(t, mp["extra"], []string{"city", "github"})

// use pointer
type UserPtrSub struct {
*Extra `json:"extra"`
Name string `json:"name"`
Age int `json:"age"`
}

u2 := &UserPtrSub{
Name: "inhere",
Age: 30,
Extra: &e,
}

mp = structs.MustToMap(u2)
dump.P(mp)
assert.ContainsKeys(t, mp, []string{"name", "age", "extra"})
assert.ContainsKeys(t, mp["extra"], []string{"city", "github"})
}

func TestToMap_anonymousStruct(t *testing.T) {
u := &User1{
Name: "inhere",
Age: 30,
Extra: Extra{
Expand All @@ -86,10 +172,30 @@ func TestToMap_nestStruct(t *testing.T) {
},
}

mp := structs.MustToMap(u)
mp := structs.MustToMap(u, structs.MergeAnonymous)
dump.P(mp)
assert.ContainsKeys(t, mp, []string{"name", "age", "extra"})
assert.ContainsKeys(t, mp["extra"], []string{"city", "github"})

assert.ContainsKeys(t, mp, []string{"name", "age", "city", "github"})
assert.NotContainsKey(t, mp, "extra")
assert.NotContainsKeys(t, mp, []string{"extra"})

u2 := &User2{
Name: "inhere",
Age: 30,
Extra1: Extra1{
ExtraSub: ExtraSub{
ESubKey: "sub key",
},
City: "chengdu",
Github: "https://github.com/inhere",
},
}

mp = structs.MustToMap(u2, structs.MergeAnonymous)
dump.P(mp)

assert.ContainsKeys(t, mp, []string{"name", "age", "city", "github", "e_sub_key"})
assert.NotContainsKey(t, mp, "extra")
}

func TestTryToMap_customTag(t *testing.T) {
Expand All @@ -105,9 +211,7 @@ func TestTryToMap_customTag(t *testing.T) {
FullName: "inhere xyz",
}

mp, err := structs.TryToMap(u1, func(opt *structs.MapOptions) {
opt.TagName = "export"
})
mp, err := structs.TryToMap(u1, structs.WithMapTagName("export"))
assert.NoErr(t, err)
assert.NotEmpty(t, mp)

Expand Down
7 changes: 1 addition & 6 deletions structs/structs.go
@@ -1,17 +1,12 @@
// Package structs Provide some extends util functions for struct. eg: tag parse, struct init, value set
package structs

// MapStruct simple copy src struct value to dst struct
// func MapStruct(srcSt, dstSt any) {
// // TODO
// }

// IsExported field name on struct
func IsExported(fieldName string) bool {
return fieldName[0] >= 'A' && fieldName[0] <= 'Z'
}

// IsUnexported field name on struct
func IsUnexported(fieldName string) bool {
return !IsExported(fieldName)
return fieldName[0] >= 'a' && fieldName[0] <= 'z'
}

0 comments on commit 6886bd7

Please sign in to comment.