Skip to content

Commit

Permalink
Refactor work done by martinpasaribu <[email protected]> (…
Browse files Browse the repository at this point in the history
…binding multipart files by using struct tags)
  • Loading branch information
aldas committed Oct 20, 2024
1 parent 2d7acbf commit 42dd676
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 194 deletions.
117 changes: 72 additions & 45 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ func (b *DefaultBinder) BindPathParams(c Context, i interface{}) error {
for i, name := range names {
params[name] = []string{values[i]}
}
if err := b.bindData(i, params, "param"); err != nil {
if err := b.bindData(i, params, "param", nil); err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
return nil
}

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

ctype := req.Header.Get(HeaderContentType)
switch {
case strings.HasPrefix(ctype, MIMEApplicationJSON):
// mediatype is found like `mime.ParseMediaType()` does it
base, _, _ := strings.Cut(req.Header.Get(HeaderContentType), ";")
mediatype := strings.TrimSpace(base)

switch mediatype {
case MIMEApplicationJSON:
if err = c.Echo().JSONSerializer.Deserialize(c, i); err != nil {
switch err.(type) {
case *HTTPError:
Expand All @@ -82,7 +85,7 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
}
case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML):
case MIMEApplicationXML, MIMETextXML:
if err = xml.NewDecoder(req.Body).Decode(i); err != nil {
if ute, ok := err.(*xml.UnsupportedTypeError); ok {
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error())).SetInternal(err)
Expand All @@ -91,15 +94,15 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
}
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
case strings.HasPrefix(ctype, MIMEApplicationForm):
case MIMEApplicationForm:
params, err := c.FormParams()
if err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
if err = b.bindData(i, params, "form"); err != nil {
if err = b.bindData(i, params, "form", nil); err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
case strings.HasPrefix(ctype, MIMEMultipartForm):
case MIMEMultipartForm:
params, err := c.MultipartForm()
if err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
Expand All @@ -115,7 +118,7 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {

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

// bindData will bind data ONLY fields in destination struct that have EXPLICIT tag
func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string, files ...map[string][]*multipart.FileHeader) error {
if destination == nil || (len(data) == 0 && len(files) == 0) {
func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string, dataFiles map[string][]*multipart.FileHeader) error {
if destination == nil || (len(data) == 0 && len(dataFiles) == 0) {
return nil
}
hasFiles := len(dataFiles) > 0
typ := reflect.TypeOf(destination).Elem()
val := reflect.ValueOf(destination).Elem()

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

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

if inputFieldName == "" {
// 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).
// 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).
// structs that implement BindUnmarshaler are bound only when they have explicit tag
if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct {
if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
if err := b.bindData(structField.Addr().Interface(), data, tag, dataFiles); err != nil {
return err
}
}
// does not have explicit tag and is not an ordinary struct - so move to next field
continue
}

// Handle multiple file uploads ([]*multipart.FileHeader, *multipart.FileHeader, []multipart.FileHeader)
if len(files) > 0 && isMultipartFile(structField.Type()) {
for _, fileMap := range files {
fileHeaders, exists := fileMap[inputFieldName]
if exists && len(fileHeaders) > 0 {
switch structField.Type() {
case reflect.TypeOf([]*multipart.FileHeader(nil)):
structField.Set(reflect.ValueOf(fileHeaders))
continue
case reflect.TypeOf([]multipart.FileHeader(nil)):
headers := make([]multipart.FileHeader, len(fileHeaders))
for i, fileHeader := range fileHeaders {
headers[i] = *fileHeader
}
structField.Set(reflect.ValueOf(headers))
continue
case reflect.TypeOf(&multipart.FileHeader{}):
structField.Set(reflect.ValueOf(fileHeaders[0]))
continue
case reflect.TypeOf(multipart.FileHeader{}):
structField.Set(reflect.ValueOf(*fileHeaders[0]))
continue
}
if hasFiles {
if ok, err := isFieldMultipartFile(structField.Type()); err != nil {
return err
} else if ok {
if ok := setMultipartFileHeaderTypes(structField, inputFieldName, dataFiles); ok {
continue
}
}
}

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

func isMultipartFile(field reflect.Type) bool {
return reflect.TypeOf(&multipart.FileHeader{}) == field ||
reflect.TypeOf(multipart.FileHeader{}) == field ||
reflect.TypeOf([]*multipart.FileHeader(nil)) == field ||
reflect.TypeOf([]multipart.FileHeader(nil)) == field
var (
// NOT supported by bind as you can NOT check easily empty struct being actual file or not
multipartFileHeaderType = reflect.TypeOf(multipart.FileHeader{})
// supported by bind as you can check by nil value if file existed or not
multipartFileHeaderPointerType = reflect.TypeOf(&multipart.FileHeader{})
multipartFileHeaderSliceType = reflect.TypeOf([]multipart.FileHeader(nil))
multipartFileHeaderPointerSliceType = reflect.TypeOf([]*multipart.FileHeader(nil))
)

func isFieldMultipartFile(field reflect.Type) (bool, error) {
switch field {
case multipartFileHeaderPointerType,
multipartFileHeaderSliceType,
multipartFileHeaderPointerSliceType:
return true, nil
case multipartFileHeaderType:
return true, errors.New("binding to multipart.FileHeader struct is not supported, use pointer to struct")
default:
return false, nil
}
}

func setMultipartFileHeaderTypes(structField reflect.Value, inputFieldName string, files map[string][]*multipart.FileHeader) bool {
fileHeaders := files[inputFieldName]
if len(fileHeaders) == 0 {
return false
}

result := true
switch structField.Type() {
case multipartFileHeaderPointerSliceType:
structField.Set(reflect.ValueOf(fileHeaders))
case multipartFileHeaderSliceType:
headers := make([]multipart.FileHeader, len(fileHeaders))
for i, fileHeader := range fileHeaders {
headers[i] = *fileHeader
}
structField.Set(reflect.ValueOf(headers))
case multipartFileHeaderPointerType:
structField.Set(reflect.ValueOf(fileHeaders[0]))
default:
result = false
}

return result
}
Loading

0 comments on commit 42dd676

Please sign in to comment.