From 2d2c27709025cf7ccd1d53620592263b76afdf22 Mon Sep 17 00:00:00 2001 From: zm <360475097@qq.com> Date: Wed, 15 Mar 2023 14:26:34 +0800 Subject: [PATCH] [StructUtil] add support that the Struct can nest any type to transform (#80) * add support json tag attribute for StructToMap function * add the structutil to provide more rich functions and fixed #77 * add support that the nested struct to map for structutil * recover code * add structutil unit test * [StructUtil] add unit test --- docs/structutil/field.md | 0 docs/structutil/struct.md | 0 pointer/pointer.go | 14 ++ pointer/pointer_test.go | 18 +++ structutil/error.go | 7 + structutil/field.go | 70 ++++++++-- structutil/field_test.go | 272 ++++++++++++++++++++++++++++++++++++++ structutil/struct.go | 35 +++-- structutil/struct_test.go | 77 ++++++++++- 9 files changed, 473 insertions(+), 20 deletions(-) create mode 100644 docs/structutil/field.md create mode 100644 docs/structutil/struct.md create mode 100644 pointer/pointer.go create mode 100644 pointer/pointer_test.go create mode 100644 structutil/error.go create mode 100644 structutil/field_test.go diff --git a/docs/structutil/field.md b/docs/structutil/field.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/structutil/struct.md b/docs/structutil/struct.md new file mode 100644 index 0000000..e69de29 diff --git a/pointer/pointer.go b/pointer/pointer.go new file mode 100644 index 0000000..365d048 --- /dev/null +++ b/pointer/pointer.go @@ -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()) +} diff --git a/pointer/pointer_test.go b/pointer/pointer_test.go new file mode 100644 index 0000000..c003703 --- /dev/null +++ b/pointer/pointer_test.go @@ -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)) +} diff --git a/structutil/error.go b/structutil/error.go new file mode 100644 index 0000000..8b808e1 --- /dev/null +++ b/structutil/error.go @@ -0,0 +1,7 @@ +package structutil + +import "fmt" + +func ErrInvalidStruct(v any) error { + return fmt.Errorf("invalid struct %v", v) +} diff --git a/structutil/field.go b/structutil/field.go index c429ffe..3419172 100644 --- a/structutil/field.go +++ b/structutil/field.go @@ -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 } diff --git a/structutil/field_test.go b/structutil/field_test.go new file mode 100644 index 0000000..8663a27 --- /dev/null +++ b/structutil/field_test.go @@ -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) + }) +} diff --git a/structutil/struct.go b/structutil/struct.go index e53543c..eb053b7 100644 --- a/structutil/struct.go +++ b/structutil/struct.go @@ -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) { diff --git a/structutil/struct_test.go b/structutil/struct_test.go index cf0eed5..f5da905 100644 --- a/structutil/struct_test.go +++ b/structutil/struct_test.go @@ -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()) +}