├── .gitignore ├── util_test.go ├── example ├── example.go ├── server_fasthttp.go └── server_http.go ├── util.go ├── error.go ├── apiware.go ├── doc.go ├── convert.go ├── paramapi_test.go ├── README.md ├── param.go ├── LICENSE └── paramapi.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.rar 4 | *.psd 5 | *.sublime-project 6 | *.sublime-workspace -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package apiware 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInterfaceToSnake(t *testing.T) { 8 | type SampleModel struct{} 9 | name := interfaceToSnake(&SampleModel{}) 10 | if name != "sample_model" { 11 | t.Fatal("wrong table name", name) 12 | } 13 | name = interfaceToSnake(SampleModel{}) 14 | if name != "sample_model" { 15 | t.Fatal("wrong table name", name) 16 | } 17 | } 18 | 19 | func TestSnakeToUpperCamel(t *testing.T) { 20 | if s := snakeToUpperCamel("table_name"); s != "TableName" { 21 | t.Fatal("wrong string", s) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/henrylee2cn/apiware" 5 | "strings" 6 | ) 7 | 8 | var myApiware = apiware.New(pathDecodeFunc, nil, nil) 9 | 10 | var pattern = "/test/:id" 11 | 12 | func pathDecodeFunc(urlPath, pattern string) apiware.KV { 13 | idx := map[int]string{} 14 | for k, v := range strings.Split(pattern, "/") { 15 | if !strings.HasPrefix(v, ":") { 16 | continue 17 | } 18 | idx[k] = v[1:] 19 | } 20 | pathParams := make(map[string]string, len(idx)) 21 | for k, v := range strings.Split(urlPath, "/") { 22 | name, ok := idx[k] 23 | if !ok { 24 | continue 25 | } 26 | pathParams[name] = v 27 | } 28 | return apiware.Map(pathParams) 29 | } 30 | 31 | func main() { 32 | // Check whether these structs meet the requirements of apiware, and register them 33 | err := myApiware.Register( 34 | new(httpTestApiware), 35 | new(fasthttpTestApiware), 36 | ) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | // http server 42 | println("[http] listen on :8080") 43 | go httpServer(":8080") 44 | // fasthttp server 45 | println("[fasthttp] listen on :8081") 46 | fasthttpServer(":8081") 47 | } 48 | -------------------------------------------------------------------------------- /example/server_fasthttp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | // "mime/multipart" 6 | "github.com/valyala/fasthttp" 7 | "net/http" 8 | ) 9 | 10 | type fasthttpTestApiware struct { 11 | Id int `param:"in(path),required,desc(ID),range(1:2)"` 12 | Num float32 `param:"in(query),name(n),range(0.1:10.19)"` 13 | Title string `param:"in(query),nonzero"` 14 | Paragraph []string `param:"in(query),name(p),len(1:10)" regexp:"(^[\\w]*$)"` 15 | Cookie int `param:"in(cookie),name(apiwareid)"` 16 | // Picture multipart.FileHeader `param:"in(formData),name(pic),maxmb(30)"` 17 | } 18 | 19 | func fasthttpTestHandler(ctx *fasthttp.RequestCtx) { 20 | // set cookies 21 | var c fasthttp.Cookie 22 | c.SetKey("apiwareid") 23 | c.SetValue("123") 24 | ctx.Response.Header.SetCookie(&c) 25 | 26 | // bind params 27 | params := new(fasthttpTestApiware) 28 | err := myApiware.FasthttpBind(params, ctx, pattern) 29 | b, _ := json.MarshalIndent(params, "", " ") 30 | 31 | if err != nil { 32 | ctx.SetStatusCode(http.StatusBadRequest) 33 | ctx.Write(append([]byte(err.Error()+"\n"), b...)) 34 | } else { 35 | ctx.SetStatusCode(http.StatusOK) 36 | ctx.Write(b) 37 | } 38 | } 39 | 40 | func fasthttpServer(addr string) { 41 | // server 42 | fasthttp.ListenAndServe(addr, fasthttpTestHandler) 43 | } 44 | -------------------------------------------------------------------------------- /example/server_http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | // "mime/multipart" 6 | "net/http" 7 | ) 8 | 9 | type httpTestApiware struct { 10 | Id int `param:"in(path),required,desc(ID),range(1:2)"` 11 | Num float32 `param:"in(query),name(n),range(0.1:10.19)"` 12 | Title string `param:"in(query),nonzero"` 13 | Paragraph []string `param:"in(query),name(p),len(1:10)" regexp:"(^[\\w]*$)"` 14 | Cookie http.Cookie `param:"in(cookie),name(apiwareid),nonzero"` 15 | CookieValue int `param:"in(cookie),name(apiwareid)"` 16 | // Picture multipart.FileHeader `param:"in(formData),name(pic),maxmb(30)"` 17 | } 18 | 19 | func httpTestHandler(resp http.ResponseWriter, req *http.Request) { 20 | // set cookies 21 | http.SetCookie(resp, &http.Cookie{ 22 | Name: "apiwareid", 23 | Value: "123", 24 | }) 25 | 26 | // bind params 27 | params := new(httpTestApiware) 28 | err := myApiware.Bind(params, req, pattern) 29 | b, _ := json.MarshalIndent(params, "", " ") 30 | 31 | if err != nil { 32 | resp.WriteHeader(http.StatusBadRequest) 33 | resp.Write(append([]byte(err.Error()+"\n"), b...)) 34 | } else { 35 | resp.WriteHeader(http.StatusOK) 36 | resp.Write(b) 37 | } 38 | } 39 | 40 | func httpServer(addr string) { 41 | // server 42 | http.HandleFunc("/test/0", httpTestHandler) 43 | http.HandleFunc("/test/1", httpTestHandler) 44 | http.HandleFunc("/test/1.1", httpTestHandler) 45 | http.HandleFunc("/test/2", httpTestHandler) 46 | http.HandleFunc("/test/3", httpTestHandler) 47 | http.ListenAndServe(addr, nil) 48 | } 49 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 HenryLee. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apiware 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "reflect" 21 | "strings" 22 | ) 23 | 24 | func toSnake(s string) string { 25 | buf := bytes.NewBufferString("") 26 | for i, v := range s { 27 | if i > 0 && v >= 'A' && v <= 'Z' { 28 | buf.WriteRune('_') 29 | } 30 | buf.WriteRune(v) 31 | } 32 | return strings.ToLower(buf.String()) 33 | } 34 | 35 | func interfaceToSnake(f interface{}) string { 36 | t := reflect.TypeOf(f) 37 | for { 38 | c := false 39 | switch t.Kind() { 40 | case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice: 41 | t = t.Elem() 42 | c = true 43 | } 44 | if !c { 45 | break 46 | } 47 | } 48 | return toSnake(t.Name()) 49 | } 50 | 51 | func snakeToUpperCamel(s string) string { 52 | buf := bytes.NewBufferString("") 53 | for _, v := range strings.Split(s, "_") { 54 | if len(v) > 0 { 55 | buf.WriteString(strings.ToUpper(v[:1])) 56 | buf.WriteString(v[1:]) 57 | } 58 | } 59 | return buf.String() 60 | } 61 | 62 | func bodyJONS(dest reflect.Value, body []byte) error { 63 | var err error 64 | if dest.Kind() == reflect.Ptr { 65 | err = json.Unmarshal(body, dest.Interface()) 66 | } else { 67 | err = json.Unmarshal(body, dest.Addr().Interface()) 68 | } 69 | return err 70 | } 71 | 72 | type ( 73 | KV interface { 74 | Get(k string) (v string, found bool) 75 | } 76 | Map map[string]string 77 | ) 78 | 79 | func (m Map) Get(k string) (string, bool) { 80 | v, found := m[k] 81 | return v, found 82 | } 83 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 HenryLee. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apiware 16 | 17 | const ( 18 | ValidationErrorValueNotSet = (1<<16 + iota) 19 | ValidationErrorValueTooSmall 20 | ValidationErrorValueTooBig 21 | ValidationErrorValueTooShort 22 | ValidationErrorValueTooLong 23 | ValidationErrorValueNotMatch 24 | ) 25 | 26 | // Validation error type 27 | type ValidationError struct { 28 | kind int 29 | field string 30 | } 31 | 32 | // NewValidationError returns a new validation error with the specified id and 33 | // text. The id's purpose is to distinguish different validation error types. 34 | // Built-in validation error ids start at 65536, so you should keep your custom 35 | // ids under that value. 36 | func NewValidationError(id int, field string) error { 37 | return &ValidationError{id, field} 38 | } 39 | 40 | func (e *ValidationError) Error() string { 41 | kindStr := "" 42 | switch e.kind { 43 | case ValidationErrorValueNotSet: 44 | kindStr = " not set" 45 | case ValidationErrorValueTooBig: 46 | kindStr = " too big" 47 | case ValidationErrorValueTooLong: 48 | kindStr = " too long" 49 | case ValidationErrorValueTooSmall: 50 | kindStr = " too small" 51 | case ValidationErrorValueTooShort: 52 | kindStr = " too short" 53 | case ValidationErrorValueNotMatch: 54 | kindStr = " not match" 55 | } 56 | return e.field + kindStr 57 | } 58 | 59 | func (e *ValidationError) Kind() int { 60 | return e.kind 61 | } 62 | 63 | func (e *ValidationError) Field() string { 64 | return e.field 65 | } 66 | 67 | type Error struct { 68 | Api string `json:"api"` 69 | Param string `json:"param"` 70 | Reason string `json:"reason"` 71 | } 72 | 73 | func NewError(api string, param string, reason string) *Error { 74 | return &Error{ 75 | Api: api, 76 | Param: param, 77 | Reason: reason, 78 | } 79 | } 80 | 81 | var _ error = new(Error) 82 | 83 | func (e *Error) Error() string { 84 | return "[apiware] " + e.Api + " | " + e.Param + " | " + e.Reason 85 | } 86 | -------------------------------------------------------------------------------- /apiware.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 HenryLee. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apiware 16 | 17 | import ( 18 | "errors" 19 | "net/http" 20 | 21 | "github.com/valyala/fasthttp" 22 | ) 23 | 24 | type ( 25 | Apiware struct { 26 | ParamNameFunc 27 | PathDecodeFunc 28 | BodyDecodeFunc 29 | } 30 | 31 | // Parse path params function, return pathParams of KV type 32 | PathDecodeFunc func(urlPath, pattern string) (pathParams KV) 33 | ) 34 | 35 | // Create a new apiware engine. 36 | // Parse and store the struct object, requires a struct pointer, 37 | // if `paramNameFunc` is nil, `paramNameFunc=toSnake`, 38 | // if `bodyDecodeFunc` is nil, `bodyDecodeFunc=bodyJONS`, 39 | func New(pathDecodeFunc PathDecodeFunc, bodyDecodeFunc BodyDecodeFunc, paramNameFunc ParamNameFunc) *Apiware { 40 | return &Apiware{ 41 | ParamNameFunc: paramNameFunc, 42 | PathDecodeFunc: pathDecodeFunc, 43 | BodyDecodeFunc: bodyDecodeFunc, 44 | } 45 | } 46 | 47 | // Check whether structs meet the requirements of apiware, and register them. 48 | // note: requires a structure pointer. 49 | func (a *Apiware) Register(structPointers ...interface{}) error { 50 | var errStr string 51 | for _, obj := range structPointers { 52 | err := Register(obj, a.ParamNameFunc, a.BodyDecodeFunc) 53 | if err != nil { 54 | errStr += err.Error() + "\n" 55 | } 56 | } 57 | if len(errStr) > 0 { 58 | return errors.New(errStr) 59 | } 60 | return nil 61 | } 62 | 63 | // Bind the net/http request params to the structure and validate. 64 | // note: structPointer must be structure pointer. 65 | func (a *Apiware) Bind( 66 | structPointer interface{}, 67 | req *http.Request, 68 | pattern string, 69 | ) error { 70 | return Bind(structPointer, req, a.PathDecodeFunc(req.URL.Path, pattern)) 71 | } 72 | 73 | // FasthttpBind the fasthttp request params to the structure and validate. 74 | // note: structPointer must be structure pointer. 75 | func (a *Apiware) FasthttpBind(structPointer interface{}, reqCtx *fasthttp.RequestCtx, pattern string) (err error) { 76 | return FasthttpBind(structPointer, reqCtx, a.PathDecodeFunc(string(reqCtx.Path()), pattern)) 77 | } 78 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package apiware provides a tools which can bind the http/fasthttp request params to the structure and validate. 3 | 4 | Copyright 2016 HenryLee. All Rights Reserved. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | Param tag value description: 19 | tag | key | required | value | desc 20 | ------|----------|----------|---------------|---------------------------------- 21 | param | in | only one | path | (position of param) if `required` is unsetted, auto set it. e.g. url: "http://www.abc.com/a/{path}" 22 | param | in | only one | query | (position of param) e.g. url: "http://www.abc.com/a?b={query}" 23 | param | in | only one | formData | (position of param) e.g. "request body: a=123&b={formData}" 24 | param | in | only one | body | (position of param) request body can be any content 25 | param | in | only one | header | (position of param) request header info 26 | param | in | only one | cookie | (position of param) request cookie info, support: `http.Cookie`,`fasthttp.Cookie`,`string`,`[]byte` 27 | param | name | no | (e.g. "id") | specify request param`s name 28 | param | required | no | required | request param is required 29 | param | desc | no | (e.g. "id") | request param description 30 | param | len | no | (e.g. 3:6, 3) | length range of param's value 31 | param | range | no | (e.g. 0:10) | numerical range of param's value 32 | param | nonzero | no | nonzero | param`s value can not be zero 33 | param | maxmb | no | (e.g. 32) | when request Content-Type is multipart/form-data, the max memory for body.(multi-param, whichever is greater) 34 | regexp| | no |(e.g. "^\\w+$")| param value can not be null 35 | err | | no |(e.g. "incorrect password format")| the custom error for binding or validating 36 | 37 | NOTES: 38 | 1. the binding object must be a struct pointer 39 | 2. the binding struct's field can not be a pointer 40 | 3. `regexp` or `param` tag is only usable when `param:"type(xxx)"` is exist 41 | 4. if the `param` tag is not exist, anonymous field will be parsed 42 | 5. when the param's position(`in`) is `formData` and the field's type is `multipart.FileHeader`, the param receives file uploaded 43 | 6. if param's position(`in`) is `cookie`, field's type must be `http.Cookie` 44 | 7. param tags `in(formData)` and `in(body)` can not exist at the same time 45 | 8. there should not be more than one `in(body)` param tag 46 | 47 | List of supported param value types: 48 | base | slice | special 49 | --------|------------|------------------------------------------------------- 50 | string | []string | [][]byte 51 | byte | []byte | [][]uint8 52 | uint8 | []uint8 | multipart.FileHeader (only for `formData` param) 53 | bool | []bool | http.Cookie (only for `net/http`'s `cookie` param) 54 | int | []int | fasthttp.Cookie (only for `fasthttp`'s `cookie` param) 55 | int8 | []int8 | struct (struct type only for `body` param or as an anonymous field to extend params) 56 | int16 | []int16 | 57 | int32 | []int32 | 58 | int64 | []int64 | 59 | uint8 | []uint8 | 60 | uint16 | []uint16 | 61 | uint32 | []uint32 | 62 | uint64 | []uint64 | 63 | float32 | []float32 | 64 | float64 | []float64 | 65 | */ 66 | package apiware 67 | -------------------------------------------------------------------------------- /convert.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 HenryLee. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apiware 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | "strconv" 21 | "strings" 22 | ) 23 | 24 | // Type conversions for request params. 25 | // 26 | // ConvertAssign copies to dest the value in src, converting it if possible. 27 | // An error is returned if the copy would result in loss of information. 28 | // dest should be a pointer type. 29 | func ConvertAssign(dest reflect.Value, src ...string) (err error) { 30 | return convertAssign(dest, src) 31 | } 32 | 33 | func convertAssign(dest reflect.Value, src []string) (err error) { 34 | if len(src) == 0 { 35 | return nil 36 | } 37 | 38 | dest = reflect.Indirect(dest) 39 | if !dest.CanSet() { 40 | return fmt.Errorf("%s can not be setted", dest.Type().Name()) 41 | } 42 | 43 | defer func() { 44 | if p := recover(); p != nil { 45 | err = fmt.Errorf("%v", p) 46 | } 47 | }() 48 | 49 | switch dest.Interface().(type) { 50 | case string: 51 | dest.Set(reflect.ValueOf(src[0])) 52 | return nil 53 | 54 | case []string: 55 | dest.Set(reflect.ValueOf(src)) 56 | return nil 57 | 58 | case []byte: 59 | dest.Set(reflect.ValueOf([]byte(src[0]))) 60 | return nil 61 | 62 | case [][]byte: 63 | b := make([][]byte, 0, len(src)) 64 | for _, s := range src { 65 | b = append(b, []byte(s)) 66 | } 67 | dest.Set(reflect.ValueOf(b)) 68 | return nil 69 | 70 | case bool: 71 | dest.Set(reflect.ValueOf(parseBool(src[0]))) 72 | return nil 73 | 74 | case []bool: 75 | b := make([]bool, 0, len(src)) 76 | for _, s := range src { 77 | b = append(b, parseBool(s)) 78 | } 79 | dest.Set(reflect.ValueOf(b)) 80 | return nil 81 | } 82 | 83 | switch dest.Kind() { 84 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 85 | i64, err := strconv.ParseInt(src[0], 10, dest.Type().Bits()) 86 | if err != nil { 87 | err = strconvErr(err) 88 | return fmt.Errorf("converting type %T (%q) to a %s: %v", src, src[0], dest.Kind(), err) 89 | } 90 | dest.SetInt(i64) 91 | return nil 92 | 93 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 94 | u64, err := strconv.ParseUint(src[0], 10, dest.Type().Bits()) 95 | if err != nil { 96 | err = strconvErr(err) 97 | return fmt.Errorf("converting type %T (%q) to a %s: %v", src, src[0], dest.Kind(), err) 98 | } 99 | dest.SetUint(u64) 100 | return nil 101 | 102 | case reflect.Float32, reflect.Float64: 103 | f64, err := strconv.ParseFloat(src[0], dest.Type().Bits()) 104 | if err != nil { 105 | err = strconvErr(err) 106 | return fmt.Errorf("converting type %T (%q) to a %s: %v", src, src[0], dest.Kind(), err) 107 | } 108 | dest.SetFloat(f64) 109 | return nil 110 | 111 | case reflect.Slice: 112 | member := dest.Type().Elem() 113 | switch member.Kind() { 114 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 115 | for _, s := range src { 116 | i64, err := strconv.ParseInt(s, 10, member.Bits()) 117 | if err != nil { 118 | err = strconvErr(err) 119 | return fmt.Errorf("converting type %T (%q) to a %s: %v", src, s, dest.Kind(), err) 120 | } 121 | dest.Set(reflect.Append(dest, reflect.ValueOf(i64).Convert(member))) 122 | } 123 | return nil 124 | 125 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 126 | for _, s := range src { 127 | u64, err := strconv.ParseUint(s, 10, member.Bits()) 128 | if err != nil { 129 | err = strconvErr(err) 130 | return fmt.Errorf("converting type %T (%q) to a %s: %v", src, s, dest.Kind(), err) 131 | } 132 | dest.Set(reflect.Append(dest, reflect.ValueOf(u64).Convert(member))) 133 | } 134 | return nil 135 | 136 | case reflect.Float32, reflect.Float64: 137 | for _, s := range src { 138 | f64, err := strconv.ParseFloat(s, member.Bits()) 139 | if err != nil { 140 | err = strconvErr(err) 141 | return fmt.Errorf("converting type %T (%q) to a %s: %v", src, s, dest.Kind(), err) 142 | } 143 | dest.Set(reflect.Append(dest, reflect.ValueOf(f64).Convert(member))) 144 | } 145 | return nil 146 | } 147 | } 148 | 149 | return fmt.Errorf("unsupported storing type %T into type %s", src, dest.Kind()) 150 | } 151 | 152 | func parseBool(val string) bool { 153 | switch strings.TrimSpace(strings.ToLower(val)) { 154 | case "true", "on", "1": 155 | return true 156 | } 157 | return false 158 | } 159 | 160 | func strconvErr(err error) error { 161 | if ne, ok := err.(*strconv.NumError); ok { 162 | return ne.Err 163 | } 164 | return err 165 | } 166 | -------------------------------------------------------------------------------- /paramapi_test.go: -------------------------------------------------------------------------------- 1 | package apiware 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestParsetags(t *testing.T) { 11 | m := parseTags(`in(path),required,desc(banana)`) 12 | if x, ok := m["required"]; !ok { 13 | t.Fatal("wrong value", ok, x) 14 | } 15 | if x, ok := m["desc"]; !ok || x != "banana" { 16 | t.Fatal("wrong value", x) 17 | } 18 | } 19 | 20 | func TestFieldIsZero(t *testing.T) { 21 | if !isZero(reflect.ValueOf(0)) { 22 | t.Fatal("should be zero") 23 | } 24 | if !isZero(reflect.ValueOf("")) { 25 | t.Fatal("should be zero") 26 | } 27 | if !isZero(reflect.ValueOf(false)) { 28 | t.Fatal("should be zero") 29 | } 30 | if isZero(reflect.ValueOf(true)) { 31 | t.Fatal("should not be zero") 32 | } 33 | if isZero(reflect.ValueOf(-1)) { 34 | t.Fatal("should not be zero") 35 | } 36 | if isZero(reflect.ValueOf(1)) { 37 | t.Fatal("should not be zero") 38 | } 39 | if isZero(reflect.ValueOf("asdf")) { 40 | t.Fatal("should not be zero") 41 | } 42 | } 43 | 44 | func TestFieldvalidate(t *testing.T) { 45 | type Schema struct { 46 | A string `param:"in(path),len(3:6),name(p)" err:"This is a custom error!"` 47 | B float32 `param:"in(query),range(10:20)"` 48 | C string `param:"in(query),len(:4),nonzero"` 49 | D string `param:"in(query)" regexp:"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$"` 50 | } 51 | m, _ := NewParamsAPI(&Schema{B: 9.999999}, nil, nil) 52 | a := m.params[0] 53 | if x := len(a.tags); x != 5 { 54 | t.Fatal("wrong len", x, a.tags) 55 | } 56 | if x, ok := a.tags["len"]; !ok || x != "3:6" { 57 | t.Fatal("wrong value", x, ok) 58 | } 59 | if err := a.validate(a.rawValue); err == nil || err.Error() != "This is a custom error!" { 60 | t.Fatal("should not validate") 61 | } 62 | if err := a.validate(reflect.ValueOf("abc")); err != nil { 63 | t.Fatal("should validate", err) 64 | } 65 | if err := a.validate(reflect.ValueOf("abcdefg")); err == nil || err.Error() != "This is a custom error!" { 66 | t.Fatal("should not validate") 67 | } 68 | 69 | b := m.params[1] 70 | if x := len(b.tags); x != 2 { 71 | t.Fatal("wrong len", x) 72 | } 73 | if err := b.validate(b.rawValue); err == nil || err.Error() != "b too small" { 74 | t.Fatal("should not validate") 75 | } 76 | if err := b.validate(reflect.ValueOf(10)); err != nil { 77 | t.Fatal("should validate", err) 78 | } 79 | if err := b.validate(reflect.ValueOf(21)); err == nil || err.Error() != "b too big" { 80 | t.Fatal("should not validate") 81 | } 82 | 83 | c := m.params[2] 84 | if x := len(c.tags); x != 3 { 85 | t.Fatal("wrong len", x) 86 | } 87 | if err := c.validate(c.rawValue); err == nil || err.Error() != "c not set" { 88 | t.Fatal("should not validate") 89 | } 90 | if err := c.validate(reflect.ValueOf("a")); err != nil { 91 | t.Fatal("should validate", err) 92 | } 93 | if err := c.validate(reflect.ValueOf("abcde")); err == nil || err.Error() != "c too long" { 94 | t.Fatal("should not validate") 95 | } 96 | 97 | d := m.params[3] 98 | if x := len(d.tags); x != 2 { 99 | t.Fatal("wrong len", x) 100 | } 101 | if err := d.validate(reflect.ValueOf("gggg@gmail.com")); err != nil { 102 | t.Fatal("should validate", err) 103 | } 104 | if err := d.validate(reflect.ValueOf("www.google.com")); err == nil || err.Error() != "d not match" { 105 | t.Fatal("should not validate", err) 106 | } 107 | } 108 | 109 | func TestFieldOmit(t *testing.T) { 110 | type schema struct { 111 | A string `param:"-"` 112 | B string 113 | } 114 | m, _ := NewParamsAPI(&schema{}, nil, nil) 115 | if x := len(m.params); x != 0 { 116 | t.Fatal("wrong len", x) 117 | } 118 | } 119 | 120 | func TestInterfaceNewParamsAPIWithEmbedded(t *testing.T) { 121 | type third struct { 122 | Num int64 `param:"in(query)"` 123 | } 124 | type embed struct { 125 | Name string `param:"in(query)"` 126 | Value string `param:"in(query)"` 127 | third 128 | } 129 | type table struct { 130 | ColPrimary int64 `param:"in(query)"` 131 | embed 132 | } 133 | table1 := &table{ 134 | 6, embed{"Mrs. A", "infinite", third{Num: 12345}}, 135 | } 136 | m, err := NewParamsAPI(table1, nil, nil) 137 | if err != nil { 138 | t.Fatal("error not nil", err) 139 | } 140 | f := m.params[1] 141 | if x, ok := toString(f.rawValue); !ok || x != "Mrs. A" { 142 | t.Fatal("wrong value from embedded struct") 143 | } 144 | f = m.params[3] 145 | if x, _ := f.Raw().(int64); x != 12345 { 146 | t.Fatal("wrong value from third struct") 147 | } 148 | } 149 | 150 | type indexedTable struct { 151 | ColIsRequired string `param:"in(query),required"` 152 | ColVarChar string `param:"in(query),desc(banana)"` 153 | ColTime time.Time 154 | } 155 | 156 | func TestInterfaceNewParamsAPI(t *testing.T) { 157 | now := time.Now() 158 | table1 := &indexedTable{ 159 | ColVarChar: "orange", 160 | ColTime: now, 161 | } 162 | m, err := NewParamsAPI(table1, nil, nil) 163 | if err != nil { 164 | t.Fatal("error not nil", err) 165 | } 166 | if x := len(m.params); x != 2 { 167 | t.Fatal("wrong value", x) 168 | } 169 | f := m.params[0] 170 | if !f.IsRequired() { 171 | t.Fatal("wrong value") 172 | } 173 | f = m.params[1] 174 | if x, ok := toString(f.rawValue); !ok || x != "orange" { 175 | t.Fatal("wrong value", x) 176 | } 177 | if isZero(f.rawValue) { 178 | t.Fatal("wrong value") 179 | } 180 | if f.Description() != "banana" { 181 | t.Fatal("should value", f.Description()) 182 | } 183 | if f.IsRequired() { 184 | t.Fatal("wrong value") 185 | } 186 | } 187 | 188 | func makeWhitespaceVisible(s string) string { 189 | s = strings.Replace(s, "\t", "\\t", -1) 190 | s = strings.Replace(s, "\r\n", "\\r\\n", -1) 191 | s = strings.Replace(s, "\r", "\\r", -1) 192 | s = strings.Replace(s, "\n", "\\n", -1) 193 | return s 194 | } 195 | func isZero(v reflect.Value) bool { 196 | return v.Interface() == reflect.Zero(v.Type()).Interface() 197 | } 198 | 199 | func toString(v reflect.Value) (string, bool) { 200 | s, ok := v.Interface().(string) 201 | return s, ok 202 | } 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apiware [![GoDoc](https://godoc.org/github.com/tsuna/gohbase?status.png)](https://godoc.org/github.com/henrylee2cn/apiware) 2 | 3 | Apiware binds the specified parameters of the Golang `net/http` and `fasthttp` requests to the structure and verifies the validity of the parameter values. 4 | 5 | It is suggested that you can use the struct as the Handler of the web framework, and use the middleware to quickly bind the request parameters, saving a lot of parameter type conversion and validity verification. At the same time through the struct tag, create swagger json configuration file, easy to create api document services. 6 | 7 | Apiware将Go语言`net/http`及`fasthttp`请求的指定参数绑定到结构体,并验证参数值的合法性。 8 | 建议您可以使用结构体作为web框架的Handler,并用该中间件快速绑定请求参数,节省了大量参数类型转换与有效性验证的工作。同时还可以通过该结构体标签,创建swagger的json配置文件,轻松创建api文档服务。 9 | 10 | # Demo 示例 11 | 12 | ``` 13 | package main 14 | 15 | import ( 16 | "encoding/json" 17 | "github.com/henrylee2cn/apiware" 18 | // "mime/multipart" 19 | "net/http" 20 | "strings" 21 | ) 22 | 23 | type TestApiware struct { 24 | Id int `param:"in(path),required,desc(ID),range(1:2)"` 25 | Num float32 `param:"in(query),name(n),range(0.1:10.19)"` 26 | Title string `param:"in(query),nonzero"` 27 | Paragraph []string `param:"in(query),name(p),len(1:10)" regexp:"(^[\\w]*$)"` 28 | Cookie http.Cookie `param:"in(cookie),name(apiwareid)"` 29 | CookieString string `param:"in(cookie),name(apiwareid)"` 30 | // Picture multipart.FileHeader `param:"in(formData),name(pic),maxmb(30)"` 31 | } 32 | 33 | var myApiware = apiware.New(pathDecodeFunc, nil, nil) 34 | 35 | var pattern = "/test/:id" 36 | 37 | func pathDecodeFunc(urlPath, pattern string) apiware.KV { 38 | idx := map[int]string{} 39 | for k, v := range strings.Split(pattern, "/") { 40 | if !strings.HasPrefix(v, ":") { 41 | continue 42 | } 43 | idx[k] = v[1:] 44 | } 45 | pathParams := make(map[string]string, len(idx)) 46 | for k, v := range strings.Split(urlPath, "/") { 47 | name, ok := idx[k] 48 | if !ok { 49 | continue 50 | } 51 | pathParams[name] = v 52 | } 53 | return apiware.Map(pathParams) 54 | } 55 | 56 | func testHandler(resp http.ResponseWriter, req *http.Request) { 57 | // set cookies 58 | http.SetCookie(resp, &http.Cookie{ 59 | Name: "apiwareid", 60 | Value: "http_henrylee2cn", 61 | }) 62 | 63 | // bind params 64 | params := new(TestApiware) 65 | err := myApiware.Bind(params, req, pattern) 66 | b, _ := json.MarshalIndent(params, "", " ") 67 | if err != nil { 68 | resp.WriteHeader(http.StatusBadRequest) 69 | resp.Write(append([]byte(err.Error()+"\n"), b...)) 70 | } else { 71 | resp.WriteHeader(http.StatusOK) 72 | resp.Write(b) 73 | } 74 | } 75 | 76 | func main() { 77 | // Check whether `testHandler` meet the requirements of apiware, and register it 78 | err := myApiware.Register(new(TestApiware)) 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | // server 84 | http.HandleFunc("/test/0", testHandler) 85 | http.HandleFunc("/test/1", testHandler) 86 | http.HandleFunc("/test/1.1", testHandler) 87 | http.HandleFunc("/test/2", testHandler) 88 | http.HandleFunc("/test/3", testHandler) 89 | http.ListenAndServe(":8080", nil) 90 | } 91 | ``` 92 | 93 | # Struct&Tag 结构体及其标签 94 | 95 | tag | key | required | value | desc 96 | ------|----------|----------|---------------|---------------------------------- 97 | param | in | only one | path | (position of param) if `required` is unsetted, auto set it. e.g. url: `http://www.abc.com/a/{path}` 98 | param | in | only one | query | (position of param) e.g. url: `http://www.abc.com/a?b={query}` 99 | param | in | only one | formData | (position of param) e.g. `request body: a=123&b={formData}` 100 | param | in | only one | body | (position of param) request body can be any content 101 | param | in | only one | header | (position of param) request header info 102 | param | in | only one | cookie | (position of param) request cookie info, support: `http.Cookie`, `fasthttp.Cookie`, `string`, `[]byte` and so on 103 | param | name | no | (e.g. `id`) | specify request param`s name 104 | param | required | no | required | request param is required 105 | param | desc | no | (e.g. `id`) | request param description 106 | param | len | no | (e.g. `3:6``3`) | length range of param's value 107 | param | range | no | (e.g. `0:10`) | numerical range of param's value 108 | param | nonzero | no | nonzero | param`s value can not be zero 109 | param | maxmb | no | (e.g. `32`) | when request Content-Type is multipart/form-data, the max memory for body.(multi-param, whichever is greater) 110 | regexp| | no |(e.g. `^\w+$`)| param value can not be null 111 | err | | no |(e.g. `incorrect password format`)| the custom error for binding or validating 112 | **NOTES**: 113 | * the binding object must be a struct pointer 114 | * the binding struct's field can not be a pointer 115 | * `regexp` or `param` tag is only usable when `param:"type(xxx)"` is exist 116 | * if the `param` tag is not exist, anonymous field will be parsed 117 | * when the param's position(`in`) is `formData` and the field's type is `multipart.FileHeader`, the param receives file uploaded 118 | * if param's position(`in`) is `cookie`, field's type must be `http.Cookie` 119 | * param tags `in(formData)` and `in(body)` can not exist at the same time 120 | * there should not be more than one `in(body)` param tag 121 | 122 | # Field Types 结构体字段类型 123 | 124 | base | slice | special 125 | --------|------------|------------------------------------------------------- 126 | string | []string | [][]byte 127 | byte | []byte | [][]uint8 128 | uint8 | []uint8 | multipart.FileHeader (only for `formData` param) 129 | bool | []bool | http.Cookie (only for `net/http`'s `cookie` param) 130 | int | []int | fasthttp.Cookie (only for `fasthttp`'s `cookie` param) 131 | int8 | []int8 | struct (struct type only for `body` param or as an anonymous field to extend params) 132 | int16 | []int16 | 133 | int32 | []int32 | 134 | int64 | []int64 | 135 | uint8 | []uint8 | 136 | uint16 | []uint16 | 137 | uint32 | []uint32 | 138 | uint64 | []uint64 | 139 | float32 | []float32 | 140 | float64 | []float64 | 141 | -------------------------------------------------------------------------------- /param.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 HenryLee. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apiware 16 | 17 | import ( 18 | "fmt" 19 | "math" 20 | "reflect" 21 | "regexp" 22 | "strconv" 23 | "strings" 24 | ) 25 | 26 | const ( 27 | TAG_PARAM = "param" //request param tag name 28 | TAG_REGEXP = "regexp" //regexp validate tag name(optio) 29 | TAG_ERR = "err" //the custom error for binding or validating 30 | TAG_IGNORE_PARAM = "-" //ignore request param tag value 31 | 32 | MB = 1 << 20 // 1MB 33 | defaultMaxMemory = 32 * MB // 32 MB 34 | defaultMaxMemoryMB = 32 35 | ) 36 | 37 | // func ParseTags(s string) map[string]string { 38 | // c := strings.Split(s, ",") 39 | // m := make(map[string]string) 40 | // for _, v := range c { 41 | // c2 := strings.Split(v, "(") 42 | // if len(c2) == 2 && len(c2[1]) > 1 { 43 | // m[c2[0]] = c2[1][:len(c2[1])-1] 44 | // } else { 45 | // m[v] = "" 46 | // } 47 | // } 48 | // return m 49 | // } 50 | 51 | func ParseTags(s string) map[string]string { 52 | c := strings.Split(s, ",") 53 | m := make(map[string]string) 54 | for _, v := range c { 55 | a := strings.IndexByte(v, '(') 56 | b := strings.LastIndexByte(v, ')') 57 | if a != -1 && b != -1 { 58 | m[v[:a]] = v[a+1 : b] 59 | continue 60 | } 61 | m[v] = "" 62 | } 63 | return m 64 | } 65 | 66 | // use the struct field to define a request parameter model 67 | type Param struct { 68 | apiName string // ParamsAPI name 69 | name string // param name 70 | indexPath []int 71 | isRequired bool // file is required or not 72 | isFile bool // is file param or not 73 | tags map[string]string // struct tags for this param 74 | rawTag reflect.StructTag // the raw tag 75 | rawValue reflect.Value // the raw tag value 76 | err error // the custom error for binding or validating 77 | } 78 | 79 | const ( 80 | fileTypeString = "multipart.FileHeader" 81 | cookieTypeString = "http.Cookie" 82 | fasthttpCookieTypeString = "fasthttp.Cookie" 83 | stringTypeString = "string" 84 | bytesTypeString = "[]byte" 85 | bytes2TypeString = "[]uint8" 86 | ) 87 | 88 | var ( 89 | // values for tag 'in' 90 | TagInValues = map[string]bool{ 91 | "path": true, 92 | "query": true, 93 | "formData": true, 94 | "body": true, 95 | "header": true, 96 | "cookie": true, 97 | } 98 | ) 99 | 100 | // Raw gets the param's original value 101 | func (param *Param) Raw() interface{} { 102 | return param.rawValue.Interface() 103 | } 104 | 105 | // APIName gets ParamsAPI name 106 | func (param *Param) APIName() string { 107 | return param.apiName 108 | } 109 | 110 | // Name gets parameter field name 111 | func (param *Param) Name() string { 112 | return param.name 113 | } 114 | 115 | // In get the type value for the param 116 | func (param *Param) In() string { 117 | return param.tags["in"] 118 | } 119 | 120 | // IsRequired tests if the param is declared 121 | func (param *Param) IsRequired() bool { 122 | return param.isRequired 123 | } 124 | 125 | // Description gets the description value for the param 126 | func (param *Param) Description() string { 127 | return param.tags["desc"] 128 | } 129 | 130 | // IsFile tests if the param is type *multipart.FileHeader 131 | func (param *Param) IsFile() bool { 132 | return param.isFile 133 | } 134 | 135 | func (param *Param) validate(value reflect.Value) error { 136 | if value.Kind() != reflect.Slice { 137 | return param.validateElem(value) 138 | } 139 | var err error 140 | for i, count := 0, value.Len(); i < count; i++ { 141 | if err = param.validateElem(value.Index(i)); err != nil { 142 | return err 143 | } 144 | } 145 | return nil 146 | } 147 | 148 | // Validate tests if the param conforms to it's validation constraints specified 149 | // int the TAG_REGEXP struct tag 150 | func (param *Param) validateElem(value reflect.Value) (err error) { 151 | defer func() { 152 | p := recover() 153 | if param.err != nil { 154 | if err != nil { 155 | err = param.err 156 | } 157 | } else if p != nil { 158 | err = fmt.Errorf("%v", p) 159 | } 160 | }() 161 | // range 162 | if tuple, ok := param.tags["range"]; ok { 163 | var f64 float64 164 | switch value.Kind() { 165 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 166 | f64 = float64(value.Int()) 167 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 168 | f64 = float64(value.Uint()) 169 | case reflect.Float32, reflect.Float64: 170 | f64 = value.Float() 171 | } 172 | if err = validateRange(f64, tuple, param.name); err != nil { 173 | return err 174 | } 175 | } 176 | obj := value.Interface() 177 | // nonzero 178 | if _, ok := param.tags["nonzero"]; ok { 179 | if value.Kind() != reflect.Struct && obj == reflect.Zero(value.Type()).Interface() { 180 | return NewValidationError(ValidationErrorValueNotSet, param.name) 181 | } 182 | } 183 | s, isString := obj.(string) 184 | // length 185 | if tuple, ok := param.tags["len"]; ok && isString { 186 | if err = validateLen(s, tuple, param.name); err != nil { 187 | return err 188 | } 189 | } 190 | // regexp 191 | if reg, ok := param.tags[TAG_REGEXP]; ok && isString { 192 | if err = validateRegexp(s, reg, param.name); err != nil { 193 | return err 194 | } 195 | } 196 | return 197 | } 198 | 199 | func (param *Param) myError(reason string) error { 200 | if param.err != nil { 201 | return param.err 202 | } 203 | return NewError(param.apiName, param.name, reason) 204 | } 205 | 206 | func parseTuple(tuple string) (string, string) { 207 | c := strings.Split(tuple, ":") 208 | var a, b string 209 | switch len(c) { 210 | case 1: 211 | a = c[0] 212 | if len(a) > 0 { 213 | return a, a 214 | } 215 | case 2: 216 | a = c[0] 217 | b = c[1] 218 | if len(a) > 0 || len(b) > 0 { 219 | return a, b 220 | } 221 | } 222 | panic("invalid validation tuple") 223 | } 224 | 225 | func validateLen(s, tuple, paramName string) error { 226 | a, b := parseTuple(tuple) 227 | if len(a) > 0 { 228 | min, err := strconv.Atoi(a) 229 | if err != nil { 230 | panic(err) 231 | } 232 | if len(s) < min { 233 | return NewValidationError(ValidationErrorValueTooShort, paramName) 234 | } 235 | } 236 | if len(b) > 0 { 237 | max, err := strconv.Atoi(b) 238 | if err != nil { 239 | panic(err) 240 | } 241 | if len(s) > max { 242 | return NewValidationError(ValidationErrorValueTooLong, paramName) 243 | } 244 | } 245 | return nil 246 | } 247 | 248 | const accuracy = 0.0000001 249 | 250 | func validateRange(f64 float64, tuple, paramName string) error { 251 | a, b := parseTuple(tuple) 252 | if len(a) > 0 { 253 | min, err := strconv.ParseFloat(a, 64) 254 | if err != nil { 255 | return err 256 | } 257 | if math.Min(f64, min) == f64 && math.Abs(f64-min) > accuracy { 258 | return NewValidationError(ValidationErrorValueTooSmall, paramName) 259 | } 260 | } 261 | if len(b) > 0 { 262 | max, err := strconv.ParseFloat(b, 64) 263 | if err != nil { 264 | return err 265 | } 266 | if math.Max(f64, max) == f64 && math.Abs(f64-max) > accuracy { 267 | return NewValidationError(ValidationErrorValueTooBig, paramName) 268 | } 269 | } 270 | return nil 271 | } 272 | 273 | func validateRegexp(s, reg, paramName string) error { 274 | matched, err := regexp.MatchString(reg, s) 275 | if err != nil { 276 | return err 277 | } 278 | if !matched { 279 | return NewValidationError(ValidationErrorValueNotMatch, paramName) 280 | } 281 | return nil 282 | } 283 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 HenryLee 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /paramapi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 HenryLee. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apiware 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "io/ioutil" 21 | // "mime/multipart" 22 | "net/http" 23 | "net/url" 24 | "reflect" 25 | "strconv" 26 | "sync" 27 | 28 | "github.com/valyala/fasthttp" 29 | ) 30 | 31 | type ( 32 | // ParamsAPI defines a parameter model for an web api. 33 | ParamsAPI struct { 34 | name string 35 | params []*Param 36 | //used to create a new struct (non-pointer) 37 | structType reflect.Type 38 | //the raw struct pointer 39 | rawStructPointer interface{} 40 | // create param name from struct field name 41 | paramNameFunc ParamNameFunc 42 | // decode params from request body 43 | bodyDecodeFunc BodyDecodeFunc 44 | //when request Content-Type is multipart/form-data, the max memory for body. 45 | maxMemory int64 46 | } 47 | 48 | // Schema is a collection of ParamsAPI 49 | Schema struct { 50 | lib map[string]*ParamsAPI 51 | sync.RWMutex 52 | } 53 | 54 | // Create param name from struct param name 55 | ParamNameFunc func(fieldName string) (paramName string) 56 | 57 | // Decode params from request body 58 | BodyDecodeFunc func(dest reflect.Value, body []byte) error 59 | ) 60 | 61 | var ( 62 | defaultSchema = &Schema{ 63 | lib: map[string]*ParamsAPI{}, 64 | } 65 | ) 66 | 67 | // NewParamsAPI parses and store the struct object, requires a struct pointer, 68 | // if `paramNameFunc` is nil, `paramNameFunc=toSnake`, 69 | // if `bodyDecodeFunc` is nil, `bodyDecodeFunc=bodyJONS`, 70 | func NewParamsAPI( 71 | structPointer interface{}, 72 | paramNameFunc ParamNameFunc, 73 | bodyDecodeFunc BodyDecodeFunc, 74 | ) ( 75 | *ParamsAPI, 76 | error, 77 | ) { 78 | name := reflect.TypeOf(structPointer).String() 79 | v := reflect.ValueOf(structPointer) 80 | if v.Kind() != reflect.Ptr { 81 | return nil, NewError(name, "*", "the binding object must be a struct pointer") 82 | } 83 | v = reflect.Indirect(v) 84 | if v.Kind() != reflect.Struct { 85 | return nil, NewError(name, "*", "the binding object must be a struct pointer") 86 | } 87 | var m = &ParamsAPI{ 88 | name: name, 89 | params: []*Param{}, 90 | structType: v.Type(), 91 | rawStructPointer: structPointer, 92 | } 93 | if paramNameFunc != nil { 94 | m.paramNameFunc = paramNameFunc 95 | } else { 96 | m.paramNameFunc = toSnake 97 | } 98 | if bodyDecodeFunc != nil { 99 | m.bodyDecodeFunc = bodyDecodeFunc 100 | } else { 101 | m.bodyDecodeFunc = bodyJONS 102 | } 103 | err := m.addFields([]int{}, m.structType, v) 104 | if err != nil { 105 | return nil, err 106 | } 107 | defaultSchema.set(m) 108 | return m, nil 109 | } 110 | 111 | // Register is similar to a `NewParamsAPI`, but only return error. 112 | // Parse and store the struct object, requires a struct pointer, 113 | // if `paramNameFunc` is nil, `paramNameFunc=toSnake`, 114 | // if `bodyDecodeFunc` is nil, `bodyDecodeFunc=bodyJONS`, 115 | func Register( 116 | structPointer interface{}, 117 | paramNameFunc ParamNameFunc, 118 | bodyDecodeFunc BodyDecodeFunc, 119 | ) error { 120 | _, err := NewParamsAPI(structPointer, paramNameFunc, bodyDecodeFunc) 121 | return err 122 | } 123 | 124 | func (m *ParamsAPI) addFields(parentIndexPath []int, t reflect.Type, v reflect.Value) error { 125 | var err error 126 | var maxMemoryMB int64 127 | var hasFormData, hasBody bool 128 | var deep = len(parentIndexPath) + 1 129 | for i := 0; i < t.NumField(); i++ { 130 | indexPath := make([]int, deep) 131 | copy(indexPath, parentIndexPath) 132 | indexPath[deep-1] = i 133 | 134 | var field = t.Field(i) 135 | tag, ok := field.Tag.Lookup(TAG_PARAM) 136 | if !ok { 137 | if field.Anonymous && field.Type.Kind() == reflect.Struct { 138 | if err = m.addFields(indexPath, field.Type, v.Field(i)); err != nil { 139 | return err 140 | } 141 | } 142 | continue 143 | } 144 | 145 | if tag == TAG_IGNORE_PARAM { 146 | continue 147 | } 148 | 149 | if field.Type.Kind() == reflect.Ptr { 150 | return NewError(t.String(), field.Name, "field can not be a pointer") 151 | } 152 | 153 | var parsedTags = ParseTags(tag) 154 | var paramPosition = parsedTags["in"] 155 | var paramTypeString = field.Type.String() 156 | 157 | switch paramTypeString { 158 | case fileTypeString: 159 | if paramPosition != "formData" { 160 | return NewError(t.String(), field.Name, "when field type is `"+paramTypeString+"`, tag `in` value must be `formData`") 161 | } 162 | case cookieTypeString, fasthttpCookieTypeString: 163 | if paramPosition != "cookie" { 164 | return NewError(t.String(), field.Name, "when field type is `"+paramTypeString+"`, tag `in` value must be `cookie`") 165 | } 166 | } 167 | 168 | switch paramPosition { 169 | case "formData": 170 | if hasBody { 171 | return NewError(t.String(), field.Name, "tags of `in(formData)` and `in(body)` can not exist at the same time") 172 | } 173 | hasFormData = true 174 | case "body": 175 | if hasFormData { 176 | return NewError(t.String(), field.Name, "tags of `in(formData)` and `in(body)` can not exist at the same time") 177 | } 178 | if hasBody { 179 | return NewError(t.String(), field.Name, "there should not be more than one tag `in(body)`") 180 | } 181 | hasBody = true 182 | case "path": 183 | parsedTags["required"] = "required" 184 | // case "cookie": 185 | // switch paramTypeString { 186 | // case cookieTypeString, fasthttpCookieTypeString, stringTypeString, bytesTypeString, bytes2TypeString: 187 | // default: 188 | // return NewError(t.String(), field.Name, "invalid field type for `in(cookie)`, refer to the following: `http.Cookie`, `fasthttp.Cookie`, `string`, `[]byte` or `[]uint8`") 189 | // } 190 | default: 191 | if !TagInValues[paramPosition] { 192 | return NewError(t.String(), field.Name, "invalid tag `in` value, refer to the following: `path`, `query`, `formData`, `body`, `header` or `cookie`") 193 | } 194 | } 195 | if _, ok := parsedTags["len"]; ok && paramTypeString != "string" && paramTypeString != "[]string" { 196 | return NewError(t.String(), field.Name, "invalid `len` tag for non-string field") 197 | } 198 | if _, ok := parsedTags["range"]; ok { 199 | switch paramTypeString { 200 | case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "float32", "float64": 201 | case "[]int", "[]int8", "[]int16", "[]int32", "[]int64", "[]uint", "[]uint8", "[]uint16", "[]uint32", "[]uint64", "[]float32", "[]float64": 202 | default: 203 | return NewError(t.String(), field.Name, "invalid `range` tag for non-number field") 204 | } 205 | } 206 | if a, ok := field.Tag.Lookup(TAG_REGEXP); ok { 207 | if paramTypeString != "string" && paramTypeString != "[]string" { 208 | return NewError(t.String(), field.Name, "invalid `"+TAG_REGEXP+"` tag for non-string field") 209 | } 210 | parsedTags[TAG_REGEXP] = a 211 | } 212 | if a, ok := parsedTags["maxmb"]; ok { 213 | i, err := strconv.ParseInt(a, 10, 64) 214 | if err != nil { 215 | return NewError(t.String(), field.Name, "invalid `maxmb` tag, it must be positive integer") 216 | } 217 | if i > maxMemoryMB { 218 | maxMemoryMB = i 219 | } 220 | } 221 | 222 | fd := &Param{ 223 | apiName: m.name, 224 | indexPath: indexPath, 225 | tags: parsedTags, 226 | rawTag: field.Tag, 227 | rawValue: v.Field(i), 228 | } 229 | 230 | if errStr, ok := field.Tag.Lookup(TAG_ERR); ok { 231 | fd.tags[TAG_ERR] = errStr 232 | fd.err = errors.New(errStr) 233 | } 234 | 235 | // fmt.Printf("%#v\n", fd.tags) 236 | 237 | if fd.name, ok = parsedTags["name"]; !ok { 238 | fd.name = m.paramNameFunc(field.Name) 239 | } 240 | 241 | fd.isFile = paramTypeString == fileTypeString 242 | _, fd.isRequired = parsedTags["required"] 243 | 244 | // err = fd.validate(v) 245 | // if err != nil { 246 | // return NewError(t.String(), field.Name, "the initial value failed validation:"+err.Error()) 247 | // } 248 | 249 | m.params = append(m.params, fd) 250 | } 251 | if maxMemoryMB > 0 { 252 | m.maxMemory = maxMemoryMB * MB 253 | } else { 254 | m.maxMemory = defaultMaxMemory 255 | } 256 | return nil 257 | } 258 | 259 | // GetParamsAPI gets the `*ParamsAPI` object according to the type name 260 | func GetParamsAPI(paramsAPIName string) (*ParamsAPI, error) { 261 | m, ok := defaultSchema.get(paramsAPIName) 262 | if !ok { 263 | return nil, errors.New("struct `" + paramsAPIName + "` is not registered") 264 | } 265 | return m, nil 266 | } 267 | 268 | // SetParamsAPI caches `*ParamsAPI` 269 | func SetParamsAPI(m *ParamsAPI) { 270 | defaultSchema.set(m) 271 | } 272 | 273 | func (schema *Schema) get(paramsAPIName string) (*ParamsAPI, bool) { 274 | schema.RLock() 275 | defer schema.RUnlock() 276 | m, ok := schema.lib[paramsAPIName] 277 | return m, ok 278 | } 279 | 280 | func (schema *Schema) set(m *ParamsAPI) { 281 | schema.Lock() 282 | schema.lib[m.name] = m 283 | defer schema.Unlock() 284 | } 285 | 286 | // Name gets the name 287 | func (paramsAPI *ParamsAPI) Name() string { 288 | return paramsAPI.name 289 | } 290 | 291 | // Params gets the parameter information 292 | func (paramsAPI *ParamsAPI) Params() []*Param { 293 | return paramsAPI.params 294 | } 295 | 296 | // Number returns the number of parameters to be bound 297 | func (paramsAPI *ParamsAPI) Number() int { 298 | return len(paramsAPI.params) 299 | } 300 | 301 | // Raw returns the ParamsAPI's original value 302 | func (paramsAPI *ParamsAPI) Raw() interface{} { 303 | return paramsAPI.rawStructPointer 304 | } 305 | 306 | // MaxMemory gets maxMemory 307 | // when request Content-Type is multipart/form-data, the max memory for body. 308 | func (paramsAPI *ParamsAPI) MaxMemory() int64 { 309 | return paramsAPI.maxMemory 310 | } 311 | 312 | // SetMaxMemory sets maxMemory for the request which Content-Type is multipart/form-data. 313 | func (paramsAPI *ParamsAPI) SetMaxMemory(maxMemory int64) { 314 | paramsAPI.maxMemory = maxMemory 315 | } 316 | 317 | // NewReceiver creates a new struct pointer and the field's values for its receive parameterste it. 318 | func (paramsAPI *ParamsAPI) NewReceiver() (interface{}, []reflect.Value) { 319 | object := reflect.New(paramsAPI.structType) 320 | return object.Interface(), paramsAPI.fieldsForBinding(object.Elem()) 321 | } 322 | 323 | func (paramsAPI *ParamsAPI) fieldsForBinding(structElem reflect.Value) []reflect.Value { 324 | count := len(paramsAPI.params) 325 | fields := make([]reflect.Value, count) 326 | for i := 0; i < count; i++ { 327 | value := structElem 328 | param := paramsAPI.params[i] 329 | for _, index := range param.indexPath { 330 | value = value.Field(index) 331 | } 332 | fields[i] = value 333 | } 334 | return fields 335 | } 336 | 337 | // BindByName binds the net/http request params to a new struct and validate it. 338 | func BindByName( 339 | paramsAPIName string, 340 | req *http.Request, 341 | pathParams KV, 342 | ) ( 343 | interface{}, 344 | error, 345 | ) { 346 | paramsAPI, err := GetParamsAPI(paramsAPIName) 347 | if err != nil { 348 | return nil, err 349 | } 350 | return paramsAPI.BindNew(req, pathParams) 351 | } 352 | 353 | // Bind binds the net/http request params to the `structPointer` param and validate it. 354 | // note: structPointer must be struct pointer. 355 | func Bind( 356 | structPointer interface{}, 357 | req *http.Request, 358 | pathParams KV, 359 | ) error { 360 | paramsAPI, err := GetParamsAPI(reflect.TypeOf(structPointer).String()) 361 | if err != nil { 362 | return err 363 | } 364 | return paramsAPI.BindAt(structPointer, req, pathParams) 365 | } 366 | 367 | // BindAt binds the net/http request params to a struct pointer and validate it. 368 | // note: structPointer must be struct pointer. 369 | func (paramsAPI *ParamsAPI) BindAt( 370 | structPointer interface{}, 371 | req *http.Request, 372 | pathParams KV, 373 | ) error { 374 | name := reflect.TypeOf(structPointer).String() 375 | if name != paramsAPI.name { 376 | return errors.New("the structPointer's type `" + name + "` does not match type `" + paramsAPI.name + "`") 377 | } 378 | return paramsAPI.BindFields( 379 | paramsAPI.fieldsForBinding(reflect.ValueOf(structPointer).Elem()), 380 | req, 381 | pathParams, 382 | ) 383 | } 384 | 385 | // BindNew binds the net/http request params to a struct pointer and validate it. 386 | func (paramsAPI *ParamsAPI) BindNew( 387 | req *http.Request, 388 | pathParams KV, 389 | ) ( 390 | interface{}, 391 | error, 392 | ) { 393 | structPrinter, fields := paramsAPI.NewReceiver() 394 | err := paramsAPI.BindFields(fields, req, pathParams) 395 | return structPrinter, err 396 | } 397 | 398 | // RawBind binds the net/http request params to the original struct pointer and validate it. 399 | func (paramsAPI *ParamsAPI) RawBind( 400 | req *http.Request, 401 | pathParams KV, 402 | ) ( 403 | interface{}, 404 | error, 405 | ) { 406 | var fields []reflect.Value 407 | for _, param := range paramsAPI.params { 408 | fields = append(fields, param.rawValue) 409 | } 410 | err := paramsAPI.BindFields(fields, req, pathParams) 411 | return paramsAPI.rawStructPointer, err 412 | } 413 | 414 | // BindFields binds the net/http request params to a struct and validate it. 415 | // Must ensure that the param `fields` matches `paramsAPI.params`. 416 | func (paramsAPI *ParamsAPI) BindFields( 417 | fields []reflect.Value, 418 | req *http.Request, 419 | pathParams KV, 420 | ) ( 421 | err error, 422 | ) { 423 | if pathParams == nil { 424 | pathParams = Map(map[string]string{}) 425 | } 426 | if req.Form == nil { 427 | req.ParseMultipartForm(paramsAPI.maxMemory) 428 | } 429 | var queryValues url.Values 430 | defer func() { 431 | if p := recover(); p != nil { 432 | err = NewError(paramsAPI.name, "?", fmt.Sprint(p)) 433 | } 434 | }() 435 | 436 | for i, param := range paramsAPI.params { 437 | value := fields[i] 438 | switch param.In() { 439 | case "path": 440 | paramValue, ok := pathParams.Get(param.name) 441 | if !ok { 442 | return param.myError("missing path param") 443 | } 444 | // fmt.Printf("paramName:%s\nvalue:%#v\n\n", param.name, paramValue) 445 | if err = convertAssign(value, []string{paramValue}); err != nil { 446 | return param.myError(err.Error()) 447 | } 448 | 449 | case "query": 450 | if queryValues == nil { 451 | queryValues, err = url.ParseQuery(req.URL.RawQuery) 452 | if err != nil { 453 | queryValues = make(url.Values) 454 | } 455 | } 456 | paramValues, ok := queryValues[param.name] 457 | if ok { 458 | if err = convertAssign(value, paramValues); err != nil { 459 | return param.myError(err.Error()) 460 | } 461 | } else if param.IsRequired() { 462 | return param.myError("missing query param") 463 | } 464 | 465 | case "formData": 466 | // Can not exist with `body` param at the same time 467 | if param.IsFile() { 468 | if req.MultipartForm != nil { 469 | fhs := req.MultipartForm.File[param.name] 470 | if len(fhs) == 0 { 471 | if param.IsRequired() { 472 | return param.myError("missing formData param") 473 | } 474 | continue 475 | } 476 | value.Set(reflect.ValueOf(fhs[0]).Elem()) 477 | } else if param.IsRequired() { 478 | return param.myError("missing formData param") 479 | } 480 | continue 481 | } 482 | 483 | paramValues, ok := req.PostForm[param.name] 484 | if ok { 485 | if err = convertAssign(value, paramValues); err != nil { 486 | return param.myError(err.Error()) 487 | } 488 | } else if param.IsRequired() { 489 | return param.myError("missing formData param") 490 | } 491 | 492 | case "body": 493 | // Theoretically there should be at most one `body` param, and can not exist with `formData` at the same time 494 | var body []byte 495 | body, err = ioutil.ReadAll(req.Body) 496 | req.Body.Close() 497 | if err == nil { 498 | if err = paramsAPI.bodyDecodeFunc(value, body); err != nil { 499 | return param.myError(err.Error()) 500 | } 501 | } else if param.IsRequired() { 502 | return param.myError("missing body param") 503 | } 504 | 505 | case "header": 506 | paramValues, ok := req.Header[param.name] 507 | if ok { 508 | if err = convertAssign(value, paramValues); err != nil { 509 | return param.myError(err.Error()) 510 | } 511 | } else if param.IsRequired() { 512 | return param.myError("missing header param") 513 | } 514 | 515 | case "cookie": 516 | c, _ := req.Cookie(param.name) 517 | if c != nil { 518 | switch value.Type().String() { 519 | case cookieTypeString: 520 | value.Set(reflect.ValueOf(c).Elem()) 521 | default: 522 | if err = convertAssign(value, []string{c.Value}); err != nil { 523 | return param.myError(err.Error()) 524 | } 525 | } 526 | } else if param.IsRequired() { 527 | return param.myError("missing cookie param") 528 | } 529 | } 530 | if err = param.validate(value); err != nil { 531 | return err 532 | } 533 | } 534 | return 535 | } 536 | 537 | // FasthttpBindByName binds the net/http request params to a new struct and validate it. 538 | func FasthttpBindByName( 539 | paramsAPIName string, 540 | req *fasthttp.RequestCtx, 541 | pathParams KV, 542 | ) ( 543 | interface{}, 544 | error, 545 | ) { 546 | paramsAPI, err := GetParamsAPI(paramsAPIName) 547 | if err != nil { 548 | return nil, err 549 | } 550 | return paramsAPI.FasthttpBindNew(req, pathParams) 551 | } 552 | 553 | // FasthttpBind binds the net/http request params to the `structPointer` param and validate it. 554 | // note: structPointer must be struct pointer. 555 | func FasthttpBind( 556 | structPointer interface{}, 557 | req *fasthttp.RequestCtx, 558 | pathParams KV, 559 | ) error { 560 | paramsAPI, err := GetParamsAPI(reflect.TypeOf(structPointer).String()) 561 | if err != nil { 562 | return err 563 | } 564 | return paramsAPI.FasthttpBindAt(structPointer, req, pathParams) 565 | } 566 | 567 | // FasthttpBindAt binds the net/http request params to a struct pointer and validate it. 568 | // note: structPointer must be struct pointer. 569 | func (paramsAPI *ParamsAPI) FasthttpBindAt( 570 | structPointer interface{}, 571 | req *fasthttp.RequestCtx, 572 | pathParams KV, 573 | ) error { 574 | name := reflect.TypeOf(structPointer).String() 575 | if name != paramsAPI.name { 576 | return errors.New("the structPointer's type `" + name + "` does not match type `" + paramsAPI.name + "`") 577 | } 578 | return paramsAPI.FasthttpBindFields( 579 | paramsAPI.fieldsForBinding(reflect.ValueOf(structPointer).Elem()), 580 | req, 581 | pathParams, 582 | ) 583 | } 584 | 585 | // FasthttpBindNew binds the net/http request params to a struct pointer and validate it. 586 | func (paramsAPI *ParamsAPI) FasthttpBindNew( 587 | req *fasthttp.RequestCtx, 588 | pathParams KV, 589 | ) ( 590 | interface{}, 591 | error, 592 | ) { 593 | structPrinter, fields := paramsAPI.NewReceiver() 594 | err := paramsAPI.FasthttpBindFields(fields, req, pathParams) 595 | return structPrinter, err 596 | } 597 | 598 | // RawBind binds the net/http request params to the original struct pointer and validate it. 599 | func (paramsAPI *ParamsAPI) FasthttpRawBind( 600 | req *fasthttp.RequestCtx, 601 | pathParams KV, 602 | ) ( 603 | interface{}, 604 | error, 605 | ) { 606 | var fields []reflect.Value 607 | for _, param := range paramsAPI.params { 608 | fields = append(fields, param.rawValue) 609 | } 610 | err := paramsAPI.FasthttpBindFields(fields, req, pathParams) 611 | return paramsAPI.rawStructPointer, err 612 | } 613 | 614 | // FasthttpBindFields binds the net/http request params to a struct and validate it. 615 | // Must ensure that the param `fields` matches `paramsAPI.params`. 616 | func (paramsAPI *ParamsAPI) FasthttpBindFields( 617 | fields []reflect.Value, 618 | req *fasthttp.RequestCtx, 619 | pathParams KV, 620 | ) ( 621 | err error, 622 | ) { 623 | if pathParams == nil { 624 | pathParams = Map(map[string]string{}) 625 | } 626 | 627 | defer func() { 628 | if p := recover(); p != nil { 629 | err = NewError(paramsAPI.name, "?", fmt.Sprint(p)) 630 | } 631 | }() 632 | 633 | var formValues = fasthttpFormValues(req) 634 | for i, param := range paramsAPI.params { 635 | value := fields[i] 636 | switch param.In() { 637 | case "path": 638 | paramValue, ok := pathParams.Get(param.name) 639 | if !ok { 640 | return param.myError("missing path param") 641 | } 642 | // fmt.Printf("paramName:%s\nvalue:%#v\n\n", param.name, paramValue) 643 | if err = convertAssign(value, []string{paramValue}); err != nil { 644 | return param.myError(err.Error()) 645 | } 646 | 647 | case "query": 648 | paramValuesBytes := req.QueryArgs().PeekMulti(param.name) 649 | if len(paramValuesBytes) > 0 { 650 | var paramValues = make([]string, len(paramValuesBytes)) 651 | for i, b := range paramValuesBytes { 652 | paramValues[i] = string(b) 653 | } 654 | if err = convertAssign(value, paramValues); err != nil { 655 | return param.myError(err.Error()) 656 | } 657 | } else if len(paramValuesBytes) == 0 && param.IsRequired() { 658 | return param.myError("missing query param") 659 | } 660 | 661 | case "formData": 662 | // Can not exist with `body` param at the same time 663 | if param.IsFile() { 664 | if fh, err := req.FormFile(param.name); err == nil { 665 | value.Set(reflect.ValueOf(fh).Elem()) 666 | } else if param.IsRequired() { 667 | return param.myError("missing formData param") 668 | } 669 | continue 670 | } 671 | 672 | paramValues, ok := formValues[param.name] 673 | if ok { 674 | if err = convertAssign(value, paramValues); err != nil { 675 | return param.myError(err.Error()) 676 | } 677 | } else if param.IsRequired() { 678 | return param.myError("missing formData param") 679 | } 680 | 681 | case "body": 682 | // Theoretically there should be at most one `body` param, and can not exist with `formData` at the same time 683 | body := req.PostBody() 684 | if body != nil { 685 | if err = paramsAPI.bodyDecodeFunc(value, body); err != nil { 686 | return param.myError(err.Error()) 687 | } 688 | } else if param.IsRequired() { 689 | return param.myError("missing body param") 690 | } 691 | 692 | case "header": 693 | paramValueBytes := req.Request.Header.Peek(param.name) 694 | if paramValueBytes != nil { 695 | if err = convertAssign(value, []string{string(paramValueBytes)}); err != nil { 696 | return param.myError(err.Error()) 697 | } 698 | } else if param.IsRequired() { 699 | return param.myError("missing header param") 700 | } 701 | 702 | case "cookie": 703 | bcookie := req.Request.Header.Cookie(param.name) 704 | if bcookie != nil { 705 | switch value.Type().String() { 706 | case fasthttpCookieTypeString: 707 | c := fasthttp.AcquireCookie() 708 | defer fasthttp.ReleaseCookie(c) 709 | if err = c.ParseBytes(bcookie); err != nil { 710 | return param.myError(err.Error()) 711 | } 712 | value.Set(reflect.ValueOf(*c)) 713 | 714 | default: 715 | if err = convertAssign(value, []string{string(bcookie)}); err != nil { 716 | return param.myError(err.Error()) 717 | } 718 | } 719 | } else if param.IsRequired() { 720 | return param.myError("missing cookie param") 721 | } 722 | } 723 | if err = param.validate(value); err != nil { 724 | return err 725 | } 726 | } 727 | return 728 | } 729 | 730 | // fasthttpFormValues returns all post data values with their keys 731 | // multipart, formValues data, post arguments 732 | func fasthttpFormValues(req *fasthttp.RequestCtx) map[string][]string { 733 | // first check if we have multipart formValues 734 | multipartForm, err := req.MultipartForm() 735 | if err == nil { 736 | //we have multipart formValues 737 | return multipartForm.Value 738 | } 739 | valuesAll := make(map[string][]string) 740 | // if no multipart and post arguments ( means normal formValues ) 741 | if req.PostArgs().Len() == 0 { 742 | return valuesAll // no found 743 | } 744 | req.PostArgs().VisitAll(func(k []byte, v []byte) { 745 | key := string(k) 746 | value := string(v) 747 | // for slices 748 | if valuesAll[key] != nil { 749 | valuesAll[key] = append(valuesAll[key], value) 750 | } else { 751 | valuesAll[key] = []string{value} 752 | } 753 | }) 754 | return valuesAll 755 | } 756 | --------------------------------------------------------------------------------