diff --git a/binding/binder.go b/binding/binder.go index fd00d50..7886171 100644 --- a/binding/binder.go +++ b/binding/binder.go @@ -7,6 +7,7 @@ import ( type ( // Binder interface Binder interface { + Name() string Bind(r *http.Request, obj interface{}) error } @@ -31,19 +32,20 @@ var ( // YAML = YAMLBinder{} // MSGPACK = MSGPACKBinder{} // PROTOBUF = PROTOBUFBinder{} -) -var binders = map[string]Binder{ - "xml": XML, - "json": JSON, - "query": Query, - "form": Form, - "header": Header, - // TODO more driver - // "yaml": YAML, - // "msgpack": MSGPACK, - // "protobuf": PROTOBUF, -} + // Binders mapping + Binders = map[string]Binder{ + "xml": XML, + "json": JSON, + "query": Query, + "form": Form, + "header": Header, + // TODO more driver + // "yaml": YAML, + // "msgpack": MSGPACK, + // "protobuf": PROTOBUF, + } +) // BinderFunc implements the Binder interface func (fn BinderFunc) Name() string { @@ -55,9 +57,9 @@ func (fn BinderFunc) Bind(r *http.Request, obj interface{}) error { return fn(r, obj) } -// Get an binder by name -func Get(name string) Binder { - if b, ok := binders[name]; ok { +// GetBinder get an binder by name +func GetBinder(name string) Binder { + if b, ok := Binders[name]; ok { return b } return nil @@ -66,15 +68,15 @@ func Get(name string) Binder { // Register new binder with name func Register(name string, b Binder) { if name != "" && b != nil { - binders[name] = b + Binders[name] = b } } // Remove exists binder(s) func Remove(names ...string) { for _, name := range names { - if _, ok := binders[name]; ok { - delete(binders, name) + if _, ok := Binders[name]; ok { + delete(Binders, name) } } } diff --git a/binding/binder_test.go b/binding/binder_test.go new file mode 100644 index 0000000..848bb09 --- /dev/null +++ b/binding/binder_test.go @@ -0,0 +1,28 @@ +package binding_test + +import ( + "net/http" + "testing" + + "github.com/gookit/rux/binding" + "github.com/stretchr/testify/assert" +) + +func TestBinder_Name(t *testing.T) { + is := assert.New(t) + for name, binder := range binding.Binders { + is.Equal(name, binder.Name()) + } +} + +func TestGetBinder(t *testing.T) { + is := assert.New(t) + b := binding.GetBinder("query") + + req, err := http.NewRequest("GET", "/?"+userQuery, nil) + is.NoError(err) + + u := &User{} + err = b.Bind(req, u) + testBoundedUserIsOK(is, err, u) +} diff --git a/binding/binding_test.go b/binding/binding_test.go index a16b4aa..50a38b6 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -1,4 +1,16 @@ -package binding +package binding_test + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/gookit/goutil/netutil/httpctype" + "github.com/gookit/goutil/testutil" + "github.com/gookit/rux/binding" + "github.com/stretchr/testify/assert" +) var ( userQuery = "age=12&name=inhere" @@ -9,3 +21,80 @@ var ( inhere ` ) + +type User struct { + Age int `query:"age" form:"age" xml:"age"` + Name string `query:"name" form:"name" xml:"name"` +} + +func TestAuto(t *testing.T) { + is := assert.New(t) + r := http.NewServeMux() + + r.HandleFunc("/AutoBind", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + u := &User{} + if ctype := r.Header.Get(httpctype.Key); ctype != "" { + fmt.Printf(" - auto bind data by content type: %s\n", ctype) + } else { + fmt.Println(" - auto bind data from URL query string") + } + + err := binding.Bind(r, u) + testBoundedUserIsOK(is, err, u) + })) + + // post Form body + w := testutil.MockRequest(r, http.MethodPost, "/AutoBind", &testutil.MD{ + Body: strings.NewReader(userQuery), + Headers: testutil.M{ + httpctype.Key: httpctype.MIMEPOSTForm, + }, + }) + is.Equal(http.StatusOK, w.Code) + + // post JSON body + w = testutil.MockRequest(r, http.MethodPost, "/AutoBind", &testutil.MD{ + Body: strings.NewReader(userJSON), + Headers: testutil.M{ + httpctype.Key: httpctype.MIMEJSON, + }, + }) + is.Equal(http.StatusOK, w.Code) + + // post XML body + w = testutil.MockRequest(r, http.MethodPost, "/AutoBind", &testutil.MD{ + Body: strings.NewReader(userXML), + Headers: testutil.M{ + httpctype.Key: httpctype.MIMEXML, + }, + }) + is.Equal(http.StatusOK, w.Code) + + // URL query string + w = testutil.MockRequest(r, http.MethodGet, "/AutoBind?"+userQuery, nil) + is.Equal(http.StatusOK, w.Code) +} + +func TestHeaderBinder_Bind(t *testing.T) { + req, err := http.NewRequest("POST", "/", nil) + is := assert.New(t) + is.NoError(err) + + req.Header.Set("age", "12") + req.Header.Set("name", "inhere") + + u := &User{} + err = binding.Header.Bind(req, u) + testBoundedUserIsOK(is, err, u) + + u = &User{} + err = binding.Header.BindValues(req.Header, u) + testBoundedUserIsOK(is, err, u) +} + +func testBoundedUserIsOK(is *assert.Assertions, err error, u *User) { + is.NoError(err) + is.NotEmpty(u) + is.Equal(12, u.Age) + is.Equal("inhere", u.Name) +} diff --git a/binding/query.go b/binding/query.go index b4ef01d..3bac035 100644 --- a/binding/query.go +++ b/binding/query.go @@ -15,7 +15,7 @@ type QueryBinder struct { // Name get name func (QueryBinder) Name() string { - return "url-query" + return "query" } // Bind Query data binder diff --git a/context_binding.go b/context_binding.go index 8d56a10..a699ffa 100644 --- a/context_binding.go +++ b/context_binding.go @@ -42,11 +42,18 @@ func (c *Context) Bind(obj interface{}) error { * quick context data binding *************************************************************/ +// BindForm request data to an struct, will auto call validator +// +// Usage: +// err := c.BindForm(&user) +func (c *Context) BindForm(obj interface{}) error { + return binding.Form.Bind(c.Req, obj) +} + // BindJSON request data to an struct, will auto call validator // // Usage: // err := c.BindJSON(&user) -// func (c *Context) BindJSON(obj interface{}) error { return binding.JSON.Bind(c.Req, obj) } @@ -55,7 +62,6 @@ func (c *Context) BindJSON(obj interface{}) error { // // Usage: // err := c.BindXML(&user) -// func (c *Context) BindXML(obj interface{}) error { return binding.XML.Bind(c.Req, obj) } diff --git a/context_binding_test.go b/context_binding_test.go index 3a9e3ec..f854397 100644 --- a/context_binding_test.go +++ b/context_binding_test.go @@ -35,9 +35,7 @@ func TestContext_ShouldBind(t *testing.T) { u := &User{} err := c.ShouldBind(u, binding.JSON) - is.NoError(err) - is.Equal(12, u.Age) - is.Equal("inhere", u.Name) + testBoundedUserIsOK(is, err, u) }) r.POST("/ShouldBind-err", func(c *Context) { u := &User{} @@ -69,10 +67,6 @@ func TestContext_MustBind(t *testing.T) { c.MustBind(u, binding.JSON) is.Equal(12, u.Age) is.Equal("inhere", u.Name) - - // fmt.Println(u) - // bs, _ := xml.Marshal(u) - // fmt.Println(string(bs)) }) w := testutil.MockRequest(r, POST, "/MustBind", &testutil.MD{ @@ -101,10 +95,8 @@ func TestContext_Bind(t *testing.T) { u := &User{} fmt.Printf(" - auto bind data by content type: %s\n", c.ContentType()) - err := c.AutoBind(u) - is.NoError(err) - is.Equal(12, u.Age) - is.Equal("inhere", u.Name) + err := c.Bind(u) + testBoundedUserIsOK(is, err, u) }, GET, POST) // post Form body @@ -124,11 +116,14 @@ func TestContext_AutoBind(t *testing.T) { r.Add("/AutoBind", func(c *Context) { u := &User{} - fmt.Printf(" - auto bind data by content type: %s\n", c.ContentType()) + if ctype := c.ContentType(); ctype != "" { + fmt.Printf(" - auto bind data by content type: %s\n", ctype) + } else { + fmt.Println(" - auto bind data from URL query string") + } + err := c.AutoBind(u) - is.NoError(err) - is.Equal(12, u.Age) - is.Equal("inhere", u.Name) + testBoundedUserIsOK(is, err, u) }, GET, POST) // post Form body @@ -157,4 +152,66 @@ func TestContext_AutoBind(t *testing.T) { }, }) is.Equal(http.StatusOK, w.Code) + + // URL query string + w = testutil.MockRequest(r, GET, "/AutoBind?"+userQuery, nil) + is.Equal(http.StatusOK, w.Code) +} + +func TestContext_BindForm(t *testing.T) { + r := New() + is := assert.New(t) + + r.POST("/BindForm", func(c *Context) { + u := &User{} + err := c.BindForm(u) + testBoundedUserIsOK(is, err, u) + }) + + w := testutil.MockRequest(r, POST, "/BindForm", &testutil.MD{ + Body: strings.NewReader(userQuery), + Headers: testutil.M{ + httpctype.Key: httpctype.MIMEPOSTForm, + }, + }) + is.Equal(http.StatusOK, w.Code) +} + +func TestContext_BindJSON(t *testing.T) { + r := New() + is := assert.New(t) + + r.POST("/BindJSON", func(c *Context) { + u := &User{} + err := c.BindJSON(u) + testBoundedUserIsOK(is, err, u) + }) + + w := testutil.MockRequest(r, POST, "/BindJSON", &testutil.MD{ + Body: strings.NewReader(userJSON), + }) + is.Equal(http.StatusOK, w.Code) +} + +func TestContext_BindXML(t *testing.T) { + r := New() + is := assert.New(t) + + r.POST("/BindXML", func(c *Context) { + u := &User{} + err := c.BindXML(u) + testBoundedUserIsOK(is, err, u) + }) + + w := testutil.MockRequest(r, POST, "/BindXML", &testutil.MD{ + Body: strings.NewReader(userXML), + }) + is.Equal(http.StatusOK, w.Code) +} + +func testBoundedUserIsOK(is *assert.Assertions, err error, u *User) { + is.NoError(err) + is.NotEmpty(u) + is.Equal(12, u.Age) + is.Equal("inhere", u.Name) } diff --git a/render/auto.go b/render/auto.go deleted file mode 100644 index 468c365..0000000 --- a/render/auto.go +++ /dev/null @@ -1,90 +0,0 @@ -package render - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - - "github.com/gookit/goutil/netutil/httpctype" -) - -// FallbackType for auto response -var FallbackType = httpctype.MIMEText - -// Auto render data to response -func Auto(w http.ResponseWriter, r *http.Request, obj interface{}) (err error) { - accepts := parseAccept(r.Header.Get("Accept")) - - // fallback use FallbackType - if len(accepts) == 0 { - accepts = []string{FallbackType} - } - - var handled bool - // auto render response by Accept type. - for _, accept := range accepts { - switch accept { - case httpctype.MIMEJSON: - err = JSON(w, obj) - handled = true - break - case httpctype.MIMEHTML: - handled = true - break - case httpctype.MIMEText: - err = responseText(w, obj) - handled = true - break - case httpctype.MIMEXML: - case httpctype.MIMEXML2: - err = XML(w, obj) - handled = true - break - // case httpctype.MIMEYAML: - // break - } - - if handled { - break - } - } - - if !handled { - return errors.New("not supported Accept type") - } - return -} - -func responseText(w http.ResponseWriter, obj interface{}) error { - switch typVal := obj.(type) { - case string: - return Text(w, typVal) - case []byte: - return TextBytes(w, typVal) - default: - jsonBs, err := json.Marshal(obj) - if err != nil { - return err - } - - return TextBytes(w, jsonBs) - } -} - -// from gin framework -func parseAccept(acceptHeader string) []string { - if acceptHeader == "" { - return []string{} - } - - parts := strings.Split(acceptHeader, ",") - outs := make([]string, 0, len(parts)) - - for _, part := range parts { - if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" { - outs = append(outs, part) - } - } - return outs -} diff --git a/render/render.go b/render/render.go index 7d4a2b6..0adae3a 100644 --- a/render/render.go +++ b/render/render.go @@ -1,7 +1,10 @@ package render import ( + "encoding/json" + "errors" "net/http" + "strings" "github.com/gookit/goutil/netutil/httpctype" ) @@ -9,6 +12,9 @@ import ( // PrettyIndent indent string for render JSON or XML var PrettyIndent = " " +// FallbackType for auto response +var FallbackType = httpctype.MIMEText + // Renderer interface type Renderer interface { Render(w http.ResponseWriter, obj interface{}) error @@ -57,6 +63,83 @@ func Blob(w http.ResponseWriter, contentType string, data []byte) (err error) { return } +// Auto render data to response +func Auto(w http.ResponseWriter, r *http.Request, obj interface{}) (err error) { + accepts := parseAccept(r.Header.Get("Accept")) + + // fallback use FallbackType + if len(accepts) == 0 { + accepts = []string{FallbackType} + } + + var handled bool + // auto render response by Accept type. + for _, accept := range accepts { + switch accept { + case httpctype.MIMEJSON: + err = JSON(w, obj) + handled = true + break + case httpctype.MIMEHTML: + handled = true + break + case httpctype.MIMEText: + err = responseText(w, obj) + handled = true + break + case httpctype.MIMEXML: + case httpctype.MIMEXML2: + err = XML(w, obj) + handled = true + break + // case httpctype.MIMEYAML: + // break + } + + if handled { + break + } + } + + if !handled { + return errors.New("not supported Accept type") + } + return +} + +func responseText(w http.ResponseWriter, obj interface{}) error { + switch typVal := obj.(type) { + case string: + return Text(w, typVal) + case []byte: + return TextBytes(w, typVal) + default: + jsonBs, err := json.Marshal(obj) + if err != nil { + return err + } + + return TextBytes(w, jsonBs) + } +} + +// from gin framework +func parseAccept(acceptHeader string) []string { + if acceptHeader == "" { + return []string{} + } + + parts := strings.Split(acceptHeader, ",") + outs := make([]string, 0, len(parts)) + + for _, part := range parts { + if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" { + outs = append(outs, part) + } + } + return outs +} + func writeContentType(w http.ResponseWriter, value string) { header := w.Header() if val := header["Content-Type"]; len(val) == 0 {