From bd88523241c624a7c4eb147fcd9f7f0a981788e1 Mon Sep 17 00:00:00 2001 From: henrylee2cn Date: Mon, 10 Jun 2019 23:14:07 +0800 Subject: [PATCH] refactor: binding Change-Id: Ia06d2cd943891ce81dca32833d4e89b72ff4e316 --- binding/README.md | 65 ++++++------- binding/bind.go | 197 ++++++++++++++-------------------------- binding/bind_test.go | 181 ++++++++++++++++++------------------ binding/example_test.go | 30 +++--- binding/param_info.go | 19 ++-- binding/receiver.go | 55 ++++++++--- binding/tag_names.go | 142 +++++++++++++++++++++++++++++ binding/tools.go | 3 + 8 files changed, 393 insertions(+), 299 deletions(-) create mode 100644 binding/tag_names.go diff --git a/binding/README.md b/binding/README.md index 72f26e8..17c278b 100644 --- a/binding/README.md +++ b/binding/README.md @@ -20,22 +20,22 @@ import ( func Example() { type InfoRequest struct { - Name string `api:"path:'name'"` - Year []int `api:"query:'year'"` - Email *string `api:"body:'email'; @:email($)"` - Friendly bool `api:"body:'friendly'"` - Pie float32 `api:"body:'pie'; required:true"` - Hobby []string `api:"body:'hobby'"` - BodyNotFound *int `api:"body:'xxx'"` - Authorization string `api:"header:'Authorization'; required:true; @:$=='Basic 123456'"` - SessionID string `api:"cookie:'sessionid'; required:true"` + Name string `path:"name"` + Year []int `query:"year"` + Email *string `json:"email" vd:"email($)"` + Friendly bool `json:"friendly"` + Pie float32 `json:"pie,required"` + Hobby []string `json:",required"` + BodyNotFound *int `json:"BodyNotFound"` + Authorization string `header:"Authorization,required" vd:"$=='Basic 123456'"` + SessionID string `cookie:"sessionid,required"` AutoBody string AutoQuery string AutoNotFound *string } args := new(InfoRequest) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(args, requestExample(), new(testPathParams)) fmt.Println("bind and validate result:") @@ -56,7 +56,7 @@ func Example() { // Cookie: sessionid=987654 // // 83 - // {"AutoBody":"autobody_test","email":"henrylee2cn@gmail.com","friendly":true,"hobby":["Coding","Mountain climbing"],"pie":3.1415926} + // {"AutoBody":"autobody_test","Hobby":["Coding","Mountain climbing"],"email":"henrylee2cn@gmail.com","friendly":true,"pie":3.1415926} // 0 // // bind and validate result: @@ -68,9 +68,9 @@ func Example() { // 2018, // 2019 // ], - // "Email": "henrylee2cn@gmail.com", - // "Friendly": true, - // "Pie": 3.1415925, + // "email": "henrylee2cn@gmail.com", + // "friendly": true, + // "pie": 3.1415925, // "Hobby": [ // "Coding", // "Mountain climbing" @@ -86,32 +86,23 @@ func Example() { ... ``` -## Position +## Syntax The parameter position in HTTP request: -|expression|description| -|---------------|-----------| -|`path:'$name'`|URL path parameter -|`query:'$name'`|URL query parameter -|`body:'$name'`|The field in body, support:
`application/json`,
`application/x-www-form-urlencoded`,
`multipart/form-data` -|`header:'$name'`|Header parameter -|`cookie:'$name'`|Cookie parameter +|expression|renameable|description| +|----------|----------|-----------| +|`path:"$name"`|Yes|URL path parameter +|`query:"$name"`|Yes|URL query parameter +|`header:"$name"`|Yes|Header parameter +|`cookie:"$name"`|Yes|Cookie parameter +|`form:"$name"`|Yes|The field in body, support:
`application/x-www-form-urlencoded`,
`multipart/form-data` +|`json:"$name"`|No|The field in body, support:
`application/json` +|`protobuf:"$name"`|No|The field in body, support:
`application/x-protobuf` **NOTE:** -- `'$name'` is variable placeholder -- If `'$name'` is empty, use the name of field -- If no position is tagged, use `body` first, followed by `query` -- Expression `required:true` indicates that the parameter is required - - -## Level - -The level of handling tags: - -|level|default|description| -|-----|-------|-----------| -|OnlyFirst|No|Handle only the first level fields -|FirstAndTagged|Yes|Handle the first level fields and all the tagged fields| -|Any|No|Handle any level fields| \ No newline at end of file +- `"$name"` is variable placeholder +- If `"$name"` is empty, use the name of field +- Expression `$tagname:"$name,required"` indicates that the parameter is required +- If no position is tagged, binding from body first, followed by URL query diff --git a/binding/bind.go b/binding/bind.go index 0d439cb..ffc7abe 100644 --- a/binding/bind.go +++ b/binding/bind.go @@ -11,52 +11,29 @@ import ( "github.com/henrylee2cn/goutil/tpack" ) -// Level the level of handling tags -type Level uint8 - -const ( - // OnlyFirst handle only the first level fields - OnlyFirst Level = iota - // FirstAndTagged handle the first level fields and all the tagged fields - FirstAndTagged - // Any handle any level fields - Any -) - // Binding binding and verification tool for http request type Binding struct { - level Level vd *validator.Validator recvs map[int32]*receiver lock sync.RWMutex bindErrFactory func(failField, msg string) error + tagNames TagNames } // New creates a binding tool. // NOTE: -// If tagName=='', `api` is used -func New(tagName string) *Binding { - if tagName == "" { - tagName = "api" +// Use default tag name for tagNames fields that are empty +func New(tagNames *TagNames) *Binding { + if tagNames == nil { + tagNames = new(TagNames) } b := &Binding{ - vd: validator.New(tagName), - recvs: make(map[int32]*receiver, 1024), - } - return b.SetLevel(FirstAndTagged).SetErrorFactory(nil, nil) -} - -// SetLevel set the level of handling tags. -// NOTE: -// default is First -func (b *Binding) SetLevel(level Level) *Binding { - switch level { - case OnlyFirst, FirstAndTagged, Any: - b.level = level - default: - b.level = FirstAndTagged + recvs: make(map[int32]*receiver, 1024), + tagNames: *tagNames, } - return b + b.tagNames.init() + b.vd = validator.New(b.tagNames.Validator) + return b.SetErrorFactory(nil, nil) } var defaultValidatingErrFactory = newDefaultErrorFactory("invalid parameter") @@ -79,11 +56,7 @@ func (b *Binding) SetErrorFactory(bindErrFactory, validatingErrFactory func(fail // BindAndValidate binds the request parameters and validates them if needed. func (b *Binding) BindAndValidate(structPointer interface{}, req *http.Request, pathParams PathParams) error { - v, err := b.structValueOf(structPointer) - if err != nil { - return err - } - hasVd, err := b.bind(v, req, pathParams) + v, hasVd, err := b.bind(structPointer, req, pathParams) if err != nil { return err } @@ -95,11 +68,7 @@ func (b *Binding) BindAndValidate(structPointer interface{}, req *http.Request, // Bind binds the request parameters. func (b *Binding) Bind(structPointer interface{}, req *http.Request, pathParams PathParams) error { - v, err := b.structValueOf(structPointer) - if err != nil { - return err - } - _, err = b.bind(v, req, pathParams) + _, _, err := b.bind(structPointer, req, pathParams) return err } @@ -143,35 +112,6 @@ func (b *Binding) getObjOrPrepare(value reflect.Value) (*receiver, error) { var errMsg string expr.RangeFields(func(fh *tagexpr.FieldHandler) bool { - paths, name := fh.FieldSelector().Split() - var evals map[tagexpr.ExprSelector]func() interface{} - - switch b.level { - case OnlyFirst: - if len(paths) > 0 { - return true - } - - case FirstAndTagged: - if len(paths) > 0 { - var canHandle bool - evals = fh.EvalFuncs() - for es := range evals { - switch v := es.Name(); v { - case "raw_body", "body", "query", "path", "header", "cookie", "required": - canHandle = true - break - } - } - if !canHandle { - return true - } - } - - default: - // Any - } - if !fh.Value(true).CanSet() { selector := fh.StringSelector() errMsg = "field cannot be set: " + selector @@ -179,60 +119,48 @@ func (b *Binding) getObjOrPrepare(value reflect.Value) (*receiver, error) { return false } - in := auto + tagKVs := b.tagNames.parse(fh.StructField()) p := recv.getOrAddParam(fh, b.bindErrFactory) - if evals == nil { - evals = fh.EvalFuncs() - } - L: - for es, eval := range evals { - switch es.Name() { - case validator.MatchExprName: + for _, tagKV := range tagKVs { + switch tagKV.name { + case b.tagNames.Validator: recv.hasVd = true continue L - case validator.ErrMsgExprName: - continue L - - case "required": - p.required = tagexpr.FakeBool(eval()) - continue L - case "raw_body": - recv.hasRawBody = true - in = raw_body - case "body": - recv.hasBody = true - in = body - case "query": + case b.tagNames.Query: recv.hasQuery = true - in = query - case "path": + p.in = query + case b.tagNames.PathParam: recv.hasPath = true - in = path - case "header": - in = header - case "cookie": + p.in = path + case b.tagNames.Header: + p.in = header + case b.tagNames.Cookie: recv.hasCookie = true - in = cookie - + p.in = cookie + case b.tagNames.RawBody: + recv.hasBody = true + p.in = rawBody + case b.tagNames.FormBody: + recv.hasForm = true + p.in = form + case b.tagNames.protobufBody, b.tagNames.jsonBody: + recv.hasBody = true + p.in = otherBody default: continue L } - - name, errMsg = getParamName(eval, name) - if errMsg != "" { - errExprSelector = es - return false - } + p.name, p.required = tagKV.defaultSplit() + break L } - - if in == auto { + if !recv.hasVd { + _, recv.hasVd = tagKVs.lookup(b.tagNames.Validator) + } + if p.in == auto { recv.hasBody = true recv.hasAuto = true } - p.in = in - p.name = name return true }) @@ -249,27 +177,35 @@ func (b *Binding) getObjOrPrepare(value reflect.Value) (*receiver, error) { return recv, nil } -func (b *Binding) bind(value reflect.Value, req *http.Request, pathParams PathParams) (hasVd bool, err error) { +func (b *Binding) bind(structPointer interface{}, req *http.Request, pathParams PathParams) (value reflect.Value, hasVd bool, err error) { + value, err = b.structValueOf(structPointer) + if err != nil { + return + } recv, err := b.getObjOrPrepare(value) if err != nil { - return false, err + return } expr, err := b.vd.VM().Run(value) if err != nil { - return false, err + return } bodyCodec := recv.getBodyCodec(req) - bodyBytes, bodyString, err := recv.getBody(req, bodyCodec == jsonBody) + bodyBytes, bodyString, err := recv.getBody(req) if err != nil { - return false, err + return + } + err = recv.bindOtherBody(structPointer, value, bodyCodec, bodyBytes) + if err != nil { + return } - postForm, err := recv.getPostForm(req, bodyCodec == formBody) + postForm, err := recv.getPostForm(req, bodyCodec) if err != nil { - return false, err + return } queryValues := recv.getQuery(req) @@ -285,32 +221,33 @@ func (b *Binding) bind(value reflect.Value, req *http.Request, pathParams PathPa _, err = param.bindHeader(expr, req.Header) case cookie: err = param.bindCookie(expr, cookies) - case body: + case rawBody: + err = param.bindRawBody(expr, bodyBytes) + case otherBody: switch bodyCodec { - case formBody: + case bodyForm: _, err = param.bindMapStrings(expr, postForm) - case jsonBody: - _, err = param.bindJSON(expr, bodyString) + case bodyJSON: + err = param.requireJSON(expr, bodyString) + case bodyProtobuf: default: err = param.contentTypeError } - case raw_body: - err = param.bindRawBody(expr, bodyBytes) default: var found bool - switch bodyCodec { - case formBody: + if bodyCodec == bodyForm { found, err = param.bindMapStrings(expr, postForm) - case jsonBody: - found, err = param.bindJSON(expr, bodyString) } if !found { + if queryValues == nil { + queryValues = req.URL.Query() + } _, err = param.bindQuery(expr, queryValues) } } if err != nil { - return recv.hasVd, err + return value, recv.hasVd, err } } - return recv.hasVd, nil + return value, recv.hasVd, nil } diff --git a/binding/bind_test.go b/binding/bind_test.go index ffe3c20..5459ddc 100644 --- a/binding/bind_test.go +++ b/binding/bind_test.go @@ -18,19 +18,19 @@ import ( func TestRawBody(t *testing.T) { type Recv struct { rawBody **struct { - A []byte `api:"raw_body:nil"` - B *[]byte `api:"raw_body:nil"` - C **[]byte `api:"raw_body:nil"` - D string `api:"raw_body:nil"` - E *string `api:"raw_body:nil"` - F **string `api:"raw_body:nil; @:len($)<3; msg:'too long'"` + A []byte `raw_body:""` + B *[]byte `raw_body:""` + C **[]byte `raw_body:""` + D string `raw_body:""` + E *string `raw_body:""` + F **string `raw_body:"" vd:"@:len($)<3; msg:'too long'"` } - S string `api:"raw_body:nil"` + S string `raw_body:""` } bodyBytes := []byte("rawbody.............") req := newRequest("", nil, nil, bytes.NewReader(bodyBytes)) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) assert.NotNil(t, err) assert.Equal(t, err.Error(), "too long") @@ -50,17 +50,17 @@ func TestRawBody(t *testing.T) { func TestQueryString(t *testing.T) { type Recv struct { X **struct { - A []string `api:"query:'a'"` - B string `api:"query:'b'"` - C *[]string `api:"query:'c'; required:true"` - D *string `api:"query:'d'"` + A []string `query:"a"` + B string `query:"b"` + C *[]string `query:"c,required"` + D *string `query:"d"` } - Y string `api:"query:'y'; required:true"` - Z *string `api:"query:'z'"` + Y string `query:"y,required"` + Z *string `query:"z"` } req := newRequest("http://localhost:8080/?a=a1&a=a2&b=b1&c=c1&c=c2&d=d1&d=d2&y=y1", nil, nil, nil) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) assert.Nil(t, err) assert.Equal(t, []string{"a1", "a2"}, (**recv.X).A) @@ -74,17 +74,17 @@ func TestQueryString(t *testing.T) { func TestQueryNum(t *testing.T) { type Recv struct { X **struct { - A []int `api:"query:'a'"` - B int32 `api:"query:'b'"` - C *[]uint16 `api:"query:'c'; required:true"` - D *float32 `api:"query:'d'"` + A []int `query:"a"` + B int32 `query:"b"` + C *[]uint16 `query:"c,required"` + D *float32 `query:"d"` } - Y bool `api:"query:'y'; required:true"` - Z *int64 `api:"query:'z'"` + Y bool `query:"y,required"` + Z *int64 `query:"z"` } req := newRequest("http://localhost:8080/?a=11&a=12&b=21&c=31&c=32&d=41&d=42&y=true", nil, nil, nil) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) assert.Nil(t, err) assert.Equal(t, []int{11, 12}, (**recv.X).A) @@ -98,13 +98,13 @@ func TestQueryNum(t *testing.T) { func TestHeaderString(t *testing.T) { type Recv struct { X **struct { - A []string `api:"header:'X-A'"` - B string `api:"header:'X-B'"` - C *[]string `api:"header:'X-C'; required:true"` - D *string `api:"header:'X-D'"` + A []string `header:"X-A"` + B string `header:"X-B"` + C *[]string `header:"X-C,required"` + D *string `header:"X-D"` } - Y string `api:"header:'X-Y'; required:true"` - Z *string `api:"header:'X-Z'"` + Y string `header:"X-Y,required"` + Z *string `header:"X-Z"` } header := make(http.Header) header.Add("X-A", "a1") @@ -117,7 +117,7 @@ func TestHeaderString(t *testing.T) { header.Add("X-Y", "y1") req := newRequest("", header, nil, nil) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) assert.Nil(t, err) assert.Equal(t, []string{"a1", "a2"}, (**recv.X).A) @@ -131,13 +131,13 @@ func TestHeaderString(t *testing.T) { func TestHeaderNum(t *testing.T) { type Recv struct { X **struct { - A []int `api:"header:'X-A'"` - B int32 `api:"header:'X-B'"` - C *[]uint16 `api:"header:'X-C'; required:true"` - D *float32 `api:"header:'X-D'"` + A []int `header:"X-A"` + B int32 `header:"X-B"` + C *[]uint16 `header:"X-C,required"` + D *float32 `header:"X-D"` } - Y bool `api:"header:'X-Y'; required:true"` - Z *int64 `api:"header:'X-Z'"` + Y bool `header:"X-Y,required"` + Z *int64 `header:"X-Z"` } header := make(http.Header) header.Add("X-A", "11") @@ -150,7 +150,7 @@ func TestHeaderNum(t *testing.T) { header.Add("X-Y", "true") req := newRequest("", header, nil, nil) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) assert.Nil(t, err) assert.Equal(t, []int{11, 12}, (**recv.X).A) @@ -164,13 +164,13 @@ func TestHeaderNum(t *testing.T) { func TestCookieString(t *testing.T) { type Recv struct { X **struct { - A []string `api:"cookie:'a'"` - B string `api:"cookie:'b'"` - C *[]string `api:"cookie:'c'; required:true"` - D *string `api:"cookie:'d'"` + A []string `cookie:"a"` + B string `cookie:"b"` + C *[]string `cookie:"c,required"` + D *string `cookie:"d"` } - Y string `api:"cookie:'y'; required:true"` - Z *string `api:"cookie:'z'"` + Y string `cookie:"y,required"` + Z *string `cookie:"z"` } cookies := []*http.Cookie{ {Name: "a", Value: "a1"}, @@ -184,7 +184,7 @@ func TestCookieString(t *testing.T) { } req := newRequest("", nil, cookies, nil) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) assert.Nil(t, err) assert.Equal(t, []string{"a1", "a2"}, (**recv.X).A) @@ -198,13 +198,13 @@ func TestCookieString(t *testing.T) { func TestCookieNum(t *testing.T) { type Recv struct { X **struct { - A []int `api:"cookie:'a'"` - B int32 `api:"cookie:'b'"` - C *[]uint16 `api:"cookie:'c'; required:true"` - D *float32 `api:"cookie:'d'"` + A []int `cookie:"a"` + B int32 `cookie:"b"` + C *[]uint16 `cookie:"c,required"` + D *float32 `cookie:"d"` } - Y bool `api:"cookie:'y'; required:true"` - Z *int64 `api:"cookie:'z'"` + Y bool `cookie:"y,required"` + Z *int64 `cookie:"z"` } cookies := []*http.Cookie{ {Name: "a", Value: "11"}, @@ -218,7 +218,7 @@ func TestCookieNum(t *testing.T) { } req := newRequest("", nil, cookies, nil) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) assert.Nil(t, err) assert.Equal(t, []int{11, 12}, (**recv.X).A) @@ -232,13 +232,13 @@ func TestCookieNum(t *testing.T) { func TestFormString(t *testing.T) { type Recv struct { X **struct { - A []string `api:"body:'a'"` - B string `api:"body:'b'"` - C *[]string `api:"body:'c'; required:true"` - D *string `api:"body:'d'"` + A []string `form:"a"` + B string `form:"b"` + C *[]string `form:"c,required"` + D *string `form:"d"` } - Y string `api:"body:'y'; required:true"` - Z *string `api:"body:'z'"` + Y string `form:"y,required"` + Z *string `form:"z"` } values := make(url.Values) values.Add("a", "a1") @@ -259,7 +259,7 @@ func TestFormString(t *testing.T) { header.Set("Content-Type", contentType) req := newRequest("", header, nil, bodyReader) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) assert.Nil(t, err) assert.Equal(t, []string{"a1", "a2"}, (**recv.X).A) @@ -274,13 +274,13 @@ func TestFormString(t *testing.T) { func TestFormNum(t *testing.T) { type Recv struct { X **struct { - A []int `api:"body:'a'"` - B int32 `api:"body:'b'"` - C *[]uint16 `api:"body:'c'; required:true"` - D *float32 `api:"body:'d'"` + A []int `form:"a"` + B int32 `form:"b"` + C *[]uint16 `form:"c,required"` + D *float32 `form:"d"` } - Y bool `api:"body:'y'; required:true"` - Z *int64 `api:"body:'z'"` + Y bool `form:"y,required"` + Z *int64 `form:"z"` } values := make(url.Values) values.Add("a", "11") @@ -301,7 +301,7 @@ func TestFormNum(t *testing.T) { header.Set("Content-Type", contentType) req := newRequest("", header, nil, bodyReader) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) assert.Nil(t, err) assert.Equal(t, []int{11, 12}, (**recv.X).A) @@ -316,13 +316,13 @@ func TestFormNum(t *testing.T) { func TestJSON(t *testing.T) { type Recv struct { X **struct { - A []string `api:"body:'a'"` - B int32 `api:""` - C *[]uint16 `api:"required:true"` - D *float32 `api:"body:'d'"` + A []string `json:"a"` + B int32 `json:""` + C *[]uint16 `json:",required"` + D *float32 `json:"d"` } - Y string `api:"body:'y'; required:true"` - Z *int64 `api:""` + Y string `json:"y,required"` + Z *int64 } bodyReader := strings.NewReader(`{ @@ -332,35 +332,36 @@ func TestJSON(t *testing.T) { "C": [31,32], "d": 41 }, - "y": "y1" + "Z": 6 }`) header := make(http.Header) header.Set("Content-Type", "application/json") req := newRequest("", header, nil, bodyReader) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) - assert.Nil(t, err) + assert.NotNil(t, err) + assert.Equal(t, "missing required parameter", err.Error()) assert.Equal(t, []string{"a1", "a2"}, (**recv.X).A) assert.Equal(t, int32(21), (**recv.X).B) assert.Equal(t, &[]uint16{31, 32}, (**recv.X).C) assert.Equal(t, float32(41), *(**recv.X).D) - assert.Equal(t, "y1", recv.Y) - assert.Equal(t, (*int64)(nil), recv.Z) + assert.Equal(t, "", recv.Y) + assert.Equal(t, (int64)(6), *recv.Z) } func BenchmarkBindJSON(b *testing.B) { type Recv struct { X **struct { - A []string `api:"body:'a'"` + A []string `form:"a"` B int32 C *[]uint16 - D *float32 `api:"body:'d'"` + D *float32 `form:"d"` } - Y string `api:"body:'y'"` + Y string `form:"y"` } - binder := binding.New("api") + binder := binding.New(nil) header := make(http.Header) header.Set("Content-Type", "application/json") test := func() { @@ -455,18 +456,18 @@ func (testPathParams) Get(name string) (string, bool) { func TestPath(t *testing.T) { type Recv struct { X **struct { - A []string `api:"path:'a'"` - B int32 `api:"path:'b'"` - C *[]uint16 `api:"path:'c'; required:true"` - D *float32 `api:"path:'d'"` + A []string `path:"a"` + B int32 `path:"b"` + C *[]uint16 `path:"c,required"` + D *float32 `path:"d"` } - Y string `api:"path:'y'; required:true"` + Y string `path:"y,required"` Z *int64 } req := newRequest("", nil, nil, nil) recv := new(Recv) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, new(testPathParams)) assert.Nil(t, err) assert.Equal(t, []string{"a1"}, (**recv.X).A) @@ -480,12 +481,12 @@ func TestPath(t *testing.T) { func TestAuto(t *testing.T) { type Recv struct { X **struct { - A []string `api:""` - B int32 `api:""` - C *[]uint16 `api:"required:true"` + A []string + B int32 + C *[]uint16 `vd:"$!=nil"` D *float32 } - Y string `api:"required:true"` + Y string `vd:"$!=''"` Z *int64 } query := make(url.Values) @@ -509,7 +510,7 @@ func TestAuto(t *testing.T) { header.Set("Content-Type", contentType) req := newRequest("http://localhost/?"+query.Encode(), header, nil, bodyReader) recv := new(Recv) - binder := binding.New("api").SetLevel(binding.Any) + binder := binding.New(nil) err := binder.BindAndValidate(recv, req, nil) assert.Nil(t, err) assert.Equal(t, []string{"a1", "a2"}, (**recv.X).A) diff --git a/binding/example_test.go b/binding/example_test.go index 5d1b4f6..4125053 100644 --- a/binding/example_test.go +++ b/binding/example_test.go @@ -13,22 +13,22 @@ import ( func Example() { type InfoRequest struct { - Name string `api:"path:'name'"` - Year []int `api:"query:'year'"` - Email *string `api:"body:'email'; @:email($)"` - Friendly bool `api:"body:'friendly'"` - Pie float32 `api:"body:'pie'; required:true"` - Hobby []string `api:"body:'hobby'"` - BodyNotFound *int `api:"body:'xxx'"` - Authorization string `api:"header:'Authorization'; required:true; @:$=='Basic 123456'"` - SessionID string `api:"cookie:'sessionid'; required:true"` + Name string `path:"name"` + Year []int `query:"year"` + Email *string `json:"email" vd:"email($)"` + Friendly bool `json:"friendly"` + Pie float32 `json:"pie,required"` + Hobby []string `json:",required"` + BodyNotFound *int `json:"BodyNotFound"` + Authorization string `header:"Authorization,required" vd:"$=='Basic 123456'"` + SessionID string `cookie:"sessionid,required"` AutoBody string AutoQuery string AutoNotFound *string } args := new(InfoRequest) - binder := binding.New("api") + binder := binding.New(nil) err := binder.BindAndValidate(args, requestExample(), new(testPathParams)) fmt.Println("bind and validate result:") @@ -49,7 +49,7 @@ func Example() { // Cookie: sessionid=987654 // // 83 - // {"AutoBody":"autobody_test","email":"henrylee2cn@gmail.com","friendly":true,"hobby":["Coding","Mountain climbing"],"pie":3.1415926} + // {"AutoBody":"autobody_test","Hobby":["Coding","Mountain climbing"],"email":"henrylee2cn@gmail.com","friendly":true,"pie":3.1415926} // 0 // // bind and validate result: @@ -61,9 +61,9 @@ func Example() { // 2018, // 2019 // ], - // "Email": "henrylee2cn@gmail.com", - // "Friendly": true, - // "Pie": 3.1415925, + // "email": "henrylee2cn@gmail.com", + // "friendly": true, + // "pie": 3.1415925, // "Hobby": [ // "Coding", // "Mountain climbing" @@ -82,7 +82,7 @@ func requestExample() *http.Request { "email": "henrylee2cn@gmail.com", "friendly": true, "pie": 3.1415926, - "hobby": []string{"Coding", "Mountain climbing"}, + "Hobby": []string{"Coding", "Mountain climbing"}, "AutoBody": "autobody_test", }) header := make(http.Header) diff --git a/binding/param_info.go b/binding/param_info.go index c6a2189..78bdad4 100644 --- a/binding/param_info.go +++ b/binding/param_info.go @@ -7,7 +7,6 @@ import ( "strconv" "github.com/bytedance/go-tagexpr" - "github.com/bytedance/go-tagexpr/binding/jsonparam" "github.com/henrylee2cn/goutil" "github.com/tidwall/gjson" ) @@ -101,20 +100,14 @@ func (p *paramInfo) bindCookie(expr *tagexpr.TagExpr, cookies []*http.Cookie) er return p.bindStringSlice(expr, r) } -func (p *paramInfo) bindJSON(expr *tagexpr.TagExpr, bodyString string) (bool, error) { - r := gjson.Get(bodyString, p.namePath) - if !r.Exists() { - if p.required { - return false, p.requiredError +func (p *paramInfo) requireJSON(expr *tagexpr.TagExpr, bodyString string) error { + if p.required { + r := gjson.Get(bodyString, p.namePath) + if !r.Exists() { + return p.requiredError } - return false, nil - } - v, err := p.getField(expr) - if err != nil || !v.IsValid() { - return false, err } - jsonparam.Assign(r, v) - return true, nil + return nil } func (p *paramInfo) bindMapStrings(expr *tagexpr.TagExpr, values map[string][]string) (bool, error) { diff --git a/binding/receiver.go b/binding/receiver.go index dfacd52..f1da3fd 100644 --- a/binding/receiver.go +++ b/binding/receiver.go @@ -1,12 +1,17 @@ package binding import ( + "errors" "net/http" "net/url" + "reflect" "strings" "github.com/bytedance/go-tagexpr" + "github.com/bytedance/go-tagexpr/binding/jsonparam" + "github.com/gogo/protobuf/proto" "github.com/henrylee2cn/goutil" + "github.com/tidwall/gjson" ) const ( @@ -15,18 +20,20 @@ const ( path header cookie - body - raw_body + rawBody + form + otherBody ) const ( - unsupportBody uint8 = iota - jsonBody - formBody + bodyUnsupport int8 = iota + bodyForm + bodyJSON + bodyProtobuf ) type receiver struct { - hasAuto, hasQuery, hasCookie, hasPath, hasBody, hasRawBody, hasVd bool + hasAuto, hasQuery, hasCookie, hasPath, hasForm, hasBody, hasVd bool params []*paramInfo } @@ -47,14 +54,16 @@ func (r *receiver) getOrAddParam(fh *tagexpr.FieldHandler, bindErrFactory func(f return p } p = new(paramInfo) + p.in = auto p.fieldSelector = fieldSelector p.structField = fh.StructField() + p.name = p.structField.Name p.bindErrFactory = bindErrFactory r.params = append(r.params, p) return p } -func (r *receiver) getBodyCodec(req *http.Request) uint8 { +func (r *receiver) getBodyCodec(req *http.Request) int8 { ct := req.Header.Get("Content-Type") idx := strings.Index(ct, ";") if idx != -1 { @@ -62,16 +71,18 @@ func (r *receiver) getBodyCodec(req *http.Request) uint8 { } switch ct { case "application/json": - return jsonBody + return bodyJSON + case "application/x-protobuf": + return bodyProtobuf case "application/x-www-form-urlencoded", "multipart/form-data": - return formBody + return bodyForm default: - return unsupportBody + return bodyUnsupport } } -func (r *receiver) getBody(req *http.Request, must bool) ([]byte, string, error) { - if must || r.hasRawBody { +func (r *receiver) getBody(req *http.Request) ([]byte, string, error) { + if r.hasBody { bodyBytes, err := copyBody(req) if err == nil { return bodyBytes, goutil.BytesToString(bodyBytes), nil @@ -81,12 +92,28 @@ func (r *receiver) getBody(req *http.Request, must bool) ([]byte, string, error) return nil, "", nil } +func (r *receiver) bindOtherBody(structPointer interface{}, value reflect.Value, bodyCodec int8, bodyBytes []byte) error { + switch bodyCodec { + case bodyJSON: + jsonparam.Assign(gjson.Parse(goutil.BytesToString(bodyBytes)), value) + case bodyProtobuf: + msg, ok := structPointer.(proto.Message) + if !ok { + return errors.New("protobuf content type is not supported") + } + if err := proto.Unmarshal(bodyBytes, msg); err != nil { + return err + } + } + return nil +} + const ( defaultMaxMemory = 32 << 20 // 32 MB ) -func (r *receiver) getPostForm(req *http.Request, must bool) (url.Values, error) { - if must { +func (r *receiver) getPostForm(req *http.Request, bodyCodec int8) (url.Values, error) { + if bodyCodec == bodyForm && (r.hasForm || r.hasBody) { if req.PostForm == nil { req.ParseMultipartForm(defaultMaxMemory) } diff --git a/binding/tag_names.go b/binding/tag_names.go new file mode 100644 index 0000000..a816084 --- /dev/null +++ b/binding/tag_names.go @@ -0,0 +1,142 @@ +package binding + +import ( + "reflect" + "sort" + "strings" +) + +// TagNames struct tag naming +type TagNames struct { + // PathParam use 'path' by default when empty + PathParam string + // Query use 'query' by default when empty + Query string + // Header use 'header' by default when empty + Header string + // Cookie use 'cookie' by default when empty + Cookie string + // RawBody use 'raw' by default when empty + RawBody string + // FormBody use 'form' by default when empty + FormBody string + // Validator use 'vd' by default when empty + Validator string + // protobufBody use 'protobuf' by default when empty + protobufBody string + // jsonBody use 'json' by default when empty + jsonBody string + + list []string +} + +func (t *TagNames) init() { + setDefault(&t.PathParam, "path") + setDefault(&t.Query, "query") + setDefault(&t.Header, "header") + setDefault(&t.Cookie, "cookie") + setDefault(&t.RawBody, "raw_body") + setDefault(&t.FormBody, "form") + setDefault(&t.Validator, "vd") + setDefault(&t.protobufBody, "protobuf") + setDefault(&t.jsonBody, "json") + t.list = []string{ + t.PathParam, + t.Query, + t.Header, + t.Cookie, + t.RawBody, + t.FormBody, + t.Validator, + t.protobufBody, + t.jsonBody, + } +} + +func setDefault(s *string, def string) { + if *s == "" { + *s = def + } +} + +func (t *TagNames) parse(field reflect.StructField) tagKVs { + tag := field.Tag + fieldName := field.Name + + kvs := make(tagKVs, 0, len(t.list)) + s := string(tag) + + for _, name := range t.list { + value, ok := tag.Lookup(name) + if !ok { + continue + } + value = strings.Replace(strings.TrimSpace(value), " ", "", -1) + value = strings.Replace(value, "\t", "", -1) + if name == t.RawBody { + if _, required := defaultSplitTag(value); required { + value = "," + tagRequired + } + } else if value == "" { + value = fieldName + } else if value == ","+tagRequired { + value = fieldName + value + } + kvs = append(kvs, &tagKV{name: name, value: value, pos: strings.Index(s, name)}) + } + sort.Sort(kvs) + return kvs +} + +type tagKV struct { + name string + value string + pos int +} + +const tagRequired = "required" + +func (t *tagKV) defaultSplit() (paramName string, required bool) { + return defaultSplitTag(t.value) +} + +func defaultSplitTag(value string) (paramName string, required bool) { + for i, v := range strings.Split(value, ",") { + v = strings.TrimSpace(v) + if i == 0 { + paramName = v + } else { + if v == tagRequired { + required = true + } + } + } + return paramName, required +} + +type tagKVs []*tagKV + +// Len is the number of elements in the collection. +func (a tagKVs) Len() int { + return len(a) +} + +// Less reports whether the element with +// index i should sort before the element with index j. +func (a tagKVs) Less(i, j int) bool { + return a[i].pos < a[j].pos +} + +// Swap swaps the elements with indexes i and j. +func (a tagKVs) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a tagKVs) lookup(name string) (string, bool) { + for _, v := range a { + if v.name == name { + return v.value, true + } + } + return "", false +} diff --git a/binding/tools.go b/binding/tools.go index 9a27f6e..e3774be 100644 --- a/binding/tools.go +++ b/binding/tools.go @@ -11,6 +11,9 @@ import ( ) func copyBody(req *http.Request) ([]byte, error) { + if req.Body == nil { + return nil, nil + } b, err := ioutil.ReadAll(req.Body) req.Body.Close() if err != nil {