Skip to content

Commit cb75214

Browse files
committed
Add form binding
Signed-off-by: Vishal Rana <vr@labstack.com>
1 parent 041252c commit cb75214

File tree

8 files changed

+437
-94
lines changed

8 files changed

+437
-94
lines changed

binder.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package echo
2+
3+
import (
4+
"encoding/json"
5+
"encoding/xml"
6+
"errors"
7+
"net/http"
8+
"reflect"
9+
"strconv"
10+
"strings"
11+
)
12+
13+
type (
14+
// Binder is the interface that wraps the Bind method.
15+
Binder interface {
16+
Bind(interface{}, Context) error
17+
}
18+
19+
binder struct{}
20+
)
21+
22+
func (b *binder) Bind(i interface{}, c Context) (err error) {
23+
req := c.Request()
24+
ctype := req.Header().Get(HeaderContentType)
25+
if req.Body() == nil {
26+
err = NewHTTPError(http.StatusBadRequest, "request body can't be empty")
27+
return
28+
}
29+
err = ErrUnsupportedMediaType
30+
switch {
31+
case strings.HasPrefix(ctype, MIMEApplicationJSON):
32+
if err = json.NewDecoder(req.Body()).Decode(i); err != nil {
33+
err = NewHTTPError(http.StatusBadRequest, err.Error())
34+
}
35+
case strings.HasPrefix(ctype, MIMEApplicationXML):
36+
if err = xml.NewDecoder(req.Body()).Decode(i); err != nil {
37+
err = NewHTTPError(http.StatusBadRequest, err.Error())
38+
}
39+
case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm):
40+
if err = b.bindForm(i, req.FormParams()); err != nil {
41+
err = NewHTTPError(http.StatusBadRequest, err.Error())
42+
}
43+
}
44+
return
45+
}
46+
47+
func (b *binder) bindForm(ptr interface{}, form map[string][]string) error {
48+
typ := reflect.TypeOf(ptr).Elem()
49+
val := reflect.ValueOf(ptr).Elem()
50+
51+
for i := 0; i < typ.NumField(); i++ {
52+
typeField := typ.Field(i)
53+
structField := val.Field(i)
54+
if !structField.CanSet() {
55+
continue
56+
}
57+
structFieldKind := structField.Kind()
58+
inputFieldName := typeField.Tag.Get("form")
59+
60+
if inputFieldName == "" {
61+
inputFieldName = typeField.Name
62+
// If "form" tag is nil, we inspect if the field is a struct.
63+
if structFieldKind == reflect.Struct {
64+
err := b.bindForm(structField.Addr().Interface(), form)
65+
if err != nil {
66+
return err
67+
}
68+
continue
69+
}
70+
}
71+
inputValue, exists := form[inputFieldName]
72+
if !exists {
73+
continue
74+
}
75+
76+
numElems := len(inputValue)
77+
if structFieldKind == reflect.Slice && numElems > 0 {
78+
sliceOf := structField.Type().Elem().Kind()
79+
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
80+
for i := 0; i < numElems; i++ {
81+
if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil {
82+
return err
83+
}
84+
}
85+
val.Field(i).Set(slice)
86+
} else {
87+
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
88+
return err
89+
}
90+
}
91+
}
92+
return nil
93+
}
94+
95+
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
96+
switch valueKind {
97+
case reflect.Int:
98+
return setIntField(val, 0, structField)
99+
case reflect.Int8:
100+
return setIntField(val, 8, structField)
101+
case reflect.Int16:
102+
return setIntField(val, 16, structField)
103+
case reflect.Int32:
104+
return setIntField(val, 32, structField)
105+
case reflect.Int64:
106+
return setIntField(val, 64, structField)
107+
case reflect.Uint:
108+
return setUintField(val, 0, structField)
109+
case reflect.Uint8:
110+
return setUintField(val, 8, structField)
111+
case reflect.Uint16:
112+
return setUintField(val, 16, structField)
113+
case reflect.Uint32:
114+
return setUintField(val, 32, structField)
115+
case reflect.Uint64:
116+
return setUintField(val, 64, structField)
117+
case reflect.Bool:
118+
return setBoolField(val, structField)
119+
case reflect.Float32:
120+
return setFloatField(val, 32, structField)
121+
case reflect.Float64:
122+
return setFloatField(val, 64, structField)
123+
case reflect.String:
124+
structField.SetString(val)
125+
default:
126+
return errors.New("unknown type")
127+
}
128+
return nil
129+
}
130+
131+
func setIntField(value string, bitSize int, field reflect.Value) error {
132+
if value == "" {
133+
value = "0"
134+
}
135+
intVal, err := strconv.ParseInt(value, 10, bitSize)
136+
if err == nil {
137+
field.SetInt(intVal)
138+
}
139+
return err
140+
}
141+
142+
func setUintField(value string, bitSize int, field reflect.Value) error {
143+
if value == "" {
144+
value = "0"
145+
}
146+
uintVal, err := strconv.ParseUint(value, 10, bitSize)
147+
if err == nil {
148+
field.SetUint(uintVal)
149+
}
150+
return err
151+
}
152+
153+
func setBoolField(value string, field reflect.Value) error {
154+
if value == "" {
155+
value = "false"
156+
}
157+
boolVal, err := strconv.ParseBool(value)
158+
if err == nil {
159+
field.SetBool(boolVal)
160+
}
161+
return err
162+
}
163+
164+
func setFloatField(value string, bitSize int, field reflect.Value) error {
165+
if value == "" {
166+
value = "0.0"
167+
}
168+
floatVal, err := strconv.ParseFloat(value, bitSize)
169+
if err == nil {
170+
field.SetFloat(floatVal)
171+
}
172+
return err
173+
}

