Skip to content

Commit

Permalink
add feature nested pointer support(#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhlingbo committed Jul 12, 2023
1 parent 38926db commit 0b2555c
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 16 deletions.
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,83 @@ fmt.Println(example.Bar) //Prints: 0

```

Pointer Set
-------

Pointer field struct is a tricky usage to avoid covering existed values.

Take the basic example in the above section and change it slightly:
```go

type ExamplePointer struct {
Foo *bool `default:"true"` //<-- StructTag with a default key
Bar *string `default:"example"`
Qux *int `default:"22"`
Oed *int64 `default:"64"`
}

...

boolZero := false
stringZero := ""
intZero := 0
example := &ExamplePointer{
Foo: &boolZero,
Bar: &stringZero,
Qux: &intZero,
}
defaults.SetDefaults(example)

fmt.Println(*example.Foo) //Prints: false (zero value `false` for bool but not for bool ptr)
fmt.Println(*example.Bar) //Prints: "" (print "" which set in advance, not "example" for default)
fmt.Println(*example.Qux) //Prints: 0 (0 instead of 22)
fmt.Println(*example.Oed) //Prints: 64 (64, because the ptr addr is nil when SetDefaults)

```

It's also a very useful feature for web application which default values are needed while binding request json.

For example:
```go
type ExamplePostBody struct {
Foo *bool `json:"foo" default:"true"` //<-- StructTag with a default key
Bar *string `json:"bar" default:"example"`
Qux *int `json:"qux" default:"22"`
Oed *int64 `json:"oed" default:"64"`
}
```

HTTP request seems like this:
```bash
curl --location --request POST ... \
... \
--header 'Content-Type: application/json' \
--data-raw '{
"foo": false,
"bar": "",
"qux": 0
}'
```

Request handler:
```go
func PostExampleHandler(c *gin.Context) {
var reqBody ExamplePostBody
if err := c.ShouldBindJSON(&reqBody); err != nil {
c.JSON(http.StatusBadRequest, nil)
return
}
defaults.SetDefaults(&reqBody)

fmt.Println(*reqBody.Foo) //Prints: false (zero value `false` for bool but not for bool ptr)
fmt.Println(*reqBody.Bar) //Prints: "" (print "" which set in advance, not "example" for default)
fmt.Println(*reqBody.Qux) //Prints: 0 (0 instead of 22, did not confused from whether zero value is in json or not)
fmt.Println(*reqBody.Oed) //Prints: 64 (In this case "oed" is not in req json, so set default 64)

...
}
```

License
-------

Expand Down
56 changes: 45 additions & 11 deletions defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ import (
// the StructTag with name "default" and the directed value.
//
// Usage
// type ExampleBasic struct {
// Foo bool `default:"true"`
// Bar string `default:"33"`
// Qux int8
// Dur time.Duration `default:"2m3s"`
// }
//
// foo := &ExampleBasic{}
// SetDefaults(foo)
// type ExampleBasic struct {
// Foo bool `default:"true"`
// Bar string `default:"33"`
// Qux int8
// Dur time.Duration `default:"2m3s"`
// }
//
// foo := &ExampleBasic{}
// SetDefaults(foo)
func SetDefaults(variable interface{}) {
getDefaultFiller().Fill(variable)
}
Expand Down Expand Up @@ -90,7 +91,11 @@ func newDefaultFiller() *Filler {
types := make(map[TypeHash]FillerFunc, 1)
types["time.Duration"] = func(field *FieldData) {
d, _ := time.ParseDuration(field.TagValue)
field.Value.Set(reflect.ValueOf(d))
if field.Value.Kind() == reflect.Ptr {
field.Value.Set(reflect.ValueOf(&d))
} else {
field.Value.Set(reflect.ValueOf(d))
}
}

funcs[reflect.Slice] = func(field *FieldData) {
Expand All @@ -107,6 +112,16 @@ func newDefaultFiller() *Filler {
fields := getDefaultFiller().GetFieldsFromValue(field.Value.Index(i), nil)
getDefaultFiller().SetDefaultValues(fields)
}
case reflect.Ptr:
count := field.Value.Len()
for i := 0; i < count; i++ {
if field.Value.Index(i).IsZero() {
newValue := reflect.New(field.Value.Index(i).Type().Elem())
field.Value.Index(i).Set(newValue)
}
fields := getDefaultFiller().GetFieldsFromValue(field.Value.Index(i).Elem(), nil)
getDefaultFiller().SetDefaultValues(fields)
}
default:
//处理形如 [1,2,3,4]
reg := regexp.MustCompile(`^\[(.*)\]$`)
Expand Down Expand Up @@ -134,6 +149,27 @@ func newDefaultFiller() *Filler {
}
}

funcs[reflect.Ptr] = func(field *FieldData) {
k := field.Value.Type().Elem().Kind()
if k != reflect.Struct && field.TagValue == "" {
return
}
if field.Value.IsNil() {
v := reflect.New(field.Value.Type().Elem())
field.Value.Set(v)
}
elemField := &FieldData{
Value: field.Value.Elem(),
Field: reflect.StructField{
Type: field.Field.Type.Elem(),
Tag: field.Field.Tag,
},
TagValue: field.TagValue,
Parent: nil,
}
funcs[field.Value.Elem().Kind()](elemField)
}

return &Filler{FuncByKind: funcs, FuncByType: types, Tag: "default"}
}

Expand All @@ -159,13 +195,11 @@ func parseDateTimeString(data string) string {
case "date":
str := time.Now().AddDate(values[0], values[1], values[2]).Format("2006-01-02")
data = strings.Replace(data, match[0], str, -1)
break
case "time":
str := time.Now().Add((time.Duration(values[0]) * time.Hour) +
(time.Duration(values[1]) * time.Minute) +
(time.Duration(values[2]) * time.Second)).Format("15:04:05")
data = strings.Replace(data, match[0], str, -1)
break
}
}
}
Expand Down
55 changes: 55 additions & 0 deletions defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ type Child struct {
Age int `default:"10"`
}

type ChildPtr struct {
Name *string
Age *int `default:"10"`
}

type ExampleBasic struct {
Bool bool `default:"true"`
Integer int `default:"33"`
Expand Down Expand Up @@ -61,6 +66,27 @@ type ExampleBasic struct {
StringSliceSlice [][]string `default:"[[1],[]]"`

DateTime string `default:"{{date:1,-10,0}} {{time:1,-5,10}}"`

BoolPtr *bool `default:"false"`
IntPtr *int `default:"33"`
Int8Ptr *int8 `default:"8"`
Int16Ptr *int16 `default:"16"`
Int32Ptr *int32 `default:"32"`
Int64Ptr *int64 `default:"64"`
UIntPtr *uint `default:"11"`
UInt8Ptr *uint8 `default:"18"`
UInt16Ptr *uint16 `default:"116"`
UInt32Ptr *uint32 `default:"132"`
UInt64Ptr *uint64 `default:"164"`
Float32Ptr *float32 `default:"3.2"`
Float64Ptr *float64 `default:"6.4"`
DurationPtr *time.Duration `default:"1s"`
SecondPtr *time.Duration `default:"1s"`
StructPtr *struct {
Bool bool `default:"true"`
Integer *int `default:"33"`
}
ChildrenPtr []*ChildPtr
}

func (s *DefaultsSuite) TestSetDefaultsBasic(c *C) {
Expand Down Expand Up @@ -106,6 +132,24 @@ func (s *DefaultsSuite) assertTypes(c *C, foo *ExampleBasic) {
c.Assert(foo.IntSliceSlice, DeepEquals, [][]int{[]int{1}, []int{2}, []int{3}, []int{4}})
c.Assert(foo.StringSliceSlice, DeepEquals, [][]string{[]string{"1"}, []string{}})
c.Assert(foo.DateTime, Equals, "2020-08-10 12:55:10")
c.Assert(*foo.BoolPtr, Equals, false)
c.Assert(*foo.IntPtr, Equals, 33)
c.Assert(*foo.Int8Ptr, Equals, int8(8))
c.Assert(*foo.Int16Ptr, Equals, int16(16))
c.Assert(*foo.Int32Ptr, Equals, int32(32))
c.Assert(*foo.Int64Ptr, Equals, int64(64))
c.Assert(*foo.UIntPtr, Equals, uint(11))
c.Assert(*foo.UInt8Ptr, Equals, uint8(18))
c.Assert(*foo.UInt16Ptr, Equals, uint16(116))
c.Assert(*foo.UInt32Ptr, Equals, uint32(132))
c.Assert(*foo.UInt64Ptr, Equals, uint64(164))
c.Assert(*foo.Float32Ptr, Equals, float32(3.2))
c.Assert(*foo.Float64Ptr, Equals, 6.4)
c.Assert(*foo.DurationPtr, Equals, time.Second)
c.Assert(*foo.SecondPtr, Equals, time.Second)
c.Assert(foo.StructPtr.Bool, Equals, true)
c.Assert(*foo.StructPtr.Integer, Equals, 33)
c.Assert(foo.ChildrenPtr, IsNil)
}

func (s *DefaultsSuite) TestSetDefaultsWithValues(c *C) {
Expand All @@ -118,6 +162,13 @@ func (s *DefaultsSuite) TestSetDefaultsWithValues(c *C) {
Children: []Child{{Name: "alice"}, {Name: "bob", Age: 2}},
}

intzero := 0
foo.IntPtr = &intzero

ageZero := 0
childPtr := &ChildPtr{Age: &ageZero}
foo.ChildrenPtr = append(foo.ChildrenPtr, childPtr)

SetDefaults(foo)

c.Assert(foo.Integer, Equals, 55)
Expand All @@ -127,6 +178,10 @@ func (s *DefaultsSuite) TestSetDefaultsWithValues(c *C) {
c.Assert(string(foo.Bytes), Equals, "foo")
c.Assert(foo.Children[0].Age, Equals, 10)
c.Assert(foo.Children[1].Age, Equals, 2)
c.Assert(*foo.ChildrenPtr[0].Age, Equals, 0)
c.Assert(foo.ChildrenPtr[0].Name, IsNil)

c.Assert(*foo.IntPtr, Equals, 0)
}

func (s *DefaultsSuite) BenchmarkLogic(c *C) {
Expand Down
20 changes: 15 additions & 5 deletions filler.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,23 @@ func (f *Filler) isEmpty(field *FieldData) bool {
// always assume the structs in the slice is empty and can be filled
// the actually struct filling logic should take care of the rest
return true
case reflect.Ptr:
switch field.Value.Type().Elem().Elem().Kind() {
case reflect.Struct:
return true
default:
return field.Value.Len() == 0
}
default:
return field.Value.Len() == 0
}
case reflect.String:
return field.Value.String() == ""
case reflect.Ptr:
if field.Value.Type().Elem().Kind() == reflect.Struct {
return true
}
return field.Value.IsNil()
}
return true
}
Expand All @@ -105,28 +117,26 @@ func (f *Filler) SetDefaultValue(field *FieldData) {
return
}
}

return
}

func (f *Filler) getFunctionByName(field *FieldData) FillerFunc {
if f, ok := f.FuncByName[field.Field.Name]; ok == true {
if f, ok := f.FuncByName[field.Field.Name]; ok {
return f
}

return nil
}

func (f *Filler) getFunctionByType(field *FieldData) FillerFunc {
if f, ok := f.FuncByType[GetTypeHash(field.Field.Type)]; ok == true {
if f, ok := f.FuncByType[GetTypeHash(field.Field.Type)]; ok {
return f
}

return nil
}

func (f *Filler) getFunctionByKind(field *FieldData) FillerFunc {
if f, ok := f.FuncByKind[field.Field.Type.Kind()]; ok == true {
if f, ok := f.FuncByKind[field.Field.Type.Kind()]; ok {
return f
}

Expand Down

0 comments on commit 0b2555c

Please sign in to comment.