diff --git a/bind.go b/bind.go index 16c3b7adf..08d398916 100644 --- a/bind.go +++ b/bind.go @@ -134,6 +134,10 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri // !struct if typ.Kind() != reflect.Struct { + if tag == "param" || tag == "query" { + // incompatible type, data is probably to be found in the body + return nil + } return errors.New("binding element must be a struct") } diff --git a/bind_test.go b/bind_test.go index e8868b35b..73398034e 100644 --- a/bind_test.go +++ b/bind_test.go @@ -9,6 +9,7 @@ import ( "mime/multipart" "net/http" "net/http/httptest" + "net/url" "reflect" "strconv" "strings" @@ -187,7 +188,10 @@ func TestToMultipleFields(t *testing.T) { func TestBindJSON(t *testing.T) { assert := assert.New(t) - testBindOkay(assert, strings.NewReader(userJSON), MIMEApplicationJSON) + testBindOkay(assert, strings.NewReader(userJSON), nil, MIMEApplicationJSON) + testBindOkay(assert, strings.NewReader(userJSON), dummyQuery, MIMEApplicationJSON) + testBindArrayOkay(assert, strings.NewReader(usersJSON), nil, MIMEApplicationJSON) + testBindArrayOkay(assert, strings.NewReader(usersJSON), dummyQuery, MIMEApplicationJSON) testBindError(assert, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{}) testBindError(assert, strings.NewReader(userJSONInvalidType), MIMEApplicationJSON, &json.UnmarshalTypeError{}) } @@ -195,11 +199,15 @@ func TestBindJSON(t *testing.T) { func TestBindXML(t *testing.T) { assert := assert.New(t) - testBindOkay(assert, strings.NewReader(userXML), MIMEApplicationXML) + testBindOkay(assert, strings.NewReader(userXML), nil, MIMEApplicationXML) + testBindOkay(assert, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML) + testBindArrayOkay(assert, strings.NewReader(userXML), nil, MIMEApplicationXML) + testBindArrayOkay(assert, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML) testBindError(assert, strings.NewReader(invalidContent), MIMEApplicationXML, errors.New("")) testBindError(assert, strings.NewReader(userXMLConvertNumberError), MIMEApplicationXML, &strconv.NumError{}) testBindError(assert, strings.NewReader(userXMLUnsupportedTypeError), MIMEApplicationXML, &xml.SyntaxError{}) - testBindOkay(assert, strings.NewReader(userXML), MIMETextXML) + testBindOkay(assert, strings.NewReader(userXML), nil, MIMETextXML) + testBindOkay(assert, strings.NewReader(userXML), dummyQuery, MIMETextXML) testBindError(assert, strings.NewReader(invalidContent), MIMETextXML, errors.New("")) testBindError(assert, strings.NewReader(userXMLConvertNumberError), MIMETextXML, &strconv.NumError{}) testBindError(assert, strings.NewReader(userXMLUnsupportedTypeError), MIMETextXML, &xml.SyntaxError{}) @@ -208,7 +216,8 @@ func TestBindXML(t *testing.T) { func TestBindForm(t *testing.T) { assert := assert.New(t) - testBindOkay(assert, strings.NewReader(userForm), MIMEApplicationForm) + testBindOkay(assert, strings.NewReader(userForm), nil, MIMEApplicationForm) + testBindOkay(assert, strings.NewReader(userForm), dummyQuery, MIMEApplicationForm) e := New() req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userForm)) rec := httptest.NewRecorder() @@ -336,14 +345,16 @@ func TestBindUnmarshalTextPtr(t *testing.T) { } func TestBindMultipartForm(t *testing.T) { - body := new(bytes.Buffer) - mw := multipart.NewWriter(body) + bodyBuffer := new(bytes.Buffer) + mw := multipart.NewWriter(bodyBuffer) mw.WriteField("id", "1") mw.WriteField("name", "Jon Snow") mw.Close() + body := bodyBuffer.Bytes() assert := assert.New(t) - testBindOkay(assert, body, mw.FormDataContentType()) + testBindOkay(assert, bytes.NewReader(body), nil, mw.FormDataContentType()) + testBindOkay(assert, bytes.NewReader(body), dummyQuery, mw.FormDataContentType()) } func TestBindUnsupportedMediaType(t *testing.T) { @@ -547,9 +558,13 @@ func assertBindTestStruct(a *assert.Assertions, ts *bindTestStruct) { a.Equal("", ts.GetCantSet()) } -func testBindOkay(assert *assert.Assertions, r io.Reader, ctype string) { +func testBindOkay(assert *assert.Assertions, r io.Reader, query url.Values, ctype string) { e := New() - req := httptest.NewRequest(http.MethodPost, "/", r) + path := "/" + if len(query) > 0 { + path += "?" + query.Encode() + } + req := httptest.NewRequest(http.MethodPost, path, r) rec := httptest.NewRecorder() c := e.NewContext(req, rec) req.Header.Set(HeaderContentType, ctype) @@ -561,6 +576,25 @@ func testBindOkay(assert *assert.Assertions, r io.Reader, ctype string) { } } +func testBindArrayOkay(assert *assert.Assertions, r io.Reader, query url.Values, ctype string) { + e := New() + path := "/" + if len(query) > 0 { + path += "?" + query.Encode() + } + req := httptest.NewRequest(http.MethodPost, path, r) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + req.Header.Set(HeaderContentType, ctype) + u := []user{} + err := c.Bind(&u) + if assert.NoError(err) { + assert.Equal(1, len(u)) + assert.Equal(1, u[0].ID) + assert.Equal("Jon Snow", u[0].Name) + } +} + func testBindError(assert *assert.Assertions, r io.Reader, ctype string, expectedInternal error) { e := New() req := httptest.NewRequest(http.MethodPost, "/", r) @@ -679,15 +713,16 @@ func TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) { expect: &Opts{ID: 0, Node: "xxx"}, // query binding has already modified bind target expectError: "code=400, message=Unmarshal type error: expected=echo.Opts, got=array, field=, offset=1, internal=json: cannot unmarshal array into Go value of type echo.Opts", }, - { // binding query params interferes with body. b.BindBody() should be used to bind only body to slice - name: "nok, GET query params bind failure - trying to bind json array to slice", + { // query param is ignored as we do not know where exactly to bind it in slice + name: "ok, GET bind to struct slice, ignore query param", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`[{"id": 1}]`), whenNoPathParams: true, whenBindTarget: &[]Opts{}, - expect: &[]Opts{}, - expectError: "code=400, message=binding element must be a struct, internal=binding element must be a struct", + expect: &[]Opts{ + {ID: 1, Node: ""}, + }, }, { // binding query params interferes with body. b.BindBody() should be used to bind only body to slice name: "ok, POST binding to slice should not be affected query params types", @@ -699,14 +734,15 @@ func TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) { expect: &[]Opts{{ID: 1}}, expectError: "", }, - { // binding path params interferes with body. b.BindBody() should be used to bind only body to slice - name: "nok, GET path params bind failure - trying to bind json array to slice", + { // path param is ignored as we do not know where exactly to bind it in slice + name: "ok, GET bind to struct slice, ignore path param", givenMethod: http.MethodGet, givenURL: "/api/real_node/endpoint?node=xxx", givenContent: strings.NewReader(`[{"id": 1}]`), whenBindTarget: &[]Opts{}, - expect: &[]Opts{}, - expectError: "code=400, message=binding element must be a struct, internal=binding element must be a struct", + expect: &[]Opts{ + {ID: 1, Node: ""}, + }, }, { name: "ok, GET body bind json array to slice", diff --git a/echo_test.go b/echo_test.go index 35c79cbc0..58ecea741 100644 --- a/echo_test.go +++ b/echo_test.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "net/http/httptest" + "net/url" "os" "reflect" "strings" @@ -30,6 +31,7 @@ type ( const ( userJSON = `{"id":1,"name":"Jon Snow"}` + usersJSON = `[{"id":1,"name":"Jon Snow"}]` userXML = `1Jon Snow` userForm = `id=1&name=Jon Snow` invalidContent = "invalid content" @@ -48,6 +50,8 @@ const userXMLPretty = ` Jon Snow ` +var dummyQuery = url.Values{"dummy": []string{"useless"}} + func TestEcho(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil)