mirror of
https://github.com/gin-gonic/gin.git
synced 2025-04-06 03:57:46 +08:00
feature: implement request auto binder utility
This commit is contained in:
parent
53fbf4dbfb
commit
0e0ea62a94
100
auto_binder.go
Normal file
100
auto_binder.go
Normal file
@ -0,0 +1,100 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
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))
|
||||
func AutoBinder(handler any) 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) {
|
||||
rt := reflect.TypeOf(handler)
|
||||
rv := reflect.ValueOf(handler)
|
||||
|
||||
if err := callHandler(rt, rv, ctx, func(obj any) error {
|
||||
return ctx.ShouldBind(obj)
|
||||
}); err != nil {
|
||||
ctx.Error(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