Skip to content

Commit

Permalink
✨ feat: structs - enhanced the SetValues() support set value for stru…
Browse files Browse the repository at this point in the history
…ct-ptr field
  • Loading branch information
inhere committed May 25, 2023
1 parent 657a56a commit 406a233
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 5 deletions.
11 changes: 7 additions & 4 deletions structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
package structs

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

// IsUnexported field name on struct
func IsUnexported(fieldName string) bool {
return fieldName[0] >= 'a' && fieldName[0] <= 'z'
func IsUnexported(name string) bool {
if name[0] == '_' {
return true
}
return name[0] >= 'a' && name[0] <= 'z'
}
2 changes: 1 addition & 1 deletion structs/structs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ func TestIsExported(t *testing.T) {
assert.True(t, structs.IsUnexported("name"))
assert.True(t, structs.IsUnexported("_name"))
assert.True(t, structs.IsUnexported("abc12"))
assert.True(t, structs.IsUnexported("123abcd"))
assert.False(t, structs.IsUnexported("123abcd"))
}
51 changes: 51 additions & 0 deletions structs/wrapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package structs

import "reflect"

// Wrapper struct for read or set field value TODO
type Wrapper struct {
// src any // source data struct
rv reflect.Value

// FieldTagName field name for read/write value. default tag: json
FieldTagName string
}

// Wrap create a struct wrapper
func Wrap(src any) *Wrapper {
return NewWrapper(src)
}

// NewWrapper create a struct wrapper
func NewWrapper(src any) *Wrapper {
return WrapValue(reflect.ValueOf(src))
}

// WrapValue create a struct wrapper
func WrapValue(rv reflect.Value) *Wrapper {
rv = reflect.Indirect(rv)
if rv.Kind() != reflect.Struct {
panic("must be provider an struct value")
}

return &Wrapper{rv: rv}
}

// Get field value by name
func (r *Wrapper) Get(name string) any {
val, ok := r.Lookup(name)
if !ok {
return nil
}
return val
}

// Lookup field value by name
func (r *Wrapper) Lookup(name string) (val any, ok bool) {
fv := r.rv.FieldByName(name)
if !fv.IsValid() {
return
}

return fv.Interface(), true
}
148 changes: 148 additions & 0 deletions structs/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package structs

import (
"errors"
"fmt"
"reflect"

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

// NewWriter create a struct writer
func NewWriter(ptr any) *Wrapper {
rv := reflect.ValueOf(ptr)
if rv.Kind() != reflect.Pointer {
panic("must be provider an pointer value")
}

return WrapValue(rv)
}

/*************************************************************
* set values to a struct
*************************************************************/

// SetOptFunc define
type SetOptFunc func(opt *SetOptions)

// SetOptions for set values to struct
type SetOptions struct {
// FieldTagName get field name for read value. default tag: json
FieldTagName string
// ValueHook before set value hook TODO
ValueHook func(val any) any

// ParseDefault init default value by DefaultValTag tag value.
// default: false
//
// see InitDefaults()
ParseDefault bool

// DefaultValTag name. tag: default
DefaultValTag string

// ParseDefaultEnv parse env var on default tag. eg: `default:"${APP_ENV}"`
//
// default: false
ParseDefaultEnv bool
}

// WithParseDefault value by tag "default"
func WithParseDefault(opt *SetOptions) {
opt.ParseDefault = true
}

// SetValues set values to struct ptr from map data.
//
// TIPS:
//
// Only support set: string, bool, intX, uintX, floatX
func SetValues(ptr any, data map[string]any, optFns ...SetOptFunc) error {
rv := reflect.ValueOf(ptr)
if rv.Kind() != reflect.Ptr {
return errors.New("must be provider an pointer value")
}

rv = rv.Elem()
if rv.Kind() != reflect.Struct {
return errors.New("must be provider an struct value")
}

opt := &SetOptions{
FieldTagName: defaultFieldTag,
DefaultValTag: defaultInitTag,
}

for _, fn := range optFns {
fn(opt)
}
return setValues(rv, data, opt)
}

func setValues(rv reflect.Value, data map[string]any, opt *SetOptions) error {
if len(data) == 0 {
return nil
}

rt := rv.Type()

for i := 0; i < rt.NumField(); i++ {
ft := rt.Field(i)
name := ft.Name
// skip don't exported field
if name[0] >= 'a' && name[0] <= 'z' {
continue
}

// get field name
tagVal, ok := ft.Tag.Lookup(opt.FieldTagName)
if ok {
info, err := ParseTagValueDefault(name, tagVal)
if err != nil {
return err
}
name = info.Get("name")
}

fv := rv.Field(i)
val, ok := data[name]

// set field value by default tag.
if !ok && opt.ParseDefault && fv.IsZero() {
defVal := ft.Tag.Get(opt.DefaultValTag)
if err := initDefaultValue(fv, defVal, opt.ParseDefaultEnv); err != nil {
return err
}
continue
}

// handle for pointer field
if fv.Kind() == reflect.Pointer {
if fv.IsNil() {
fv.Set(reflect.New(fv.Type().Elem()))
}
fv = fv.Elem()
}

// field is struct
if fv.Kind() == reflect.Struct {
asMp, err := maputil.TryAnyMap(val)
if err != nil {
return fmt.Errorf("must provide map data for field %q, err=%v", ft.Name, err)
}

if err := setValues(fv, asMp, opt); err != nil {
return err
}
continue
}

// set field value
if err := reflects.SetValue(fv, val); err != nil {
return err
}
}

return nil
}
149 changes: 149 additions & 0 deletions structs/writer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package structs_test

import (
"testing"

"github.com/gookit/goutil/dump"
"github.com/gookit/goutil/structs"
"github.com/gookit/goutil/testutil/assert"
)

func TestSetValues(t *testing.T) {
data := map[string]any{
"Name": "inhere",
"Age": 234,
"Tags": []string{"php", "go"},
"city": "chengdu",
}

type User struct {
Name string
Age int
Tags []string
city string
}

u := &User{}
err := structs.SetValues(u, data)
assert.NoErr(t, err)
assert.Eq(t, "inhere", u.Name)
assert.Eq(t, 234, u.Age)
assert.Eq(t, []string{"php", "go"}, u.Tags)
assert.Eq(t, "", u.city)
// dump.P(u)

err = structs.SetValues(u, nil)
assert.NoErr(t, err)
}

func TestSetValues_useFieldTag(t *testing.T) {
data := map[string]any{
"name": "inhere",
"age": 234,
"tags": []string{"php", "go"},
"city": "chengdu",
}

type User struct {
Name string `json:"name"`
Age int `json:"age"`
Tags []string `json:"tags"`
City string `json:"city"`
}

u := &User{}
err := structs.SetValues(u, data)
dump.P(u)
assert.NoErr(t, err)
assert.Eq(t, "inhere", u.Name)
assert.Eq(t, 234, u.Age)
assert.Eq(t, []string{"php", "go"}, u.Tags)
assert.Eq(t, "chengdu", u.City)

// test for ptr field
type User2 struct {
Name *string `json:"name"`
Age *int `json:"age"`
Tags []string `json:"tags"`
}

u2 := &User2{}
err = structs.SetValues(u2, data)
dump.P(u2)
assert.NoErr(t, err)
assert.Eq(t, "inhere", *u2.Name)
assert.Eq(t, 234, *u2.Age)
assert.Eq(t, []string{"php", "go"}, u2.Tags)
}

func TestSetValues_structField(t *testing.T) {
type Address struct {
City string `json:"city"`
}

data := map[string]any{
"name": "inhere",
"age": 234,
"address": map[string]any{
"city": "chengdu",
},
}

// test for struct field
t.Run("struct field", func(t *testing.T) {
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
}

u := &User{}
err := structs.SetValues(u, data)
dump.P(u)
assert.NoErr(t, err)
assert.Eq(t, "inhere", u.Name)
assert.Eq(t, 234, u.Age)
assert.Eq(t, "chengdu", u.Address.City)

// test for error data
assert.Err(t, structs.SetValues(u, map[string]any{
"address": "string",
}))
})

// test for struct ptr field
t.Run("struct ptr field", func(t *testing.T) {
type User2 struct {
Name string `json:"name"`
Age int `json:"age"`
Address *Address `json:"address"`
}

u2 := &User2{}
err := structs.SetValues(u2, data)
dump.P(u2)
assert.NoErr(t, err)
assert.Eq(t, "inhere", u2.Name)
})
}

func TestSetValues_useDefaultTag(t *testing.T) {
data := map[string]any{
"name": "inhere",
// "age": 234,
// "city": "chengdu",
}

type User struct {
Name string `json:"name"`
Age int `json:"age" default:"345"`
City string `json:"city" default:"shanghai"`
}

u := &User{}
err := structs.SetValues(u, data, structs.WithParseDefault)
assert.NoErr(t, err)
assert.Eq(t, "inhere", u.Name)
assert.Eq(t, 345, u.Age)
assert.Eq(t, "shanghai", u.City)
}

0 comments on commit 406a233

Please sign in to comment.