Added plugin support for generic types at the web. (#5771)
* feature:Added plugin support for generic types at the web. * fix:Fix scanning issues. * feature:Added options for plugin. * feature:Remove unnecessary code. * refactor:change package and go file name. * feature:add usage example. * feature:add usage example. * fix: fix issues. * fix: fix issues.
This commit is contained in:
parent
5d5a166efd
commit
76259cc817
83
server/web/generic_wrapper.go
Normal file
83
server/web/generic_wrapper.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Copyright 2014 beego Author. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/beego/beego/v2/core/logs"
|
||||||
|
"github.com/beego/beego/v2/server/web/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bizFunc is a function that wraps a business logic function with a context and parameter extraction function.
|
||||||
|
type bizFunc[T any] func(ctx *context.Context, param T) (any, error)
|
||||||
|
|
||||||
|
// extractFunc is a function that extracts parameters from the context.
|
||||||
|
type extractFunc[T any] func(ctx *context.Context) (params T, err error)
|
||||||
|
|
||||||
|
// WrapperFromJson for handling JSON in request's body.
|
||||||
|
// It binds the JSON request body to the specified type T
|
||||||
|
// Usage can see test cases : ExampleWrapperFromJson
|
||||||
|
func WrapperFromJson[T any](
|
||||||
|
biz bizFunc[T]) func(ctx *context.Context) {
|
||||||
|
return internalWrapper(biz, func(ctx *context.Context) (params T, err error) {
|
||||||
|
err = ctx.BindJSON(¶ms)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapperFromForm for handling form data in request.
|
||||||
|
// It binds the form data to the specified type T
|
||||||
|
// Usage can see test cases : ExampleWrapperFromForm
|
||||||
|
func WrapperFromForm[T any](
|
||||||
|
biz bizFunc[T]) func(ctx *context.Context) {
|
||||||
|
return internalWrapper(biz, func(ctx *context.Context) (params T, err error) {
|
||||||
|
err = ctx.BindForm(¶ms)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper is use by beego ctx.Bind(any) api
|
||||||
|
// It binds the data to the specified type T
|
||||||
|
// Usage can see test cases: ExampleWrapper
|
||||||
|
func Wrapper[T any](
|
||||||
|
biz bizFunc[T]) func(ctx *context.Context) {
|
||||||
|
return internalWrapper(biz, func(ctx *context.Context) (params T, err error) {
|
||||||
|
err = ctx.Bind(¶ms)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func internalWrapper[T any](
|
||||||
|
biz bizFunc[T],
|
||||||
|
ef extractFunc[T]) func(ctx *context.Context) {
|
||||||
|
return func(ctx *context.Context) {
|
||||||
|
params, err := ef(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logs.Error("err {%v} happen in subject ctx ", err)
|
||||||
|
ctx.Abort(400, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := biz(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
logs.Error("err {%v} happen in biz ", err)
|
||||||
|
ctx.Abort(500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ctx.Resp(res)
|
||||||
|
if err != nil {
|
||||||
|
logs.Error("err {%v} happen in write response ", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
318
server/web/generic_wrapper_test.go
Normal file
318
server/web/generic_wrapper_test.go
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
// Copyright 2014 beego Author. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
ctx0 "context"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/beego/beego/v2/server/web/context"
|
||||||
|
"github.com/beego/beego/v2/server/web/session"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sendUrl = "/api/data"
|
||||||
|
contentType = "Content-Type"
|
||||||
|
accept = "Accept"
|
||||||
|
)
|
||||||
|
|
||||||
|
// userRequest is a struct that represents the request parameters.
|
||||||
|
type userRequest struct {
|
||||||
|
Name string `json:"name" form:"name"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// addUser is a sample business logic function that takes a context and userRequest as parameters.
|
||||||
|
func addUser(_ *context.Context, params userRequest) (any, error) {
|
||||||
|
if params.Name == "" {
|
||||||
|
return nil, errors.New("name can't be null")
|
||||||
|
}
|
||||||
|
return []any{params.Name, params.Age}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleWrapperFromJson is an example of using WrapperFromJson to handle JSON requests.
|
||||||
|
func ExampleWrapperFromJson() {
|
||||||
|
app := NewHttpServerWithCfg(newBConfig())
|
||||||
|
app.Cfg.CopyRequestBody = true
|
||||||
|
path := sendUrl
|
||||||
|
// 使用 wrapper
|
||||||
|
app.Post(path, Wrapper(addUser))
|
||||||
|
|
||||||
|
reader := strings.NewReader(`{"name": "rose", "age": 17}`)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", path, reader)
|
||||||
|
req.Header.Set(contentType, context.ApplicationJSON)
|
||||||
|
req.Header.Set(accept, "*/*")
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
app.Handlers.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// 输出结果
|
||||||
|
fmt.Println(w.Code)
|
||||||
|
fmt.Println(w.Body.String())
|
||||||
|
// Output:
|
||||||
|
// 200
|
||||||
|
// ["rose",17]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleWrapperFromForm demonstrates the usage of WrapperFromForm to handle form data.
|
||||||
|
func ExampleWrapperFromForm() {
|
||||||
|
app := NewHttpServerWithCfg(newBConfig())
|
||||||
|
//app.Cfg.CopyRequestBody = true
|
||||||
|
path := sendUrl
|
||||||
|
// Use wrapper
|
||||||
|
app.Post(path, Wrapper(addUser))
|
||||||
|
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("name", "jack")
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", path, strings.NewReader(formData.Encode()))
|
||||||
|
req.Header.Set(contentType, context.ApplicationForm)
|
||||||
|
req.Header.Set(accept, "*/*")
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
app.Handlers.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Output the result
|
||||||
|
fmt.Println(w.Code)
|
||||||
|
fmt.Println(w.Body.String())
|
||||||
|
// Output:
|
||||||
|
// 200
|
||||||
|
// ["jack",0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleWrapper demonstrates the usage of Wrapper to handle requests.
|
||||||
|
func ExampleWrapper() {
|
||||||
|
app := NewHttpServerWithCfg(newBConfig())
|
||||||
|
app.Cfg.CopyRequestBody = true
|
||||||
|
path := sendUrl
|
||||||
|
// 使用 wrapper
|
||||||
|
app.Post(path, Wrapper(addUser))
|
||||||
|
|
||||||
|
request := userRequest{
|
||||||
|
Name: "tom",
|
||||||
|
Age: 18,
|
||||||
|
}
|
||||||
|
marshal, _ := xml.Marshal(request)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", path, bytes.NewBuffer(marshal))
|
||||||
|
req.Header.Set(contentType, context.ApplicationXML)
|
||||||
|
req.Header.Set(accept, "*/*")
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
app.Handlers.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
fmt.Println(w.Code)
|
||||||
|
fmt.Println(w.Body.String())
|
||||||
|
// Output:
|
||||||
|
// 200
|
||||||
|
// ["tom",18]
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllWrapperTestCase(t *testing.T) {
|
||||||
|
|
||||||
|
type MyStruct struct {
|
||||||
|
Foo string `form:"foo" json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
myStruct := MyStruct{Foo: "bar"}
|
||||||
|
|
||||||
|
webFunc := func(_ *context.Context, s1 MyStruct) (any, error) {
|
||||||
|
return s1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
expectedCode int
|
||||||
|
expectedRes func() string
|
||||||
|
reqBody func() io.Reader
|
||||||
|
reqHeader map[string]string
|
||||||
|
useDefaultSession bool
|
||||||
|
contentType string
|
||||||
|
bizProvider func() HandleFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test post json requestBody",
|
||||||
|
expectedCode: http.StatusOK,
|
||||||
|
expectedRes: func() string {
|
||||||
|
marshal, _ := json.Marshal(myStruct)
|
||||||
|
return string(marshal)
|
||||||
|
},
|
||||||
|
reqBody: func() io.Reader {
|
||||||
|
return strings.NewReader(`{"foo": "bar"}`)
|
||||||
|
},
|
||||||
|
contentType: context.ApplicationJSON,
|
||||||
|
bizProvider: func() HandleFunc {
|
||||||
|
return WrapperFromJson(webFunc)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test post form requestBody",
|
||||||
|
expectedCode: http.StatusOK,
|
||||||
|
expectedRes: func() string {
|
||||||
|
marshal, _ := json.Marshal(myStruct)
|
||||||
|
return string(marshal)
|
||||||
|
},
|
||||||
|
reqBody: func() io.Reader {
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("foo", "bar")
|
||||||
|
return strings.NewReader(formData.Encode())
|
||||||
|
},
|
||||||
|
contentType: context.ApplicationForm,
|
||||||
|
bizProvider: func() HandleFunc {
|
||||||
|
return WrapperFromForm(webFunc)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test base binging",
|
||||||
|
expectedCode: http.StatusOK,
|
||||||
|
expectedRes: func() string {
|
||||||
|
marshal, _ := json.Marshal(myStruct)
|
||||||
|
return string(marshal)
|
||||||
|
},
|
||||||
|
reqBody: func() io.Reader {
|
||||||
|
marshal, _ := xml.Marshal(myStruct)
|
||||||
|
return bytes.NewBuffer(marshal)
|
||||||
|
},
|
||||||
|
contentType: context.ApplicationXML,
|
||||||
|
bizProvider: func() HandleFunc {
|
||||||
|
return Wrapper(webFunc)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test unWrapper error",
|
||||||
|
expectedCode: http.StatusBadRequest,
|
||||||
|
reqBody: func() io.Reader {
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("foo", "bar")
|
||||||
|
return strings.NewReader(formData.Encode())
|
||||||
|
},
|
||||||
|
contentType: context.ApplicationForm,
|
||||||
|
bizProvider: func() HandleFunc {
|
||||||
|
return internalWrapper(webFunc, func(ctx *context.Context) (params MyStruct, err error) {
|
||||||
|
err = errors.New("paras entity error")
|
||||||
|
return
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test biz error",
|
||||||
|
expectedCode: http.StatusInternalServerError,
|
||||||
|
reqBody: func() io.Reader {
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("foo", "bar")
|
||||||
|
return strings.NewReader(formData.Encode())
|
||||||
|
},
|
||||||
|
contentType: context.ApplicationForm,
|
||||||
|
bizProvider: func() HandleFunc {
|
||||||
|
testFunc := func(_ *context.Context, _ MyStruct) (any, error) {
|
||||||
|
return nil, errors.New("biz error")
|
||||||
|
}
|
||||||
|
return WrapperFromForm(testFunc)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
// 1. init web server
|
||||||
|
app := NewHttpServerWithCfg(newBConfig())
|
||||||
|
// 2. set copy request body
|
||||||
|
app.Cfg.CopyRequestBody = true
|
||||||
|
// need to config session before register route
|
||||||
|
if tc.useDefaultSession {
|
||||||
|
c := defaultSessionOption(app)
|
||||||
|
// clear session config
|
||||||
|
defer c()
|
||||||
|
}
|
||||||
|
// 3. register route
|
||||||
|
path := sendUrl
|
||||||
|
app.Post(path, tc.bizProvider())
|
||||||
|
|
||||||
|
// 4. create request
|
||||||
|
req := httptest.NewRequest("POST", path, tc.reqBody())
|
||||||
|
req.Header.Set(contentType, tc.contentType)
|
||||||
|
req.Header.Set(accept, "*/*")
|
||||||
|
|
||||||
|
for key, value := range tc.reqHeader {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. create ResponseRecorder
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// 6. process request
|
||||||
|
app.Handlers.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectedCode, w.Code)
|
||||||
|
if w.Code == http.StatusOK {
|
||||||
|
assert.Equal(t, tc.expectedRes(), w.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type userInfo struct {
|
||||||
|
ID int
|
||||||
|
Username string
|
||||||
|
Role string
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionKey = "user_info"
|
||||||
|
|
||||||
|
var defaultUser = userInfo{
|
||||||
|
ID: 0,
|
||||||
|
Username: "guest",
|
||||||
|
Role: "guest",
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSessionOption(app *HttpServer) (cancel func()) {
|
||||||
|
|
||||||
|
config := `{"cookieName":"gosessionid","enableSetCookie":false,"gclifetime":3600,"ProviderConfig":"{\"cookieName\":\"gosessionid\",\"securityKey\":\"beegocookiehashkey\"}"}`
|
||||||
|
conf := new(session.ManagerConfig)
|
||||||
|
_ = json.Unmarshal([]byte(config), conf)
|
||||||
|
GlobalSessions, _ = session.NewManager("cookie", conf)
|
||||||
|
|
||||||
|
app.Cfg.WebConfig.Session.SessionOn = true
|
||||||
|
app.Cfg.WebConfig.Session.SessionProvider = "memory"
|
||||||
|
app.Cfg.WebConfig.Session.SessionName = "beegoSessionId"
|
||||||
|
app.Cfg.WebConfig.Session.SessionGCMaxLifetime = 3600
|
||||||
|
|
||||||
|
app.InsertFilter("*", BeforeExec, func(ctx *context.Context) {
|
||||||
|
if ctx.Input.Session(sessionKey) == nil {
|
||||||
|
timeout, c := ctx0.WithTimeout(ctx0.Background(), time.Minute*10)
|
||||||
|
defer c()
|
||||||
|
_ = ctx.Input.CruSession.Set(timeout, "user_info", &defaultUser)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
GlobalSessions = nil
|
||||||
|
app.Cfg.WebConfig.Session.SessionOn = false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user