mirror of
https://github.com/gin-gonic/gin.git
synced 2025-04-06 03:57:46 +08:00
Merge 14bae4aef34259de3f1341a952f6b795759b10c0 into 8763f33c65f7df8be5b9fe7504ab7fcf20abb41d
This commit is contained in:
commit
b44001b3d3
114
auto_binder.go
Normal file
114
auto_binder.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultAutoBinderErrorHandler = func(ctx *Context, err error) {
|
||||||
|
ctx.Error(err)
|
||||||
|
ctx.Abort()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type binderType func(obj any) error
|
||||||
|
|
||||||
|
func isFunc(obj any) bool {
|
||||||
|
return reflect.TypeOf(obj).Kind() == reflect.Func
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGinContext(rt reflect.Type) bool {
|
||||||
|
return rt == reflect.TypeOf((*Context)(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPtr(rt reflect.Type) bool {
|
||||||
|
return rt.Kind() == reflect.Pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
func isStruct(rt reflect.Type) bool {
|
||||||
|
return rt.Kind() == reflect.Struct
|
||||||
|
}
|
||||||
|
|
||||||
|
func constructStruct(prt reflect.Type, binder binderType) (reflect.Value, error) {
|
||||||
|
var pInstancePtr any
|
||||||
|
|
||||||
|
if isPtr(prt) {
|
||||||
|
pInstancePtr = reflect.New(prt.Elem()).Interface()
|
||||||
|
} else {
|
||||||
|
pInstancePtr = reflect.New(prt).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := binder(pInstancePtr); err != nil {
|
||||||
|
return reflect.Value{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if prt.Kind() == reflect.Pointer {
|
||||||
|
return reflect.ValueOf(pInstancePtr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.ValueOf(pInstancePtr).Elem(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callHandler(rt reflect.Type, rv reflect.Value, ctx *Context, binder binderType) error {
|
||||||
|
numberOfParams := rt.NumIn()
|
||||||
|
|
||||||
|
var args []reflect.Value
|
||||||
|
|
||||||
|
for i := 0; i < numberOfParams; i++ {
|
||||||
|
prt := rt.In(i)
|
||||||
|
|
||||||
|
if isGinContext(prt) {
|
||||||
|
args = append(args, reflect.ValueOf(ctx))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isStruct(prt) || isStruct(prt.Elem()) {
|
||||||
|
if prv, err := constructStruct(prt, binder); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
args = append(args, prv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.Call(args)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoBinder is a handler wrapper that binds the actual handler's request.
|
||||||
|
//
|
||||||
|
// Example: func MyGetHandler(ctx *gin.Context, request *MyRequest) {}
|
||||||
|
//
|
||||||
|
// engine.GET("/endpoint", gin.AutoBinder(MyGetHandler)) and you can handel the errors by passing a handler
|
||||||
|
//
|
||||||
|
// engine.GET("/endpoint", gin.AutoBinder(MyGetHandler, func(ctx *gin.Context, err error) {}))
|
||||||
|
func AutoBinder(handler any, errorHandler ...func(*Context, error)) HandlerFunc {
|
||||||
|
rt := reflect.TypeOf(handler)
|
||||||
|
|
||||||
|
if rt.Kind() != reflect.Func {
|
||||||
|
panic(errors.New("invalid handler type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if rt.NumIn() == 0 {
|
||||||
|
panic(fmt.Errorf("handler should have at least one parameter, handler: %v", rt.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(ctx *Context) {
|
||||||
|
selectedErrorHandler := defaultAutoBinderErrorHandler
|
||||||
|
if len(errorHandler) > 0 && errorHandler[0] != nil {
|
||||||
|
selectedErrorHandler = errorHandler[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := reflect.TypeOf(handler)
|
||||||
|
rv := reflect.ValueOf(handler)
|
||||||
|
|
||||||
|
if err := callHandler(rt, rv, ctx, func(obj any) error {
|
||||||
|
return ctx.ShouldBind(obj)
|
||||||
|
}); err != nil {
|
||||||
|
selectedErrorHandler(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
auto_binder_test.go
Normal file
144
auto_binder_test.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type myRequest struct {
|
||||||
|
Field1 string `json:"field_1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoBinder_isFunc(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input any
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"valid function",
|
||||||
|
func(string, int) error { return nil },
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid zero-param function",
|
||||||
|
func() error { return nil },
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid function",
|
||||||
|
func() string { return "" }(),
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actual := isFunc(tt.input)
|
||||||
|
assert.Equal(t, tt.want, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoBinder_isGinContext(t *testing.T) {
|
||||||
|
assert.True(t, isGinContext(reflect.TypeOf(&Context{})))
|
||||||
|
assert.False(t, isGinContext(reflect.TypeOf(Context{})))
|
||||||
|
assert.False(t, isGinContext(reflect.TypeOf([]string{})))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoBinder_constructStruct_pointer(t *testing.T) {
|
||||||
|
type myType struct {
|
||||||
|
Field int `json:"field"`
|
||||||
|
}
|
||||||
|
|
||||||
|
rv, err := constructStruct(reflect.TypeOf(&myType{}), func(obj any) error {
|
||||||
|
assert.True(t, isPtr(reflect.TypeOf(obj)))
|
||||||
|
|
||||||
|
return json.Unmarshal(
|
||||||
|
[]byte(`{"field": 10}`),
|
||||||
|
obj,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
instance, ok := rv.Interface().(*myType)
|
||||||
|
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
assert.Equal(t, 10, instance.Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoBinder_constructStruct_nonPointer(t *testing.T) {
|
||||||
|
type myType struct {
|
||||||
|
Field int `json:"field"`
|
||||||
|
}
|
||||||
|
|
||||||
|
rv, err := constructStruct(reflect.TypeOf(myType{}), func(obj any) error {
|
||||||
|
assert.True(t, isPtr(reflect.TypeOf(obj)))
|
||||||
|
|
||||||
|
return json.Unmarshal(
|
||||||
|
[]byte(`{"field": 10}`),
|
||||||
|
obj,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
instance, ok := rv.Interface().(myType)
|
||||||
|
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
assert.Equal(t, 10, instance.Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoBinder_constructStruct_nonStruct(t *testing.T) {
|
||||||
|
_, err := constructStruct(reflect.TypeOf("string test"), func(obj any) error {
|
||||||
|
assert.True(t, isPtr(reflect.TypeOf(obj)))
|
||||||
|
|
||||||
|
return json.Unmarshal(
|
||||||
|
[]byte(`{"field": 10}`),
|
||||||
|
obj,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoBinder_callHandler(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
|
||||||
|
handler := func(ctx *Context, req *myRequest) {
|
||||||
|
if ctx == nil {
|
||||||
|
t.Errorf("ctx should not passed as nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Field1 != "value1" {
|
||||||
|
t.Errorf("expected %v, actual %v", "value1", req.Field1)
|
||||||
|
}
|
||||||
|
|
||||||
|
called = true
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := reflect.TypeOf(handler)
|
||||||
|
rv := reflect.ValueOf(handler)
|
||||||
|
|
||||||
|
ctx := &Context{}
|
||||||
|
|
||||||
|
err := callHandler(rt, rv, ctx, func(obj any) error {
|
||||||
|
return json.Unmarshal([]byte(`{"field_1": "value1"}`), obj)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !called {
|
||||||
|
t.Error("handler should be called")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user