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 {