Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support parsing time.Time field with layout tag #21

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions binding/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@ func (b *Binding) getOrPrepareReceiver(value reflect.Value) (*receiver, error) {
paramIn = raw_body
case b.config.defaultVal:
paramIn = default_val
case b.config.timeLayout:
p.timeLayout = tagKV.value
continue L
default:
continue L
}
Expand Down
89 changes: 89 additions & 0 deletions binding/bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,95 @@ func TestDefault(t *testing.T) {
assert.Equal(t, map[string][]map[string][]int64{"a": {{"aa": {1, 2, 3}, "bb": []int64{4, 5}}}, "b": {map[string][]int64{}}}, recv.Complex)
}

func TestTimeLayout(t *testing.T) {
type alias time.Time

type Recv struct {
X struct {
A time.Time `path:"a" layout:"2006-01-02"`
B time.Time `cookie:"b"` // using default layout
C time.Time `query:"c" layout:"2006-01-02"`
D time.Time `form:"d" layout:"2006-01-02"`
E time.Time `cookie:"e" layout:"2006-01-02"`
F time.Time `header:"F" layout:"2006-01-02"`
G time.Time `query:"g" layout:"2006-01-02"`
H time.Time `cookie:"h" layout:"2006-01-02"`
I time.Time `query:"i" layout:"2006-01-02" default:"2020-03-03"`
J []time.Time `query:"j" layout:"2006-01-02"`
K []*time.Time `cookie:"k" layout:"2006-01-02"`
L alias `form:"L" layout:"2006-01-02" default:"2020-03-03"`
II alias `form:"I" layout:"2006-01-02" default:"2020-03-03"`
}
Z time.Time `layout:"2006-01-02"` // auto binding
Y *time.Time `layout:"2006-01-02"` // auto binding
}

form := make(url.Values)
form.Add("d", "2020-03-03")
form.Add("Y", "2020-03-03")
form.Add("Z", "2020-03-03")
form.Add("L", "2020-03-03")

header := make(http.Header)
contentType, bodyReader := httpbody.NewFormBody2(form, nil)
header.Set("Content-Type", contentType)
header.Set("F", "2020-03-03")

req := newRequest("http://localhost?c=2020-03-03&j=2020-03-03&j=2020-03-04", header, []*http.Cookie{
{Name: "b", Value: "Mon, 03 Mar 2020 00:00:00 UTC"},
{Name: "e", Value: "2020-03-03"},
{Name: "h", Value: "20200303"},
{Name: "k", Value: "2020-03-03"},
{Name: "k", Value: "2020-03-04"},
}, bodyReader)
recv := new(Recv)
binder := binding.New(nil)

ts1, _ := time.Parse("2006-01-02", "2020-03-03")
ts2, _ := time.Parse("2006-01-02", "2020-03-04")
err := binder.BindAndValidate(recv, req, new(testPathParams2))
assert.NoError(t, err)
assert.Equal(t, ts1, recv.X.B)
assert.Equal(t, ts1, recv.X.C)
assert.Equal(t, ts1, recv.X.D)
assert.Equal(t, ts1, recv.X.E)
assert.Equal(t, ts1, recv.X.F)
assert.Equal(t, time.Time{}, recv.X.G) // not assigned value
assert.Equal(t, time.Time{}, recv.X.H) // invalid time value
assert.Equal(t, ts1, recv.X.I)
assert.Equal(t, alias(ts1), recv.X.II)
assert.Equal(t, []time.Time{ts1, ts2}, recv.X.J)
assert.Equal(t, []*time.Time{&ts1, &ts2}, recv.X.K)
assert.Equal(t, alias(ts1), recv.X.L)
assert.Equal(t, ts1, recv.Z)
assert.Equal(t, ts1, *recv.Y)
}

