Skip to content

Commit d5b32c6

Browse files
committed
Refactor work done by martinpasaribu <[email protected]> (binding multipart files by using struct tags)
1 parent fb769d7 commit d5b32c6

File tree

2 files changed

+200
-194
lines changed

2 files changed

+200
-194
lines changed

bind.go

+72-45
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ func (b *DefaultBinder) BindPathParams(c Context, i interface{}) error {
4646
for i, name := range names {
4747
params[name] = []string{values[i]}
4848
}
49-
if err := b.bindData(i, params, "param"); err != nil {
49+
if err := b.bindData(i, params, "param", nil); err != nil {
5050
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
5151
}
5252
return nil
5353
}
5454

5555
// BindQueryParams binds query params to bindable object
5656
func (b *DefaultBinder) BindQueryParams(c Context, i interface{}) error {
57-
if err := b.bindData(i, c.QueryParams(), "query"); err != nil {
57+
if err := b.bindData(i, c.QueryParams(), "query", nil); err != nil {
5858
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
5959
}
6060
return nil
@@ -71,9 +71,12 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
7171
return
7272
}
7373

74-
ctype := req.Header.Get(HeaderContentType)
75-
switch {
76-
case strings.HasPrefix(ctype, MIMEApplicationJSON):
74+
// mediatype is found like `mime.ParseMediaType()` does it
75+
base, _, _ := strings.Cut(req.Header.Get(HeaderContentType), ";")
76+
mediatype := strings.TrimSpace(base)
77+
78+
switch mediatype {
79+
case MIMEApplicationJSON:
7780
if err = c.Echo().JSONSerializer.Deserialize(c, i); err != nil {
7881
switch err.(type) {
7982
case *HTTPError:
@@ -82,7 +85,7 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
8285
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
8386
}
8487
}
85-
case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML):
88+
case MIMEApplicationXML, MIMETextXML:
8689
if err = xml.NewDecoder(req.Body).Decode(i); err != nil {
8790
if ute, ok := err.(*xml.UnsupportedTypeError); ok {
8891
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error())).SetInternal(err)
@@ -91,15 +94,15 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
9194
}
9295
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
9396
}
94-
case strings.HasPrefix(ctype, MIMEApplicationForm):
97+
case MIMEApplicationForm:
9598
params, err := c.FormParams()
9699
if err != nil {
97100
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
98101
}
99-
if err = b.bindData(i, params, "form"); err != nil {
102+
if err = b.bindData(i, params, "form", nil); err != nil {
100103
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
101104
}
102-
case strings.HasPrefix(ctype, MIMEMultipartForm):
105+
case MIMEMultipartForm:
103106
params, err := c.MultipartForm()
104107
if err != nil {
105108
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
@@ -115,7 +118,7 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
115118

116119
// BindHeaders binds HTTP headers to a bindable object
117120
func (b *DefaultBinder) BindHeaders(c Context, i interface{}) error {
118-
if err := b.bindData(i, c.Request().Header, "header"); err != nil {
121+
if err := b.bindData(i, c.Request().Header, "header", nil); err != nil {
119122
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
120123
}
121124
return nil
@@ -141,10 +144,11 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
141144
}
142145

143146
// bindData will bind data ONLY fields in destination struct that have EXPLICIT tag
144-
func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string, files ...map[string][]*multipart.FileHeader) error {
145-
if destination == nil || (len(data) == 0 && len(files) == 0) {
147+
func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string, dataFiles map[string][]*multipart.FileHeader) error {
148+
if destination == nil || (len(data) == 0 && len(dataFiles) == 0) {
146149
return nil
147150
}
151+
hasFiles := len(dataFiles) > 0
148152
typ := reflect.TypeOf(destination).Elem()
149153
val := reflect.ValueOf(destination).Elem()
150154

@@ -188,7 +192,7 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
188192
return errors.New("binding element must be a struct")
189193
}
190194

191-
for i := 0; i < typ.NumField(); i++ {
195+
for i := 0; i < typ.NumField(); i++ { // iterate over all destination fields
192196
typeField := typ.Field(i)
193197
structField := val.Field(i)
194198
if typeField.Anonymous {
@@ -207,48 +211,31 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
207211
}
208212

209213
if inputFieldName == "" {
210-
// If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contains fields with tags).
214+
// If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contain fields with tags).
211215
// structs that implement BindUnmarshaler are bound only when they have explicit tag
212216
if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct {
213-
if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
217+
if err := b.bindData(structField.Addr().Interface(), data, tag, dataFiles); err != nil {
214218
return err
215219
}
216220
}
217221
// does not have explicit tag and is not an ordinary struct - so move to next field
218222
continue
219223
}
220224

221-
// Handle multiple file uploads ([]*multipart.FileHeader, *multipart.FileHeader, []multipart.FileHeader)
222-
if len(files) > 0 && isMultipartFile(structField.Type()) {
223-
for _, fileMap := range files {
224-
fileHeaders, exists := fileMap[inputFieldName]
225-
if exists && len(fileHeaders) > 0 {
226-
switch structField.Type() {
227-
case reflect.TypeOf([]*multipart.FileHeader(nil)):
228-
structField.Set(reflect.ValueOf(fileHeaders))
229-
continue
230-
case reflect.TypeOf([]multipart.FileHeader(nil)):
231-
headers := make([]multipart.FileHeader, len(fileHeaders))
232-
for i, fileHeader := range fileHeaders {
233-
headers[i] = *fileHeader
234-
}
235-
structField.Set(reflect.ValueOf(headers))
236-
continue
237-
case reflect.TypeOf(&multipart.FileHeader{}):
238-
structField.Set(reflect.ValueOf(fileHeaders[0]))
239-
continue
240-
case reflect.TypeOf(multipart.FileHeader{}):
241-
structField.Set(reflect.ValueOf(*fileHeaders[0]))
242-
continue
243-
}
225+
if hasFiles {
226+
if ok, err := isFieldMultipartFile(structField.Type()); err != nil {
227+
return err
228+
} else if ok {
229+
if ok := setMultipartFileHeaderTypes(structField, inputFieldName, dataFiles); ok {
230+
continue
244231
}
245232
}
246233
}
247234

248235
inputValue, exists := data[inputFieldName]
249236
if !exists {
250-
// Go json.Unmarshal supports case insensitive binding. However the
251-
// url params are bound case sensitive which is inconsistent. To
237+
// Go json.Unmarshal supports case-insensitive binding. However the
238+
// url params are bound case-sensitive which is inconsistent. To
252239
// fix this we must check all of the map values in a
253240
// case-insensitive search.
254241
for k, v := range data {
@@ -431,9 +418,49 @@ func setFloatField(value string, bitSize int, field reflect.Value) error {
431418
return err
432419
}
433420

434-
func isMultipartFile(field reflect.Type) bool {
435-
return reflect.TypeOf(&multipart.FileHeader{}) == field ||
436-
reflect.TypeOf(multipart.FileHeader{}) == field ||
437-
reflect.TypeOf([]*multipart.FileHeader(nil)) == field ||
438-
reflect.TypeOf([]multipart.FileHeader(nil)) == field
421+
var (
422+
// NOT supported by bind as you can NOT check easily empty struct being actual file or not
423+
multipartFileHeaderType = reflect.TypeOf(multipart.FileHeader{})
424+
// supported by bind as you can check by nil value if file existed or not
425+
multipartFileHeaderPointerType = reflect.TypeOf(&multipart.FileHeader{})
426+
multipartFileHeaderSliceType = reflect.TypeOf([]multipart.FileHeader(nil))
427+
multipartFileHeaderPointerSliceType = reflect.TypeOf([]*multipart.FileHeader(nil))
428+
)
429+
430+
func isFieldMultipartFile(field reflect.Type) (bool, error) {
431+
switch field {
432+
case multipartFileHeaderPointerType,
433+
multipartFileHeaderSliceType,
434+
multipartFileHeaderPointerSliceType:
435+
return true, nil
436+
case multipartFileHeaderType:
437+
return true, errors.New("binding to multipart.FileHeader struct is not supported, use pointer to struct")
438+
default:
439+
return false, nil
440+
}
441+
}
442+
443+
func setMultipartFileHeaderTypes(structField reflect.Value, inputFieldName string, files map[string][]*multipart.FileHeader) bool {
444+
fileHeaders := files[inputFieldName]
445+
if len(fileHeaders) == 0 {
446+
return false
447+
}
448+
449+
result := true
450+
switch structField.Type() {
451+
case multipartFileHeaderPointerSliceType:
452+
structField.Set(reflect.ValueOf(fileHeaders))
453+
case multipartFileHeaderSliceType:
454+
headers := make([]multipart.FileHeader, len(fileHeaders))
455+
for i, fileHeader := range fileHeaders {
456+
headers[i] = *fileHeader
457+
}
458+
structField.Set(reflect.ValueOf(headers))
459+
case multipartFileHeaderPointerType:
460+
structField.Set(reflect.ValueOf(fileHeaders[0]))
461+
default:
462+
result = false
463+
}
464+
465+
return result
439466
}

0 commit comments

Comments
 (0)