From 924589d2daa46cac89815a56148e65fe90fbff18 Mon Sep 17 00:00:00 2001 From: zm <360475097@qq.com> Date: Mon, 13 Mar 2023 19:28:37 +0800 Subject: [PATCH] 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 --- convertor/convertor.go | 29 +----------- convertor/convertor_test.go | 13 +++--- netutil/http.go | 35 +++++---------- netutil/http_test.go | 11 +++-- netutil/net_example_test.go | 7 ++- structutil/field.go | 55 +++++++++++++++++++++++ structutil/struct.go | 88 +++++++++++++++++++++++++++++++++++++ structutil/struct_test.go | 57 ++++++++++++++++++++++++ structutil/tag.go | 32 ++++++++++++++ 9 files changed, 263 insertions(+), 64 deletions(-) create mode 100644 structutil/field.go create mode 100644 structutil/struct.go create mode 100644 structutil/struct_test.go create mode 100644 structutil/tag.go diff --git a/convertor/convertor.go b/convertor/convertor.go index 91dad35..80f9f92 100644 --- a/convertor/convertor.go +++ b/convertor/convertor.go @@ -11,9 +11,9 @@ import ( "encoding/json" "errors" "fmt" + "github.com/duke-git/lancet/v2/structutil" "math" "reflect" - "regexp" "strconv" "strings" ) @@ -235,32 +235,7 @@ func ToMap[T any, K comparable, V any](array []T, iteratee func(T) (K, V)) map[K // map key is specified same as struct field tag `json` value. // Play: https://go.dev/play/p/KYGYJqNUBOI func StructToMap(value any) (map[string]any, error) { - v := reflect.ValueOf(value) - t := reflect.TypeOf(value) - - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - if t.Kind() != reflect.Struct { - return nil, fmt.Errorf("data type %T not support, shuld be struct or pointer to struct", value) - } - - result := make(map[string]any) - - fieldNum := t.NumField() - pattern := `^[A-Z]` - regex := regexp.MustCompile(pattern) - for i := 0; i < fieldNum; i++ { - name := t.Field(i).Name - tag := t.Field(i).Tag.Get("json") - if tag == "" || strings.HasPrefix(tag, "-") || !regex.MatchString(name) { - continue - } - tag = strings.Split(tag, ",")[0] - result[tag] = v.Field(i).Interface() - } - - return result, nil + return structutil.ToMap(value) } // MapToSlice convert map to slice based on iteratee function. diff --git a/convertor/convertor_test.go b/convertor/convertor_test.go index d73ffee..76ef550 100644 --- a/convertor/convertor_test.go +++ b/convertor/convertor_test.go @@ -180,7 +180,7 @@ func TestToMap(t *testing.T) { func TestStructToMap(t *testing.T) { assert := internal.NewAssert(t, "TestStructToMap") - t.Run("StructToMap", func(t *testing.T) { + t.Run("StructToMap", func(_ *testing.T) { type People struct { Name string `json:"name"` age int @@ -194,7 +194,7 @@ func TestStructToMap(t *testing.T) { assert.Equal(expected, pm) }) - t.Run("StructToMapWithJsonAttr", func(t *testing.T) { + 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 @@ -202,13 +202,12 @@ func TestStructToMap(t *testing.T) { age int // no tag } p := People{ - "test", - "1111", - "male", - 100, + Phone: "1111", + Sex: "male", + age: 100, } pm, _ := StructToMap(p) - var expected = map[string]any{"name": "test", "phone": "1111"} + var expected = map[string]any{"phone": "1111"} assert.Equal(expected, pm) }) } diff --git a/netutil/http.go b/netutil/http.go index 6276380..70e0bb9 100644 --- a/netutil/http.go +++ b/netutil/http.go @@ -21,12 +21,11 @@ import ( "io" "net/http" "net/url" - "reflect" - "regexp" "sort" "strings" "time" + "github.com/duke-git/lancet/v2/convertor" "github.com/duke-git/lancet/v2/slice" ) @@ -219,7 +218,7 @@ func (client *HttpClient) setTLS(rawUrl string) { } } -// setHeader set http rquest header +// setHeader set http request header func (client *HttpClient) setHeader(req *http.Request, headers http.Header) { if headers == nil { headers = make(http.Header) @@ -278,29 +277,15 @@ func validateRequest(req *HttpRequest) error { // StructToUrlValues convert struct to url valuse, // only convert the field which is exported and has `json` tag. // Play: https://go.dev/play/p/pFqMkM40w9z -func StructToUrlValues(targetStruct any) url.Values { - rv := reflect.ValueOf(targetStruct) - rt := reflect.TypeOf(targetStruct) - - if rt.Kind() == reflect.Ptr { - rt = rt.Elem() - } - if rt.Kind() != reflect.Struct { - panic(fmt.Errorf("data type %T not support, shuld be struct or pointer to struct", targetStruct)) - } - +func StructToUrlValues(targetStruct any) (url.Values, error) { result := url.Values{} - - fieldNum := rt.NumField() - pattern := `^[A-Z]` - regex := regexp.MustCompile(pattern) - for i := 0; i < fieldNum; i++ { - name := rt.Field(i).Name - tag := rt.Field(i).Tag.Get("json") - if regex.MatchString(name) && tag != "" { - result.Add(tag, fmt.Sprintf("%v", rv.Field(i).Interface())) - } + s, err := convertor.StructToMap(targetStruct) + if err != nil { + return nil, err + } + for k, v := range s { + result.Add(k, fmt.Sprintf("%v", v)) } - return result + return result, nil } diff --git a/netutil/http_test.go b/netutil/http_test.go index d32915c..73d5440 100644 --- a/netutil/http_test.go +++ b/netutil/http_test.go @@ -217,17 +217,22 @@ func TestStructToUrlValues(t *testing.T) { assert := internal.NewAssert(t, "TestStructToUrlValues") type TodoQuery struct { - Id int `json:"id"` - UserId int `json:"userId"` + Id int `json:"id"` + UserId int `json:"userId"` + Name string `json:"name,omitempty"` } todoQuery := TodoQuery{ Id: 1, UserId: 1, } - todoValues := StructToUrlValues(todoQuery) + todoValues, err := StructToUrlValues(todoQuery) + if err != nil { + t.Errorf("params is invalid: %v", err) + } assert.Equal("1", todoValues.Get("id")) assert.Equal("1", todoValues.Get("userId")) + assert.Equal("", todoValues.Get("name")) request := &HttpRequest{ RawURL: "https://jsonplaceholder.typicode.com/todos", diff --git a/netutil/net_example_test.go b/netutil/net_example_test.go index c9d7ee6..80b07be 100644 --- a/netutil/net_example_test.go +++ b/netutil/net_example_test.go @@ -123,14 +123,17 @@ func ExampleHttpClient_DecodeResponse() { func ExampleStructToUrlValues() { type TodoQuery struct { - Id int `json:"id"` + Id int `json:"id,omitempty"` Name string `json:"name"` } todoQuery := TodoQuery{ Id: 1, Name: "Test", } - todoValues := StructToUrlValues(todoQuery) + todoValues, err := StructToUrlValues(todoQuery) + if err != nil { + return + } fmt.Println(todoValues.Get("id")) fmt.Println(todoValues.Get("name")) diff --git a/structutil/field.go b/structutil/field.go new file mode 100644 index 0000000..c429ffe --- /dev/null +++ b/structutil/field.go @@ -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() +} diff --git a/structutil/struct.go b/structutil/struct.go new file mode 100644 index 0000000..e53543c --- /dev/null +++ b/structutil/struct.go @@ -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() +} diff --git a/structutil/struct_test.go b/structutil/struct_test.go new file mode 100644 index 0000000..cf0eed5 --- /dev/null +++ b/structutil/struct_test.go @@ -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) + }) +} diff --git a/structutil/tag.go b/structutil/tag.go new file mode 100644 index 0000000..452147b --- /dev/null +++ b/structutil/tag.go @@ -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) +}