1
0
mirror of https://github.com/duke-git/lancet.git synced 2026-02-07 22:22:29 +08:00

Add StructUtil for provide more rich functions (#79)

* add support json tag attribute for StructToMap function

* add the structutil to provide more rich functions and fixed #77
This commit is contained in:
zm
2023-03-13 19:28:37 +08:00
committed by GitHub
parent 1755dd249b
commit 924589d2da
9 changed files with 263 additions and 64 deletions

55
structutil/field.go Normal file
View File

@@ -0,0 +1,55 @@
package structutil
import "reflect"
type Field struct {
value reflect.Value
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: f,
tag: newTag(tag),
}
}
// Tag returns the value that the key in the tag string.
func (f *Field) Tag() *Tag {
return f.tag
}
// Value returns the underlying value of the field.
func (f *Field) Value() any {
return f.value.Interface()
}
// IsEmbedded returns true if the given field is an embedded field.
func (f *Field) IsEmbedded() bool {
return f.field.Anonymous
}
// IsExported returns true if the given field is exported.
func (f *Field) IsExported() bool {
return f.field.PkgPath == ""
}
// IsZero returns true if the given field is zero value.
func (f *Field) IsZero() bool {
z := reflect.Zero(f.value.Type()).Interface()
v := f.Value()
return reflect.DeepEqual(z, v)
}
// Name returns the name of the given field
func (f *Field) Name() string {
return f.field.Name
}
// Kind returns the field's kind
func (f *Field) Kind() reflect.Kind {
return f.value.Kind()
}

88
structutil/struct.go Normal file
View File

@@ -0,0 +1,88 @@
package structutil
import (
"reflect"
)
// DefaultTagName is the default tag for struct fields to lookup.
var DefaultTagName = "json"
// Struct is abstract struct for provide several high level functions
type Struct struct {
raw any
rtype reflect.Type
rvalue reflect.Value
TagName string
}
// New returns a new *Struct
func New(value any) *Struct {
v := reflect.ValueOf(value)
t := reflect.TypeOf(value)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
return &Struct{
raw: value,
rtype: t,
rvalue: v,
TagName: DefaultTagName,
}
}
// ToMap converts the given struct to a map[string]any, where the keys
// of the keys are the field names and the values of the map are the values
// of the fields. The default map key is the struct field name, but you can
// change it. The `json` key is the default tag key. Example:
//
// // default
// Name string `json:"name"`
//
// // ignore the field
// Name string // no tag
// Age string `json:"-"` // json ignore tag
// sex string // unexported field
// Goal int `json:"goal,omitempty"` // omitempty if the field is zero value
//
// // custom map key
// Name string `json:"myName"`
//
// Only the exported fields of a struct can be converted.
func (s *Struct) ToMap() (map[string]any, error) {
result := make(map[string]any)
fields := s.Fields()
for _, f := range fields {
if !f.IsExported() || f.tag.IsEmpty() || f.tag.Name == "-" {
continue
}
if f.IsZero() && f.tag.HasOption("omitempty") {
continue
}
// TODO: sub struct
result[f.tag.Name] = f.Value()
}
return result, nil
}
// 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)
fields = append(fields, field)
}
return fields
}
// 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) {
return New(v).ToMap()
}

57
structutil/struct_test.go Normal file
View File

@@ -0,0 +1,57 @@
package structutil
import (
"github.com/duke-git/lancet/v2/internal"
"testing"
)
func TestToMap(t *testing.T) {
assert := internal.NewAssert(t, "TestStructToMap")
t.Run("StructToMap", func(_ *testing.T) {
type People struct {
Name string `json:"name"`
age int
}
p := People{
"test",
100,
}
pm, _ := ToMap(p)
var expected = map[string]any{"name": "test"}
assert.Equal(expected, pm)
})
t.Run("StructToMapWithJsonAttr", func(_ *testing.T) {
type People struct {
Name string `json:"name,omitempty"` // json tag with attribute
Phone string `json:"phone"` // json tag without attribute
Sex string `json:"-"` // ignore by "-"
Age int // ignore by no tag
email string // ignore by unexported
IsWorking bool `json:"is_working"`
}
p1 := People{
Name: "AAA", // exist
Phone: "1111",
Sex: "male",
Age: 100,
email: "11@gmail.com",
}
p1m, _ := ToMap(p1)
var expect1 = map[string]any{"name": "AAA", "phone": "1111", "is_working": false}
assert.Equal(expect1, p1m)
p2 := People{
Name: "",
Phone: "2222",
Sex: "male",
Age: 0,
email: "22@gmail.com",
IsWorking: true,
}
p2m, _ := ToMap(p2)
var expect2 = map[string]any{"phone": "2222", "is_working": true}
assert.Equal(expect2, p2m)
})
}

32
structutil/tag.go Normal file
View File

@@ -0,0 +1,32 @@
package structutil
import (
"github.com/duke-git/lancet/v2/validator"
"strings"
)
type Tag struct {
Name string
Options []string
}
func newTag(tag string) *Tag {
res := strings.Split(tag, ",")
return &Tag{
Name: res[0],
Options: res[1:],
}
}
func (t *Tag) HasOption(opt string) bool {
for _, o := range t.Options {
if o == opt {
return true
}
}
return false
}
func (t *Tag) IsEmpty() bool {
return validator.IsEmptyString(t.Name)
}