-
Notifications
You must be signed in to change notification settings - Fork 191
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ feat: structs - enhanced the SetValues() support set value for stru…
…ct-ptr field
- Loading branch information
Showing
5 changed files
with
356 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |