mirror of
https://github.com/duke-git/lancet.git
synced 2026-02-04 12:52:28 +08:00
Merge branch 'main' into v2
This commit is contained in:
0
docs/structutil/field.md
Normal file
0
docs/structutil/field.md
Normal file
0
docs/structutil/struct.md
Normal file
0
docs/structutil/struct.md
Normal file
14
pointer/pointer.go
Normal file
14
pointer/pointer.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package pointer
|
||||
|
||||
import "reflect"
|
||||
|
||||
// ExtractPointer returns the underlying value by the given interface type
|
||||
func ExtractPointer(value any) any {
|
||||
t := reflect.TypeOf(value)
|
||||
v := reflect.ValueOf(value)
|
||||
|
||||
if t.Kind() != reflect.Pointer {
|
||||
return value
|
||||
}
|
||||
return ExtractPointer(v.Elem().Interface())
|
||||
}
|
||||
18
pointer/pointer_test.go
Normal file
18
pointer/pointer_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package pointer
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/internal"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractPointer(t *testing.T) {
|
||||
|
||||
assert := internal.NewAssert(t, "TestExtractPointer")
|
||||
|
||||
a := 1
|
||||
b := &a
|
||||
c := &b
|
||||
d := &c
|
||||
|
||||
assert.Equal(1, ExtractPointer(d))
|
||||
}
|
||||
7
structutil/error.go
Normal file
7
structutil/error.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package structutil
|
||||
|
||||
import "fmt"
|
||||
|
||||
func ErrInvalidStruct(v any) error {
|
||||
return fmt.Errorf("invalid struct %v", v)
|
||||
}
|
||||
@@ -1,20 +1,25 @@
|
||||
package structutil
|
||||
|
||||
import "reflect"
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/pointer"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Field struct {
|
||||
value reflect.Value
|
||||
Struct
|
||||
field reflect.StructField
|
||||
tag *Tag
|
||||
}
|
||||
|
||||
func newField(v reflect.Value, f reflect.StructField, tagName string) *Field {
|
||||
tag := f.Tag.Get(tagName)
|
||||
return &Field{
|
||||
value: v,
|
||||
field := &Field{
|
||||
field: f,
|
||||
tag: newTag(tag),
|
||||
}
|
||||
field.rvalue = v
|
||||
field.TagName = tagName
|
||||
return field
|
||||
}
|
||||
|
||||
// Tag returns the value that the key in the tag string.
|
||||
@@ -24,22 +29,22 @@ func (f *Field) Tag() *Tag {
|
||||
|
||||
// Value returns the underlying value of the field.
|
||||
func (f *Field) Value() any {
|
||||
return f.value.Interface()
|
||||
return f.rvalue.Interface()
|
||||
}
|
||||
|
||||
// IsEmbedded returns true if the given field is an embedded field.
|
||||
func (f *Field) IsEmbedded() bool {
|
||||
return f.field.Anonymous
|
||||
return len(f.field.Index) > 1
|
||||
}
|
||||
|
||||
// IsExported returns true if the given field is exported.
|
||||
func (f *Field) IsExported() bool {
|
||||
return f.field.PkgPath == ""
|
||||
return f.field.IsExported()
|
||||
}
|
||||
|
||||
// IsZero returns true if the given field is zero value.
|
||||
func (f *Field) IsZero() bool {
|
||||
z := reflect.Zero(f.value.Type()).Interface()
|
||||
z := reflect.Zero(f.rvalue.Type()).Interface()
|
||||
v := f.Value()
|
||||
return reflect.DeepEqual(z, v)
|
||||
}
|
||||
@@ -51,5 +56,52 @@ func (f *Field) Name() string {
|
||||
|
||||
// Kind returns the field's kind
|
||||
func (f *Field) Kind() reflect.Kind {
|
||||
return f.value.Kind()
|
||||
return f.rvalue.Kind()
|
||||
}
|
||||
|
||||
func (f *Field) IsSlice() bool {
|
||||
k := f.rvalue.Kind()
|
||||
return k == reflect.Slice
|
||||
}
|
||||
|
||||
func (f *Field) MapValue(value any) any {
|
||||
val := pointer.ExtractPointer(value)
|
||||
v := reflect.ValueOf(val)
|
||||
var ret any
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
s := New(val)
|
||||
s.TagName = f.TagName
|
||||
m, _ := s.ToMap()
|
||||
ret = m
|
||||
case reflect.Map:
|
||||
mapEl := v.Type().Elem()
|
||||
switch mapEl.Kind() {
|
||||
case reflect.Ptr, reflect.Array, reflect.Map, reflect.Slice, reflect.Chan:
|
||||
// iterate the map
|
||||
m := make(map[string]any, v.Len())
|
||||
for _, key := range v.MapKeys() {
|
||||
m[key.String()] = f.MapValue(v.MapIndex(key).Interface())
|
||||
}
|
||||
ret = m
|
||||
default:
|
||||
ret = v.Interface()
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
sEl := v.Type().Elem()
|
||||
switch sEl.Kind() {
|
||||
case reflect.Ptr, reflect.Array, reflect.Map, reflect.Slice, reflect.Chan:
|
||||
slices := make([]any, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
slices[i] = f.MapValue(v.Index(i).Interface())
|
||||
}
|
||||
ret = slices
|
||||
default:
|
||||
ret = v.Interface()
|
||||
}
|
||||
default:
|
||||
ret = v.Interface()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
272
structutil/field_test.go
Normal file
272
structutil/field_test.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package structutil
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/internal"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestField_Tag(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestField_Tag")
|
||||
|
||||
type Parent struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
p1 := &Parent{"111"}
|
||||
|
||||
s := New(p1)
|
||||
n, _ := s.Field("Name")
|
||||
tag := n.Tag()
|
||||
assert.Equal("name", tag.Name)
|
||||
assert.Equal(true, tag.HasOption("omitempty"))
|
||||
}
|
||||
|
||||
func TestField_Value(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestField_Value")
|
||||
|
||||
type Parent struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
p1 := &Parent{"111"}
|
||||
|
||||
s := New(p1)
|
||||
n, _ := s.Field("Name")
|
||||
|
||||
assert.Equal("111", n.Value())
|
||||
}
|
||||
|
||||
func TestField_IsEmbedded(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestField_IsEmbedded")
|
||||
type Parent struct {
|
||||
Name string
|
||||
}
|
||||
type Child struct {
|
||||
Parent
|
||||
Age int
|
||||
}
|
||||
c1 := &Child{}
|
||||
c1.Name = "111"
|
||||
c1.Age = 11
|
||||
|
||||
s := New(c1)
|
||||
n, _ := s.Field("Name")
|
||||
a, _ := s.Field("Age")
|
||||
assert.Equal(true, n.IsEmbedded())
|
||||
assert.Equal(false, a.IsEmbedded())
|
||||
}
|
||||
|
||||
func TestField_IsExported(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestField_IsEmbedded")
|
||||
|
||||
type Parent struct {
|
||||
Name string
|
||||
age int
|
||||
}
|
||||
p1 := &Parent{Name: "11", age: 11}
|
||||
s := New(p1)
|
||||
n, _ := s.Field("Name")
|
||||
a, _ := s.Field("age")
|
||||
assert.Equal(true, n.IsExported())
|
||||
assert.Equal(false, a.IsExported())
|
||||
}
|
||||
|
||||
func TestField_IsZero(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestField_IsZero")
|
||||
|
||||
type Parent struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
p1 := &Parent{Age: 11}
|
||||
s := New(p1)
|
||||
n, _ := s.Field("Name")
|
||||
a, _ := s.Field("Age")
|
||||
assert.Equal(true, n.IsZero())
|
||||
assert.Equal(false, a.IsZero())
|
||||
}
|
||||
|
||||
func TestField_Name(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestField_Name")
|
||||
|
||||
type Parent struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
p1 := &Parent{Age: 11}
|
||||
s := New(p1)
|
||||
n, _ := s.Field("Name")
|
||||
a, _ := s.Field("Age")
|
||||
|
||||
assert.Equal("Name", n.Name())
|
||||
assert.Equal("Age", a.Name())
|
||||
}
|
||||
|
||||
func TestField_Kind(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestField_Kind")
|
||||
|
||||
type Parent struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
p1 := &Parent{Age: 11}
|
||||
s := New(p1)
|
||||
n, _ := s.Field("Name")
|
||||
a, _ := s.Field("Age")
|
||||
|
||||
assert.Equal(reflect.String, n.Kind())
|
||||
assert.Equal(reflect.Int, a.Kind())
|
||||
}
|
||||
|
||||
func TestField_IsSlice(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestField_IsSlice")
|
||||
|
||||
type Parent struct {
|
||||
Name string
|
||||
arr []int
|
||||
}
|
||||
|
||||
p1 := &Parent{arr: []int{1, 2, 3}}
|
||||
s := New(p1)
|
||||
a, _ := s.Field("arr")
|
||||
|
||||
assert.Equal(true, a.IsSlice())
|
||||
}
|
||||
|
||||
func TestField_MapValue(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestField_MapValue")
|
||||
|
||||
t.Run("nested struct", func(t *testing.T) {
|
||||
type Child struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type Parent struct {
|
||||
Name string `json:"name"`
|
||||
Child *Child `json:"child"`
|
||||
}
|
||||
|
||||
c1 := &Child{"11-1"}
|
||||
p1 := &Parent{
|
||||
Name: "11",
|
||||
Child: c1,
|
||||
}
|
||||
|
||||
s := New(p1)
|
||||
f, ok := s.Field("Child")
|
||||
val := f.MapValue(f.Value())
|
||||
|
||||
assert.Equal(true, ok)
|
||||
assert.Equal(map[string]any{"name": "11-1"}, val)
|
||||
})
|
||||
|
||||
t.Run("nested ptr struct", func(t *testing.T) {
|
||||
type Child struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type Parent struct {
|
||||
Name string `json:"name"`
|
||||
Child any `json:"child"`
|
||||
}
|
||||
c1 := &Child{"11-1"}
|
||||
c2 := &c1
|
||||
c3 := &c2
|
||||
p1 := &Parent{
|
||||
Name: "11",
|
||||
Child: c3,
|
||||
}
|
||||
|
||||
s := New(p1)
|
||||
f, ok := s.Field("Child")
|
||||
val := f.MapValue(f.Value())
|
||||
|
||||
assert.Equal(true, ok)
|
||||
assert.Equal(map[string]any{"name": "11-1"}, val)
|
||||
})
|
||||
|
||||
t.Run("nested array", func(t *testing.T) {
|
||||
type Parent struct {
|
||||
Name string `json:"name"`
|
||||
Child []int `json:"child"`
|
||||
}
|
||||
|
||||
p1 := &Parent{
|
||||
Name: "11",
|
||||
Child: []int{1, 2, 3},
|
||||
}
|
||||
|
||||
s := New(p1)
|
||||
f, ok := s.Field("Child")
|
||||
val := f.MapValue(f.Value())
|
||||
|
||||
assert.Equal(true, ok)
|
||||
assert.Equal([]int{1, 2, 3}, val)
|
||||
})
|
||||
|
||||
t.Run("nested array in struct", func(t *testing.T) {
|
||||
type Child struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type Parent struct {
|
||||
Name string `json:"name"`
|
||||
Child []*Child `json:"child"`
|
||||
}
|
||||
|
||||
c1 := &Child{"11-1"}
|
||||
c2 := &Child{"11-2"}
|
||||
|
||||
p1 := &Parent{
|
||||
Name: "11",
|
||||
Child: []*Child{c1, c2},
|
||||
}
|
||||
|
||||
s := New(p1)
|
||||
f, ok := s.Field("Child")
|
||||
val := f.MapValue(f.Value())
|
||||
|
||||
assert.Equal(true, ok)
|
||||
arr := []any{map[string]any{"name": "11-1"}, map[string]any{"name": "11-2"}}
|
||||
assert.Equal(arr, val)
|
||||
})
|
||||
|
||||
t.Run("nested ptr array in struct", func(t *testing.T) {
|
||||
type Child struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type Parent struct {
|
||||
Name string `json:"name"`
|
||||
Child *[]*Child `json:"child"`
|
||||
}
|
||||
|
||||
c1 := &Child{"11-1"}
|
||||
c2 := &Child{"11-2"}
|
||||
|
||||
p1 := &Parent{
|
||||
Name: "11",
|
||||
Child: &[]*Child{c1, c2},
|
||||
}
|
||||
|
||||
s := New(p1)
|
||||
f, ok := s.Field("Child")
|
||||
val := f.MapValue(f.Value())
|
||||
|
||||
assert.Equal(true, ok)
|
||||
arr := []any{map[string]any{"name": "11-1"}, map[string]any{"name": "11-2"}}
|
||||
assert.Equal(arr, val)
|
||||
})
|
||||
|
||||
t.Run("nested map in struct", func(t *testing.T) {
|
||||
type Parent struct {
|
||||
Name string `json:"name"`
|
||||
Child map[string]any `json:"child"`
|
||||
}
|
||||
p1 := &Parent{
|
||||
Name: "11",
|
||||
Child: map[string]any{"a": 1, "b": map[string]any{"name": "11-1"}},
|
||||
}
|
||||
|
||||
s := New(p1)
|
||||
f, ok := s.Field("Child")
|
||||
val := f.MapValue(f.Value())
|
||||
assert.Equal(true, ok)
|
||||
assert.Equal(map[string]any{"a": 1, "b": map[string]any{"name": "11-1"}}, val)
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package structutil
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/pointer"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
@@ -17,11 +18,9 @@ type Struct struct {
|
||||
|
||||
// New returns a new *Struct
|
||||
func New(value any) *Struct {
|
||||
value = pointer.ExtractPointer(value)
|
||||
v := reflect.ValueOf(value)
|
||||
t := reflect.TypeOf(value)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
return &Struct{
|
||||
raw: value,
|
||||
rtype: t,
|
||||
@@ -49,8 +48,11 @@ func New(value any) *Struct {
|
||||
//
|
||||
// Only the exported fields of a struct can be converted.
|
||||
func (s *Struct) ToMap() (map[string]any, error) {
|
||||
result := make(map[string]any)
|
||||
if !s.IsStruct() {
|
||||
return nil, ErrInvalidStruct(s)
|
||||
}
|
||||
|
||||
result := make(map[string]any)
|
||||
fields := s.Fields()
|
||||
for _, f := range fields {
|
||||
if !f.IsExported() || f.tag.IsEmpty() || f.tag.Name == "-" {
|
||||
@@ -59,8 +61,7 @@ func (s *Struct) ToMap() (map[string]any, error) {
|
||||
if f.IsZero() && f.tag.HasOption("omitempty") {
|
||||
continue
|
||||
}
|
||||
// TODO: sub struct
|
||||
result[f.tag.Name] = f.Value()
|
||||
result[f.tag.Name] = f.MapValue(f.Value())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -68,19 +69,35 @@ func (s *Struct) ToMap() (map[string]any, error) {
|
||||
|
||||
// Fields returns all the struct fields within a slice
|
||||
func (s *Struct) Fields() []*Field {
|
||||
|
||||
var fields []*Field
|
||||
fieldNum := s.rvalue.NumField()
|
||||
for i := 0; i < fieldNum; i++ {
|
||||
v := s.rvalue.Field(i)
|
||||
sf := s.rtype.Field(i)
|
||||
field := newField(v, sf, DefaultTagName)
|
||||
field := newField(v, sf, s.TagName)
|
||||
fields = append(fields, field)
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// Field returns a Field if the given field name was found
|
||||
func (s *Struct) Field(name string) (*Field, bool) {
|
||||
f, ok := s.rtype.FieldByName(name)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return newField(s.rvalue.FieldByName(name), f, s.TagName), true
|
||||
}
|
||||
|
||||
// IsStruct returns true if the given rvalue is a struct
|
||||
func (s *Struct) IsStruct() bool {
|
||||
k := s.rvalue.Kind()
|
||||
if k == reflect.Invalid {
|
||||
return false
|
||||
}
|
||||
return k == reflect.Struct
|
||||
}
|
||||
|
||||
// ToMap convert struct to map, only convert exported struct field
|
||||
// map key is specified same as struct field tag `json` value.
|
||||
func ToMap(v any) (map[string]any, error) {
|
||||
|
||||
@@ -2,11 +2,18 @@ package structutil
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/internal"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToMap(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestStructToMap")
|
||||
func TestStruct_ToMap(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestStruct_ToMap")
|
||||
|
||||
t.Run("no struct", func(t *testing.T) {
|
||||
m, _ := ToMap(1)
|
||||
var expected map[string]any
|
||||
assert.Equal(expected, m)
|
||||
})
|
||||
|
||||
t.Run("StructToMap", func(_ *testing.T) {
|
||||
type People struct {
|
||||
@@ -55,3 +62,69 @@ func TestToMap(t *testing.T) {
|
||||
assert.Equal(expect2, p2m)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStruct_Fields(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestStruct_Fields")
|
||||
|
||||
type Parent struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C []string `json:"c"`
|
||||
D map[string]any `json:"d"`
|
||||
}
|
||||
|
||||
p1 := &Parent{
|
||||
A: "1",
|
||||
B: 11,
|
||||
C: []string{"11", "22"},
|
||||
D: map[string]any{"d1": 1, "d2": 2},
|
||||
}
|
||||
|
||||
s := New(p1)
|
||||
fields := s.Fields()
|
||||
assert.Equal(4, len(fields))
|
||||
assert.Equal(reflect.String, fields[0].Kind())
|
||||
assert.Equal(reflect.Int, fields[1].Kind())
|
||||
assert.Equal(reflect.Slice, fields[2].Kind())
|
||||
assert.Equal(reflect.Map, fields[3].Kind())
|
||||
}
|
||||
|
||||
func TestStruct_Field(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestStruct_Field")
|
||||
|
||||
type Parent struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C []string `json:"c"`
|
||||
D map[string]any `json:"d"`
|
||||
}
|
||||
|
||||
p1 := &Parent{
|
||||
A: "1",
|
||||
B: 11,
|
||||
C: []string{"11", "22"},
|
||||
D: map[string]any{"d1": 1, "d2": 2},
|
||||
}
|
||||
|
||||
s := New(p1)
|
||||
a, ok := s.Field("A")
|
||||
assert.Equal(true, ok)
|
||||
assert.Equal(reflect.String, a.Kind())
|
||||
assert.Equal("1", a.Value())
|
||||
assert.Equal("a", a.tag.Name)
|
||||
assert.Equal(false, a.tag.HasOption("omitempty"))
|
||||
}
|
||||
|
||||
func TestStruct_IsStruct(t *testing.T) {
|
||||
assert := internal.NewAssert(t, "TestStruct_Field")
|
||||
|
||||
type Test1 struct{}
|
||||
t1 := &Test1{}
|
||||
t2 := 1
|
||||
|
||||
s1 := New(t1)
|
||||
s2 := New(t2)
|
||||
|
||||
assert.Equal(true, s1.IsStruct())
|
||||
assert.Equal(false, s2.IsStruct())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user