Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

refactor: binding #541

Merged
merged 91 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from 89 commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
15c8913
refactor: binding
FGYFFFF Jan 9, 2023
d08feef
feat: add struct binding
FGYFFFF Jan 12, 2023
65df9bc
refactor: refactor code style
FGYFFFF Jan 17, 2023
1f4da2d
feat: add default tag
FGYFFFF Jan 18, 2023
61da4bd
feat: add default value
FGYFFFF Jan 30, 2023
f287cdd
feat: add required validate
FGYFFFF Jan 30, 2023
260db67
feat: config json unmarshaler
FGYFFFF Jan 30, 2023
f20613e
feat: replace validator
FGYFFFF Jan 31, 2023
f965917
feat: add license
FGYFFFF Feb 1, 2023
9c0dd84
feat: add file bind
FGYFFFF Feb 9, 2023
4cbf863
style: go lint
FGYFFFF Apr 3, 2023
da1ccce
feat: go mod tidy
FGYFFFF Apr 3, 2023
4d00e4f
optimize: performance optimize
FGYFFFF May 9, 2023
a630b6b
refactor: layout
FGYFFFF May 10, 2023
6c9f894
optimzie: remove todo
FGYFFFF May 10, 2023
032b5fd
feat: unexported field
FGYFFFF May 10, 2023
4710c35
feat: add license
FGYFFFF May 10, 2023
40d41b2
ci: test
FGYFFFF May 10, 2023
8c85456
ci: go module
FGYFFFF May 10, 2023
b805df5
ci: getter
FGYFFFF May 10, 2023
15f5825
ci: gofump
FGYFFFF May 10, 2023
9ed9b83
feat: ignore field
FGYFFFF May 10, 2023
36ebf47
fix: some diff from go-tagexpr
FGYFFFF May 11, 2023
787bed4
fix: filter content type
FGYFFFF May 11, 2023
9ed9863
fix: diff between query and form
FGYFFFF May 11, 2023
f4cc3c4
ci: add license for tag_expr_test
FGYFFFF May 12, 2023
fb96343
fix: typo
FGYFFFF May 12, 2023
be72ddd
fix: byte slice bind raw body
FGYFFFF May 12, 2023
0dcc065
fix: typo
FGYFFFF May 12, 2023
4dbaf64
feat: add struct field
FGYFFFF May 16, 2023
8650309
feat: modify some comment
FGYFFFF May 17, 2023
64b06e8
feat: required validate
FGYFFFF May 17, 2023
8213aaa
ci: go fumpt
FGYFFFF May 17, 2023
b0802fd
ci: license
FGYFFFF May 17, 2023
6fc6b87
ci: lint
FGYFFFF May 18, 2023
ae3ee12
ci: test panic
FGYFFFF May 18, 2023
4d70b0f
feat: add customized type deocder
FGYFFFF May 19, 2023
d498b12
optimize: optimize performance
FGYFFFF May 23, 2023
46bda3a
optimize: optimize performence
FGYFFFF May 24, 2023
f3b95f5
ci: ci
FGYFFFF May 24, 2023
bc88f03
optimzie: slice performance
FGYFFFF May 24, 2023
eceae04
optimize: remove cache http info
FGYFFFF May 24, 2023
4ade782
ci:ci
FGYFFFF May 24, 2023
fb4680e
feat: more api
FGYFFFF May 26, 2023
38795e5
refactor: add internal
FGYFFFF May 26, 2023
01b9109
feat: assign to gin
FGYFFFF May 31, 2023
b773fe8
feat: align gin for post-form
FGYFFFF Aug 22, 2023
44eb90e
feat: modify multi-pointer erorr
FGYFFFF Aug 23, 2023
f4fcbee
optimize: more comment
FGYFFFF Aug 23, 2023
655fe6c
refactor: bind func signature
FGYFFFF Aug 24, 2023
ab16a83
feat: add context bindXXX comment
FGYFFFF Aug 24, 2023
f9be06f
refactor: BindByContentType
FGYFFFF Aug 24, 2023
79bb2b0
feat: license to 2023
FGYFFFF Aug 24, 2023
76e373a
feat: use consts content-type
FGYFFFF Aug 24, 2023
d74c955
refactor: json alias
FGYFFFF Aug 24, 2023
b4361e7
refactor: var location
FGYFFFF Aug 24, 2023
3dcd79a
feat: add setLooseMode
FGYFFFF Aug 25, 2023
6c2c398
feat: add non-struct bind
FGYFFFF Aug 28, 2023
2ee7e1b
feat: more test coverage
FGYFFFF Aug 28, 2023
4f80efc
feat: more test coverage
FGYFFFF Aug 28, 2023
40a7b05
feat: struct decode error to warn
FGYFFFF Aug 28, 2023
2398330
feat: more test coverage
FGYFFFF Aug 28, 2023
1d23b78
feat: support interface
FGYFFFF Aug 28, 2023
cd687f8
feat: more test coverage
FGYFFFF Aug 28, 2023
5269fce
feat: more test coverage
FGYFFFF Aug 29, 2023
d2ad533
refactor: rm old test file
FGYFFFF Sep 12, 2023
d79a390
feat: add license
FGYFFFF Sep 12, 2023
5170a89
fix: typo
FGYFFFF Sep 12, 2023
e6961f1
fix: golong lint
FGYFFFF Sep 12, 2023
02f3bdf
feat: resolve struct by default
FGYFFFF Sep 14, 2023
2299988
fix: gjson for windows
FGYFFFF Sep 19, 2023
f2e61b9
feat: reflect internal test
FGYFFFF Sep 19, 2023
1a3de68
feat: warn to info
FGYFFFF Sep 19, 2023
022954f
fix: modify test
FGYFFFF Sep 19, 2023
df13ac4
feat: go mod tidy
FGYFFFF Sep 19, 2023
20990ff
fix: call function
FGYFFFF Sep 19, 2023
8b4c98d
refactor: set binder and validator
FGYFFFF Sep 19, 2023
8cd8217
feat: add utils test
FGYFFFF Sep 19, 2023
4664a4c
feat: context test
FGYFFFF Sep 19, 2023
19c66a6
refactor: refactor config
FGYFFFF Sep 20, 2023
92ea388
fix: typo
FGYFFFF Sep 20, 2023
81aa5a8
refactor: default bind and validate
FGYFFFF Sep 20, 2023
a81df8a
fix: route test
FGYFFFF Sep 20, 2023
6994515
fix: ci
FGYFFFF Sep 20, 2023
ec867c7
fix: option test
FGYFFFF Sep 20, 2023
3a7ebed
feat: context test
FGYFFFF Sep 20, 2023
a6cfb63
fix: more copy
FGYFFFF Sep 21, 2023
e2ce1ce
fix: validate
FGYFFFF Sep 21, 2023
e4084df
feat: enable config to disable
FGYFFFF Sep 21, 2023
a6e4159
refactor: ctx.bind interface
FGYFFFF Sep 22, 2023
256f9bb
feat: remove normalize for header bind
FGYFFFF Sep 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ jobs:
# Exit with 1 when it find at least one finding.
fail_on_error: true
# Set staticcheck flags
staticcheck_flags: -checks=inherit,-SA1029
staticcheck_flags: -checks=inherit,-SA1029,-SA5008
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/bytedance/sonic v1.8.1
github.com/cloudwego/netpoll v0.4.2-0.20230807055039-52fd5fb7b00f
github.com/fsnotify/fsnotify v1.5.4
github.com/tidwall/gjson v1.13.0 // indirect
github.com/tidwall/gjson v1.14.4
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
google.golang.org/protobuf v1.27.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.13.0 h1:3TFY9yxOQShrvmjdM76K+jc66zJeT6D3/VFFYCGQf7M=
github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
Expand Down
100 changes: 100 additions & 0 deletions pkg/app/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ type RequestContext struct {

// clientIPFunc get form value by use custom function.
formValueFunc FormValueFunc

binder binding.Binder
validator binding.StructValidator
}

