1
0
mirror of https://github.com/duke-git/lancet.git synced 2026-02-04 12:52:28 +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

View File

@@ -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.

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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"))

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)
}