binder_test.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package echo
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"mime/multipart"
7+
"net/http"
8+
"reflect"
9+
"strings"
10+
"testing"
11+
12+
"github.com/labstack/echo/test"
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
type (
17+
binderTestStruct struct {
18+
I int
19+
I8 int8
20+
I16 int16
21+
I32 int32
22+
I64 int64
23+
UI uint
24+
UI8 uint8
25+
UI16 uint16
26+
UI32 uint32
27+
UI64 uint64
28+
B bool
29+
F32 float32
30+
F64 float64
31+
S string
32+
cantSet string
33+
DoesntExist string
34+
}
35+
)
36+
37+
func (t binderTestStruct) GetCantSet() string {
38+
return t.cantSet
39+
}
40+
41+
var values = map[string][]string{
42+
"I": {"0"},
43+
"I8": {"8"},
44+
"I16": {"16"},
45+
"I32": {"32"},
46+
"I64": {"64"},
47+
"UI": {"0"},
48+
"UI8": {"8"},
49+
"UI16": {"16"},
50+
"UI32": {"32"},
51+
"UI64": {"64"},
52+
"B": {"true"},
53+
"F32": {"32.5"},
54+
"F64": {"64.5"},
55+
"S": {"test"},
56+
"cantSet": {"test"},
57+
}
58+
59+
func TestBinderJSON(t *testing.T) {
60+
testBinderOkay(t, strings.NewReader(userJSON), MIMEApplicationJSON)
61+
testBinderError(t, strings.NewReader(invalidContent), MIMEApplicationJSON)
62+
}
63+
64+
func TestBinderXML(t *testing.T) {
65+
testBinderOkay(t, strings.NewReader(userXML), MIMEApplicationXML)
66+
testBinderError(t, strings.NewReader(invalidContent), MIMEApplicationXML)
67+
}
68+
69+
func TestBinderForm(t *testing.T) {
70+
testBinderOkay(t, strings.NewReader(userForm), MIMEApplicationForm)
71+
}
72+
73+
func TestBinderMultipartForm(t *testing.T) {
74+
body := new(bytes.Buffer)
75+
mw := multipart.NewWriter(body)
76+
mw.WriteField("id", "1")
77+
mw.WriteField("name", "Jon Snow")
78+
mw.Close()
79+
testBinderOkay(t, body, mw.FormDataContentType())
80+
}
81+
82+
func TestBinderUnsupportedMediaType(t *testing.T) {
83+
testBinderError(t, strings.NewReader(invalidContent), MIMEApplicationJSON)
84+
}
85+
86+
// func assertCustomer(t *testing.T, c *user) {
87+
// assert.Equal(t, 1, c.ID)
88+
// assert.Equal(t, "Joe", c.Name)
89+
// }
90+
91+
func TestBinderbindForm(t *testing.T) {
92+
ts := new(binderTestStruct)
93+
b := new(binder)
94+
b.bindForm(ts, values)
95+
assertBinderTestStruct(t, ts)
96+
}
97+
98+
func TestBinderSetWithProperType(t *testing.T) {
99+
ts := new(binderTestStruct)
100+
typ := reflect.TypeOf(ts).Elem()
101+
val := reflect.ValueOf(ts).Elem()
102+
for i := 0; i < typ.NumField(); i++ {
103+
typeField := typ.Field(i)
104+
structField := val.Field(i)
105+
if !structField.CanSet() {
106+
continue
107+
}
108+
if len(values[typeField.Name]) == 0 {
109+
continue
110+
}
111+
val := values[typeField.Name][0]
112+
err := setWithProperType(typeField.Type.Kind(), val, structField)
113+
assert.NoError(t, err)
114+
}
115+
assertBinderTestStruct(t, ts)
116+
117+
type foo struct {
118+
Bar bytes.Buffer
119+
}
120+
v := &foo{}
121+
typ = reflect.TypeOf(v).Elem()
122+
val = reflect.ValueOf(v).Elem()
123+
assert.Error(t, setWithProperType(typ.Field(0).Type.Kind(), "5", val.Field(0)))
124+
}
125+
126+
func TestBinderSetFields(t *testing.T) {
127+
ts := new(binderTestStruct)
128+
val := reflect.ValueOf(ts).Elem()
129+
// Int
130+
if assert.NoError(t, setIntField("5", 0, val.FieldByName("I"))) {
131+
assert.Equal(t, 5, ts.I)
132+
}
133+
if assert.NoError(t, setIntField("", 0, val.FieldByName("I"))) {
134+
assert.Equal(t, 0, ts.I)
135+
}
136+
137+
// Uint
138+
if assert.NoError(t, setUintField("10", 0, val.FieldByName("UI"))) {
139+
assert.Equal(t, uint(10), ts.UI)
140+
}
141+
if assert.NoError(t, setUintField("", 0, val.FieldByName("UI"))) {
142+
assert.Equal(t, uint(0), ts.UI)
143+
}
144+
145+
// Float
146+
if assert.NoError(t, setFloatField("15.5", 0, val.FieldByName("F32"))) {
147+
assert.Equal(t, float32(15.5), ts.F32)
148+
}
149+
if assert.NoError(t, setFloatField("", 0, val.FieldByName("F32"))) {
150+
assert.Equal(t, float32(0.0), ts.F32)
151+
}
152+
153+
// Bool
154+
if assert.NoError(t, setBoolField("true", val.FieldByName("B"))) {
155+
assert.Equal(t, true, ts.B)
156+
}
157+
if assert.NoError(t, setBoolField("", val.FieldByName("B"))) {
158+
assert.Equal(t, false, ts.B)
159+
}
160+
}
161+
162+
func assertBinderTestStruct(t *testing.T, ts *binderTestStruct) {
163+
assert.Equal(t, 0, ts.I)
164+
assert.Equal(t, int8(8), ts.I8)
165+
assert.Equal(t, int16(16), ts.I16)
166+
assert.Equal(t, int32(32), ts.I32)
167+
assert.Equal(t, int64(64), ts.I64)
168+
assert.Equal(t, uint(0), ts.UI)
169+
assert.Equal(t, uint8(8), ts.UI8)
170+
assert.Equal(t, uint16(16), ts.UI16)
171+
assert.Equal(t, uint32(32), ts.UI32)
172+
assert.Equal(t, uint64(64), ts.UI64)
173+
assert.Equal(t, true, ts.B)
174+
assert.Equal(t, float32(32.5), ts.F32)
175+
assert.Equal(t, float64(64.5), ts.F64)
176+
assert.Equal(t, "test", ts.S)
177+
assert.Equal(t, "", ts.GetCantSet())
178+
}
179+
180+
func testBinderOkay(t *testing.T, r io.Reader, ctype string) {
181+
e := New()
182+
req := test.NewRequest(POST, "/", r)
183+
rec := test.NewResponseRecorder()
184+
c := e.NewContext(req, rec)
185+
req.Header().Set(HeaderContentType, ctype)
186+
u := new(user)
187+
err := c.Bind(u)
188+
if assert.NoError(t, err) {
189+
assert.Equal(t, 1, u.ID)
190+
assert.Equal(t, "Jon Snow", u.Name)
191+
}
192+
}
193+
194+
func testBinderError(t *testing.T, r io.Reader, ctype string) {
195+
e := New()
196+
req := test.NewRequest(POST, "/", r)
197+
rec := test.NewResponseRecorder()
198+
c := e.NewContext(req, rec)
199+
req.Header().Set(HeaderContentType, ctype)
200+
u := new(user)
201+
err := c.Bind(u)
202+
203+
switch {
204+
case strings.HasPrefix(ctype, MIMEApplicationJSON), strings.HasPrefix(ctype, MIMEApplicationXML),
205+
strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm):
206+
if assert.IsType(t, new(HTTPError), err) {
207+
assert.Equal(t, http.StatusBadRequest, err.(*HTTPError).Code)
208+
}
209+
default:
210+
if assert.IsType(t, new(HTTPError), err) {
211+
assert.Equal(t, ErrUnsupportedMediaType, err)
212+
}
213+
}
214+
}

0 commit comments

Comments
 (0)