// Flush is the shortcut for ctx.Response.GetHijackWriter().Flush().
Expand All @@ -252,6 +255,14 @@ func (ctx *RequestContext) SetFormValueFunc(f FormValueFunc) {
ctx.formValueFunc = f
}

func (ctx *RequestContext) SetBinder(binder binding.Binder) {
ctx.binder = binder
}

func (ctx *RequestContext) SetValidator(validator binding.StructValidator) {
ctx.validator = validator
}

func (ctx *RequestContext) GetTraceInfo() traceinfo.TraceInfo {
return ctx.traceInfo
}
Expand Down Expand Up @@ -732,6 +743,10 @@ func (ctx *RequestContext) Copy() *RequestContext {
paramCopy := make([]param.Param, len(cp.Params))
copy(paramCopy, cp.Params)
cp.Params = paramCopy
cp.clientIPFunc = ctx.clientIPFunc
cp.formValueFunc = ctx.formValueFunc
cp.binder = ctx.binder
cp.validator = ctx.validator
return cp
}

Expand Down Expand Up @@ -1305,21 +1320,106 @@ func bodyAllowedForStatus(status int) bool {
// BindAndValidate binds data from *RequestContext to obj and validates them if needed.
// NOTE: obj should be a pointer.
func (ctx *RequestContext) BindAndValidate(obj interface{}) error {
if ctx.binder != nil {
return ctx.binder.BindAndValidate(&ctx.Request, obj, ctx.Params)
}
return binding.BindAndValidate(&ctx.Request, obj, ctx.Params)
}

// Bind binds data from *RequestContext to obj.
// NOTE: obj should be a pointer.
func (ctx *RequestContext) Bind(obj interface{}) error {
if ctx.binder != nil {
return ctx.binder.Bind(&ctx.Request, obj, ctx.Params)
}
return binding.Bind(&ctx.Request, obj, ctx.Params)
}

// Validate validates obj with "vd" tag
// NOTE: obj should be a pointer.
func (ctx *RequestContext) Validate(obj interface{}) error {
if ctx.validator != nil {
return ctx.validator.ValidateStruct(obj)
}
return binding.Validate(obj)
}

// BindQuery binds query parameters from *RequestContext to obj with 'query' tag. It will only use 'query' tag for binding.
// NOTE: obj should be a pointer.
func (ctx *RequestContext) BindQuery(obj interface{}) error {
welkeyever marked this conversation as resolved.
Show resolved Hide resolved
if ctx.binder != nil {
return ctx.binder.BindQuery(&ctx.Request, obj)
}
return binding.DefaultBinder().BindQuery(&ctx.Request, obj)
}

// BindHeader binds header parameters from *RequestContext to obj with 'header' tag. It will only use 'header' tag for binding.
// NOTE: obj should be a pointer.
func (ctx *RequestContext) BindHeader(obj interface{}) error {
if ctx.binder != nil {
return ctx.binder.BindHeader(&ctx.Request, obj)
}
return binding.DefaultBinder().BindHeader(&ctx.Request, obj)
}

// BindPath binds router parameters from *RequestContext to obj with 'path' tag. It will only use 'path' tag for binding.
// NOTE: obj should be a pointer.
func (ctx *RequestContext) BindPath(obj interface{}) error {
if ctx.binder != nil {
welkeyever marked this conversation as resolved.
Show resolved Hide resolved
return ctx.binder.BindPath(&ctx.Request, obj, ctx.Params)
}
return binding.DefaultBinder().BindPath(&ctx.Request, obj, ctx.Params)
}

// BindForm binds form parameters from *RequestContext to obj with 'form' tag. It will only use 'form' tag for binding.
// NOTE: obj should be a pointer.
func (ctx *RequestContext) BindForm(obj interface{}) error {
if len(ctx.Request.Body()) == 0 {
return fmt.Errorf("missing form body")
}
if ctx.binder != nil {
FGYFFFF marked this conversation as resolved.
Show resolved Hide resolved
return ctx.binder.BindForm(&ctx.Request, obj)
}
return binding.DefaultBinder().BindForm(&ctx.Request, obj)
}

// BindJSON binds JSON body from *RequestContext.
// NOTE: obj should be a pointer.
func (ctx *RequestContext) BindJSON(obj interface{}) error {
if ctx.binder != nil {
return ctx.binder.BindJSON(&ctx.Request, obj)
}
return binding.DefaultBinder().BindJSON(&ctx.Request, obj)
}

// BindProtobuf binds protobuf body from *RequestContext.
// NOTE: obj should be a pointer.
func (ctx *RequestContext) BindProtobuf(obj interface{}) error {
if ctx.binder != nil {
return ctx.binder.BindProtobuf(&ctx.Request, obj)
}
return binding.DefaultBinder().BindProtobuf(&ctx.Request, obj)
}

// BindByContentType will select the binding type on the ContentType automatically.
// NOTE: obj should be a pointer.
func (ctx *RequestContext) BindByContentType(obj interface{}) error {
if ctx.Request.Header.IsGet() {
return ctx.BindQuery(obj)
}
ct := utils.FilterContentType(bytesconv.B2s(ctx.Request.Header.ContentType()))
switch ct {
case consts.MIMEApplicationJSON:
return ctx.BindJSON(obj)
case consts.MIMEPROTOBUF:
return ctx.BindProtobuf(obj)
case consts.MIMEApplicationHTMLForm, consts.MIMEMultipartPOSTForm:
return ctx.BindForm(obj)
welkeyever marked this conversation as resolved.
Show resolved Hide resolved
default:
return fmt.Errorf("unsupported bind content-type for '%s'", ct)
}
}

// VisitAllQueryArgs calls f for each existing query arg.
//
// f must not retain references to key and value after returning.
Expand Down
118 changes: 118 additions & 0 deletions pkg/app/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (

"github.com/cloudwego/hertz/internal/bytesconv"
"github.com/cloudwego/hertz/internal/bytestr"
"github.com/cloudwego/hertz/pkg/app/server/binding"
"github.com/cloudwego/hertz/pkg/app/server/render"
errs "github.com/cloudwego/hertz/pkg/common/errors"
"github.com/cloudwego/hertz/pkg/common/test/assert"
Expand Down Expand Up @@ -873,6 +874,35 @@ func TestSetClientIPFunc(t *testing.T) {
assert.DeepEqual(t, reflect.ValueOf(fn).Pointer(), reflect.ValueOf(defaultClientIP).Pointer())
}

type mockValidator struct{}

func (m *mockValidator) ValidateStruct(interface{}) error {
return fmt.Errorf("test mock")
}

func (m *mockValidator) Engine() interface{} {
return nil
}

func TestSetValidator(t *testing.T) {
m := &mockValidator{}
c := NewContext(0)
c.SetValidator(m)
c.SetBinder(binding.NewDefaultBinder(&binding.BindConfig{ValidateTag: "vt"}))
type User struct {
Age int `vt:"$>=0&&$<=130"`
}

user := &User{
Age: 135,
}
err := c.Validate(user)
if err == nil {
t.Fatalf("expected an error, but got nil")
}
assert.DeepEqual(t, "test mock", err.Error())
}

func TestGetQuery(t *testing.T) {
c := NewContext(0)
c.Request.SetRequestURI("http://aaa.com?a=1&b=")
Expand Down Expand Up @@ -1457,6 +1487,94 @@ func TestBindAndValidate(t *testing.T) {
}
}

func TestBindForm(t *testing.T) {
type Test struct {
A string
B int
}

c := &RequestContext{}
c.Request.SetRequestURI("/foo/bar?a=123&b=11")
c.Request.SetBody([]byte("A=123&B=11"))
c.Request.Header.SetContentTypeBytes([]byte("application/x-www-form-urlencoded"))

var req Test
err := c.BindForm(&req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assert.DeepEqual(t, "123", req.A)
assert.DeepEqual(t, 11, req.B)

c.Request.SetBody([]byte(""))
err = c.BindForm(&req)
if err == nil {
t.Fatalf("expected error, but get nil")
}
}

type mockBinder struct{}

func (m *mockBinder) Name() string {
return "test binder"
}

func (m *mockBinder) Bind(request *protocol.Request, i interface{}, params param.Params) error {
return nil
}

func (m *mockBinder) BindAndValidate(request *protocol.Request, i interface{}, params param.Params) error {
return fmt.Errorf("test binder")
}

func (m *mockBinder) BindQuery(request *protocol.Request, i interface{}) error {
return nil
}

func (m *mockBinder) BindHeader(request *protocol.Request, i interface{}) error {
return nil
}

func (m *mockBinder) BindPath(request *protocol.Request, i interface{}, params param.Params) error {
return nil
}

func (m *mockBinder) BindForm(request *protocol.Request, i interface{}) error {
return nil
}

func (m *mockBinder) BindJSON(request *protocol.Request, i interface{}) error {
return nil
}

func (m *mockBinder) BindProtobuf(request *protocol.Request, i interface{}) error {
return nil
}

func TestSetBinder(t *testing.T) {
c := NewContext(0)
c.SetBinder(&mockBinder{})
type T struct{}
req := T{}
err := c.Bind(&req)
assert.Nil(t, err)
err = c.BindAndValidate(&req)
assert.NotNil(t, err)
assert.DeepEqual(t, "test binder", err.Error())
err = c.BindProtobuf(&req)
assert.Nil(t, err)
err = c.BindJSON(&req)
assert.Nil(t, err)
err = c.BindForm(&req)
assert.NotNil(t, err)
err = c.BindPath(&req)
assert.Nil(t, err)
err = c.BindQuery(&req)
assert.Nil(t, err)
err = c.BindHeader(&req)
assert.Nil(t, err)
}

func TestRequestContext_SetCookie(t *testing.T) {
c := NewContext(0)
c.SetCookie("user", "hertz", 1, "/", "localhost", protocol.CookieSameSiteLaxMode, true, true)
Expand Down
58 changes: 58 additions & 0 deletions pkg/app/server/binding/binder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2023 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* MIT License
*
* Copyright (c) 2019-present Fenny and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* This file may have been modified by CloudWeGo authors. All CloudWeGo
* Modifications are Copyright 2023 CloudWeGo Authors
*/

package binding

import (
"github.com/cloudwego/hertz/pkg/protocol"
"github.com/cloudwego/hertz/pkg/route/param"
)

type Binder interface {
Name() string
Bind(*protocol.Request, interface{}, param.Params) error
BindAndValidate(*protocol.Request, interface{}, param.Params) error
BindQuery(*protocol.Request, interface{}) error
BindHeader(*protocol.Request, interface{}) error
BindPath(*protocol.Request, interface{}, param.Params) error
BindForm(*protocol.Request, interface{}) error
BindJSON(*protocol.Request, interface{}) error
BindProtobuf(*protocol.Request, interface{}) error
}
Loading