diff --git a/CHANGELOG.md b/CHANGELOG.md index b9417ee6..8dbe15d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,8 +33,9 @@ - Improve: Avoid ignoring mistakes that need attention [4548](https://github.com/beego/beego/pull/4548) - Integration: DeepSource [4560](https://github.com/beego/beego/pull/4560) - Integration: Remove unnecessary function call [4577](https://github.com/beego/beego/pull/4577) - - +- Feature issue #4402 finish router get example. [4416](https://github.com/beego/beego/pull/4416) +- Proposal: Add Bind() method for `web.Controller` [4491](https://github.com/beego/beego/issues/4579) +- Optimize AddAutoPrefix: only register one router in case-insensitive mode. [4582](https://github.com/beego/beego/pull/4582) ## Fix Sonar - [4473](https://github.com/beego/beego/pull/4473) diff --git a/server/web/controller.go b/server/web/controller.go index 32378829..6a29c79e 100644 --- a/server/web/controller.go +++ b/server/web/controller.go @@ -17,8 +17,12 @@ package web import ( "bytes" context2 "context" + "encoding/json" + "encoding/xml" "errors" "fmt" + "github.com/gogo/protobuf/proto" + "gopkg.in/yaml.v2" "html/template" "io" "mime/multipart" @@ -239,6 +243,50 @@ func (c *Controller) HandlerFunc(fnname string) bool { // URLMapping register the internal Controller router. func (c *Controller) URLMapping() {} +func (c *Controller) Bind(obj interface{}) error { + ct, exist := c.Ctx.Request.Header["Content-Type"] + if !exist || len(ct) == 0 { + return c.BindJson(obj) + } + i, l := 0, len(ct[0]) + for ; i < l && ct[0][i] != ';'; i++ { + } + switch ct[0][0:i] { + case "application/json": + return c.BindJson(obj) + case "application/xml", "text/xml": + return c.BindXML(obj) + case "application/x-www-form-urlencoded": + return c.BindForm(obj) + case "application/x-protobuf": + return c.BindProtobuf(obj) + case "application/x-yaml": + return c.BindYAML(obj) + default: + return errors.New("Unsupported Content-Type:" + ct[0]) + } +} + +func (c *Controller) BindYAML(obj interface{}) error { + return yaml.Unmarshal(c.Ctx.Input.RequestBody, obj) +} + +func (c *Controller) BindForm(obj interface{}) error { + return c.ParseForm(obj) +} + +func (c *Controller) BindJson(obj interface{}) error { + return json.Unmarshal(c.Ctx.Input.RequestBody, obj) +} + +func (c *Controller) BindProtobuf(obj interface{}) error { + return proto.Unmarshal(c.Ctx.Input.RequestBody, obj.(proto.Message)) +} + +func (c *Controller) BindXML(obj interface{}) error { + return xml.Unmarshal(c.Ctx.Input.RequestBody, obj) +} + // Mapping the method to function func (c *Controller) Mapping(method string, fn func()) { c.methodMapping[method] = fn diff --git a/server/web/controller_test.go b/server/web/controller_test.go index 4f8b6d1c..8a52f097 100644 --- a/server/web/controller_test.go +++ b/server/web/controller_test.go @@ -15,7 +15,10 @@ package web import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "math" + "net/http" "os" "path/filepath" "strconv" @@ -180,3 +183,64 @@ func TestAdditionalViewPaths(t *testing.T) { ctrl.ViewPath = dir2 ctrl.RenderString() } + +func TestBindJson(t *testing.T) { + var s struct { + Foo string `json:"foo"` + } + header := map[string][]string{"Content-Type": {"application/json"}} + request := &http.Request{Header: header} + input := &context.BeegoInput{RequestBody: []byte(`{"foo": "FOO"}`)} + ctx := &context.Context{Request: request, Input: input} + ctrlr := Controller{Ctx: ctx} + err := ctrlr.Bind(&s) + require.NoError(t, err) + assert.Equal(t, "FOO", s.Foo) +} + +func TestBindNoContentType(t *testing.T) { + var s struct { + Foo string `json:"foo"` + } + header := map[string][]string{} + request := &http.Request{Header: header} + input := &context.BeegoInput{RequestBody: []byte(`{"foo": "FOO"}`)} + ctx := &context.Context{Request: request, Input: input} + ctrlr := Controller{Ctx: ctx} + err := ctrlr.Bind(&s) + require.NoError(t, err) + assert.Equal(t, "FOO", s.Foo) +} + +func TestBindXML(t *testing.T) { + + var s struct { + Foo string `xml:"foo"` + } + xmlBody := ` + + FOO +` + header := map[string][]string{"Content-Type": {"text/xml"}} + request := &http.Request{Header: header} + input := &context.BeegoInput{RequestBody: []byte(xmlBody)} + ctx := &context.Context{Request: request, Input: input} + ctrlr := Controller{Ctx: ctx} + err := ctrlr.Bind(&s) + require.NoError(t, err) + assert.Equal(t, "FOO", s.Foo) +} + +func TestBindYAML(t *testing.T) { + var s struct { + Foo string `yaml:"foo"` + } + header := map[string][]string{"Content-Type": {"application/x-yaml"}} + request := &http.Request{Header: header} + input := &context.BeegoInput{RequestBody: []byte("foo: FOO")} + ctx := &context.Context{Request: request, Input: input} + ctrlr := Controller{Ctx: ctx} + err := ctrlr.Bind(&s) + require.NoError(t, err) + assert.Equal(t, "FOO", s.Foo) +} diff --git a/server/web/router.go b/server/web/router.go index f9f8b322..7a578e4b 100644 --- a/server/web/router.go +++ b/server/web/router.go @@ -68,14 +68,21 @@ var ( "LOCK": true, "UNLOCK": true, } - // these beego.Controller's methods shouldn't reflect to AutoRouter - exceptMethod = []string{"Init", "Prepare", "Finish", "Render", "RenderString", - "RenderBytes", "Redirect", "Abort", "StopRun", "UrlFor", "ServeJSON", "ServeJSONP", - "ServeYAML", "ServeXML", "Input", "ParseForm", "GetString", "GetStrings", "GetInt", "GetBool", - "GetFloat", "GetFile", "SaveToFile", "StartSession", "SetSession", "GetSession", - "DelSession", "SessionRegenerateID", "DestroySession", "IsAjax", "GetSecureCookie", - "SetSecureCookie", "XsrfToken", "CheckXsrfCookie", "XsrfFormHtml", - "GetControllerAndAction", "ServeFormatted"} + // these web.Controller's methods shouldn't reflect to AutoRouter + exceptMethod = []string{"Abort", "CheckXSRFCookie", "CustomAbort", "DelSession", + "DestroySession", "Finish", "GetBool", "GetControllerAndAction", + "GetFile", "GetFiles", "GetFloat", "GetInt", "GetInt16", + "GetInt32", "GetInt64", "GetInt8", "GetSecureCookie", "GetSession", + "GetString", "GetStrings", "GetUint16", "GetUint32", "GetUint64", + "GetUint8", "HandlerFunc", "Init", "Input", + "IsAjax", "Mapping", "ParseForm", + "Prepare", "Redirect", "Render", "RenderBytes", + "RenderString", "SaveToFile", "SaveToFileWithBuffer", "SaveToFileWithBuffer", + "ServeFormatted", "ServeJSON", "ServeJSONP", "ServeXML", "ServeYAML", + "SessionRegenerateID", "SetData", "SetSecureCookie", "SetSession", "StartSession", + "StopRun", "URLFor", "URLMapping", "XSRFFormHTML", + "XSRFToken", + } urlPlaceholder = "{{placeholder}}" // DefaultAccessLogFilter will skip the accesslog if return true @@ -725,20 +732,30 @@ func (p *ControllerRegister) AddAutoPrefix(prefix string, c ControllerInterface) ct := reflect.Indirect(reflectVal).Type() controllerName := strings.TrimSuffix(ct.Name(), "Controller") for i := 0; i < rt.NumMethod(); i++ { - if !utils.InSlice(rt.Method(i).Name, exceptMethod) { - pattern := path.Join(prefix, strings.ToLower(controllerName), strings.ToLower(rt.Method(i).Name), "*") - patternInit := path.Join(prefix, controllerName, rt.Method(i).Name, "*") - patternFix := path.Join(prefix, strings.ToLower(controllerName), strings.ToLower(rt.Method(i).Name)) - patternFixInit := path.Join(prefix, controllerName, rt.Method(i).Name) + methodName := rt.Method(i).Name + if !utils.InSlice(methodName, exceptMethod) { + p.addAutoPrefixMethod(prefix, controllerName, methodName, ct) + } + } +} - route := p.createBeegoRouter(ct, pattern) - route.methods = map[string]string{"*": rt.Method(i).Name} - for m := range HTTPMETHOD { - p.addToRouter(m, pattern, route) - p.addToRouter(m, patternInit, route) - p.addToRouter(m, patternFix, route) - p.addToRouter(m, patternFixInit, route) - } +func (p *ControllerRegister) addAutoPrefixMethod(prefix, controllerName, methodName string, ctrl reflect.Type) { + pattern := path.Join(prefix, strings.ToLower(controllerName), strings.ToLower(methodName), "*") + patternInit := path.Join(prefix, controllerName, methodName, "*") + patternFix := path.Join(prefix, strings.ToLower(controllerName), strings.ToLower(methodName)) + patternFixInit := path.Join(prefix, controllerName, methodName) + + route := p.createBeegoRouter(ctrl, pattern) + route.methods = map[string]string{"*": methodName} + for m := range HTTPMETHOD { + + p.addToRouter(m, pattern, route) + + // only case sensitive, we add three more routes + if p.cfg.RouterCaseSensitive { + p.addToRouter(m, patternInit, route) + p.addToRouter(m, patternFix, route) + p.addToRouter(m, patternFixInit, route) } } }