func TestTimeLayout_RawBody(t *testing.T) {
type alias time.Time

type Recv struct {
X struct {
A time.Time `raw_body:"a" layout:"2006-01-02"`
}
Z time.Time `raw_body:"z" layout:"2006-01-02"`
Y alias `raw_body:"y" layout:"2006-01-02"`
}

header := make(http.Header)
bodyBytes := []byte("2020-03-03")
req := newRequest("http://localhost", header, nil, bytes.NewReader(bodyBytes))
recv := new(Recv)
binder := binding.New(nil)

ts, _ := time.Parse("2006-01-02", "2020-03-03")
err := binder.BindAndValidate(recv, req, new(testPathParams2))
assert.NoError(t, err)
assert.Equal(t, ts, recv.X.A)
assert.Equal(t, ts, recv.Z)
assert.Equal(t, alias(ts), recv.Y)
}

func TestAuto(t *testing.T) {
type Recv struct {
A string `vd:"$!=''"`
Expand Down
99 changes: 94 additions & 5 deletions binding/param_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"reflect"
"strconv"
"strings"
"time"

"github.com/henrylee2cn/ameda"
"github.com/henrylee2cn/goutil"
Expand All @@ -17,7 +18,28 @@ import (
)

const (
specialChar = "\x07"
specialChar = "\x07"
defaultLayout = time.RFC1123
)

var (
layoutMap = map[string]string{
"ANSIC": time.ANSIC,
"UnixDate": time.UnixDate,
"RubyDate": time.RubyDate,
"RFC822": time.RFC822,
"RFC822Z": time.RFC822Z,
"RFC850": time.RFC850,
"RFC1123": time.RFC1123,
"RFC1123Z": time.RFC1123Z,
"RFC3339": time.RFC3339,
"RFC3339Nano": time.RFC3339Nano,
"Kitchen": time.Kitchen,
"Stamp": time.Stamp,
"StampMilli": time.StampMilli,
"StampMicro": time.StampMicro,
"StampNano": time.StampNano,
}
)

type paramInfo struct {
Expand All @@ -28,6 +50,7 @@ type paramInfo struct {
bindErrFactory func(failField, msg string) error
looseZeroMode bool
defaultVal []byte
timeLayout string // only applicable to time.Time param
}

func (p *paramInfo) name(paramIn in) string {
Expand Down Expand Up @@ -77,6 +100,13 @@ func (p *paramInfo) bindRawBody(info *tagInfo, expr *tagexpr.TagExpr, bodyBytes
case reflect.String:
v.Set(reflect.ValueOf(goutil.BytesToString(bodyBytes)))
return nil
case reflect.Struct:
if isTimeType(v.Type()) {
t, _ := time.Parse(p.timeLayout, goutil.BytesToString(bodyBytes))
v.Set(reflect.ValueOf(t).Convert(v.Type()))
return nil
}
fallthrough
default:
return info.typeError
}
Expand Down Expand Up @@ -267,12 +297,19 @@ func (p *paramInfo) bindStringSlice(info *tagInfo, expr *tagexpr.TagExpr, a []st
return nil
}
case reflect.Slice:
vv, err := stringsToValue(v.Type().Elem(), a, p.looseZeroMode)
vv, err := stringsToValue(v.Type().Elem(), a, p.looseZeroMode, p.timeLayout)
if err == nil {
v.Set(vv)
return nil
}
fallthrough
case reflect.Struct:
if isTimeType(v.Type()) {
t, _ := time.Parse(p.timeLayout, a[0])
v.Set(reflect.ValueOf(t).Convert(v.Type()))
return nil
}
fallthrough
default:
fn := typeUnmarshalFuncs[v.Type()]
if fn != nil {
Expand All @@ -294,11 +331,16 @@ func (p *paramInfo) bindDefaultVal(expr *tagexpr.TagExpr, defaultValue []byte) (
if err != nil || !v.IsValid() {
return false, err
}
if isTimeType(v.Type()) {
t, _ := time.Parse(p.timeLayout, goutil.BytesToString(defaultValue))
v.Set(reflect.ValueOf(t).Convert(v.Type()))
return true, nil
}
return true, jsonpkg.Unmarshal(defaultValue, v.Addr().Interface())
}

// setDefaultVal preprocess the default tags and store the parsed value
func (p *paramInfo) setDefaultVal() error {
func (p *paramInfo) setDefaultVal() {
for _, info := range p.tagInfos {
if info.paramIn != default_val {
continue
Expand All @@ -319,12 +361,25 @@ func (p *paramInfo) setDefaultVal() error {
}
p.defaultVal = ameda.UnsafeStringToBytes(defaultVal)
}
return nil
}

func (p *paramInfo) SetTimeLayout() {
if !isTimeType(p.structField.Type) {
return
}

if p.timeLayout == "" {
p.timeLayout = defaultLayout
}

if realLayout, ok := layoutMap[p.timeLayout]; ok {
p.timeLayout = realLayout
}
}

var errMismatch = errors.New("type mismatch")

func stringsToValue(t reflect.Type, a []string, emptyAsZero bool) (reflect.Value, error) {
func stringsToValue(t reflect.Type, a []string, emptyAsZero bool, timeLayout string) (reflect.Value, error) {
var i interface{}
var err error
var ptrDepth int
Expand Down Expand Up @@ -363,6 +418,12 @@ func stringsToValue(t reflect.Type, a []string, emptyAsZero bool) (reflect.Value
i, err = goutil.StringsToUint16s(a, emptyAsZero)
case reflect.Uint8:
i, err = goutil.StringsToUint8s(a, emptyAsZero)
case reflect.Struct:
if isTimeType(t) {
i, err = stringsToTime(a, timeLayout, emptyAsZero)
goto End
}
fallthrough
default:
fn := typeUnmarshalFuncs[t]
if fn == nil {
Expand All @@ -378,8 +439,36 @@ func stringsToValue(t reflect.Type, a []string, emptyAsZero bool) (reflect.Value
}
return goutil.ReferenceSlice(v, ptrDepth), nil
}
End:
if err != nil {
return reflect.Value{}, errMismatch
}
return goutil.ReferenceSlice(reflect.ValueOf(i), ptrDepth), nil
}

func stringsToTime(s []string, layout string, emptyAsZero ...bool) ([]time.Time, error) {
var err error
t := make([]time.Time, len(s))
for k, v := range s {
t[k], err = stringToTime(v, layout, emptyAsZero...)
if err != nil {
return t, err
}
}
return t, nil
}

func stringToTime(v string, layout string, emptyAsZero ...bool) (time.Time, error) {
t, err := time.Parse(layout, v)
if err != nil {
if len(emptyAsZero) == 0 || !emptyAsZero[0] {
return time.Time{}, err
}
}
return t, nil
}

func isTimeType(t reflect.Type) bool {
timeType := reflect.TypeOf(time.Time{})
return t == timeType || t.ConvertibleTo(timeType)
}
1 change: 1 addition & 0 deletions binding/receiver.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,6 @@ func (r *receiver) initParams() {
info.contentTypeError = p.bindErrFactory(info.namePath, "does not support binding to the content type body")
}
p.setDefaultVal()
p.SetTimeLayout()
}
}
4 changes: 4 additions & 0 deletions binding/tag_names.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
tagProtobuf = "protobuf"
tagJSON = "json"
tagDefault = "default"
tagLayout = "layout"
)

// Config the struct tag naming and so on
Expand Down Expand Up @@ -49,6 +50,8 @@ type Config struct {
jsonBody string
// defaultVal use 'default' by default when empty
defaultVal string
// timeLayout use 'layout' by default when empty
timeLayout string

list []string
}
Expand All @@ -65,6 +68,7 @@ func (t *Config) init() {
goutil.InitAndGetString(&t.protobufBody, tagProtobuf),
goutil.InitAndGetString(&t.jsonBody, tagJSON),
goutil.InitAndGetString(&t.defaultVal, tagDefault),
goutil.InitAndGetString(&t.timeLayout, tagLayout),
}
}

Expand Down