Skip to content

Commit

Permalink
Add support to bind to struct, interface, array, slices etc for multi…
Browse files Browse the repository at this point in the history
…part form types (#986)
  • Loading branch information
Umang01-hash authored Sep 17, 2024
1 parent 1720782 commit d8dd55a
Show file tree
Hide file tree
Showing 7 changed files with 475 additions and 11 deletions.
18 changes: 18 additions & 0 deletions docs/references/context/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ parts of the request.
ctx.Bind(&p)
// the Bind() method will map the incoming request to variable p
```

### Binding multipart-form data
- To bind multipart-form data, you can use the Bind method similarly. The struct fields should be tagged appropriately
to map the form fields to the struct fields.

```go
type Data struct {
Name string `form:"name"`

Compressed file.Zip `file:"upload"`

FileHeader *multipart.FileHeader `file:"file_upload"`
}
```

- The `form` tag is used to bind non-file fields.
- The `file` tag is used to bind file fields. If the tag is not present, the field name is used as the key.


- `HostName()` - to access the host name for the incoming request
```go
Expand Down
2 changes: 1 addition & 1 deletion examples/using-file-bind/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ it to the fields of the struct. GoFr currently supports zip file type and also b
type Data struct {
Compressed file.Zip `file:"upload"`

FileHeader *multipart.FileHeader `file:"a"`
FileHeader *multipart.FileHeader `file:"file_upload"`
}

func Handler (c *gofr.Context) (interface{}, error) {
Expand Down
2 changes: 1 addition & 1 deletion examples/using-file-bind/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Data struct {

// The FileHeader determines the generic file format that we can get
// from the multipart form that gets parsed by the incoming HTTP request
FileHeader *multipart.FileHeader `file:"a"`
FileHeader *multipart.FileHeader `file:"file_upload"`
}

func UploadHandler(c *gofr.Context) (interface{}, error) {
Expand Down
2 changes: 1 addition & 1 deletion examples/using-file-bind/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func generateMultiPartBody(t *testing.T) (*bytes.Buffer, string) {
t.Fatalf("Failed to write file to form: %v", err)
}

fileHeader, err := writer.CreateFormFile("a", "hello.txt")
fileHeader, err := writer.CreateFormFile("file_upload", "hello.txt")
if err != nil {
t.Fatalf("Failed to create form file: %v", err)
}
Expand Down
205 changes: 205 additions & 0 deletions pkg/gofr/http/form_data_binder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package http

import (
"encoding/json"
"fmt"
"reflect"
"strings"
)

func (*formData) setInterfaceValue(value reflect.Value, data any) (bool, error) {
if !value.CanSet() {
return false, fmt.Errorf("%w: %s", errUnsupportedInterfaceType, value.Kind())
}

value.Set(reflect.ValueOf(data))

return true, nil
}

func (uf *formData) setSliceOrArrayValue(value reflect.Value, data string) (bool, error) {
if value.Kind() != reflect.Slice && value.Kind() != reflect.Array {
return false, fmt.Errorf("%w: %s", errUnsupportedKind, value.Kind())
}

elemType := value.Type().Elem()

elements := strings.Split(data, ",")

// Create a new slice/array with appropriate length and capacity
var newSlice reflect.Value

if value.Kind() == reflect.Slice {
newSlice = reflect.MakeSlice(value.Type(), len(elements), len(elements))
} else if len(elements) > value.Len() {
return false, errDataLengthExceeded
} else {
newSlice = reflect.New(value.Type()).Elem()
}

// Create a reusable element value to avoid unnecessary allocations
elemValue := reflect.New(elemType).Elem()

// Set the elements of the slice/array
for i, strVal := range elements {
// Update the reusable element value
if _, err := uf.setFieldValue(elemValue, strVal); err != nil {
return false, fmt.Errorf("%w %d: %w", errSettingValueFailure, i, err)
}

newSlice.Index(i).Set(elemValue)
}

value.Set(newSlice)

return true, nil
}

func (*formData) setStructValue(value reflect.Value, data string) (bool, error) {
if value.Kind() != reflect.Struct {
return false, errNotAStruct
}

dataMap, err := parseStringToMap(data)
if err != nil {
return false, err
}

if len(dataMap) == 0 {
return false, errFieldsNotSet
}

numFieldsSet := 0

var multiErr error

// Create a map for case-insensitive lookups
caseInsensitiveMap := make(map[string]interface{})
for key, val := range dataMap {
caseInsensitiveMap[strings.ToLower(key)] = val
}

for i := 0; i < value.NumField(); i++ {
fieldType := value.Type().Field(i)
fieldValue := value.Field(i)
fieldName := fieldType.Name

// Perform case-insensitive lookup for the key in dataMap
val, exists := caseInsensitiveMap[strings.ToLower(fieldName)]
if !exists {
continue
}

if !fieldValue.CanSet() {
multiErr = fmt.Errorf("%w: %s", errUnexportedField, fieldName)
continue
}

if err := setFieldValueFromData(fieldValue, val); err != nil {
multiErr = fmt.Errorf("%w; %w", multiErr, err)
continue
}

numFieldsSet++
}

if numFieldsSet == 0 {
return false, errFieldsNotSet
}

return true, multiErr
}

// setFieldValueFromData sets the field's value based on the provided data.
func setFieldValueFromData(field reflect.Value, data interface{}) error {
switch field.Kind() {
case reflect.String:
return setStringField(field, data)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return setIntField(field, data)
case reflect.Float32, reflect.Float64:
return setFloatField(field, data)
case reflect.Bool:
return setBoolField(field, data)
case reflect.Invalid, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Complex64, reflect.Complex128, reflect.Array, reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
reflect.Pointer, reflect.Slice, reflect.Struct, reflect.UnsafePointer:
return fmt.Errorf("%w: %s, %T", errUnsupportedFieldType, field.Type().Name(), data)
default:
return fmt.Errorf("%w: %s, %T", errUnsupportedFieldType, field.Type().Name(), data)
}
}

type customUnmarshaller struct {
dataMap map[string]interface{}
}

// UnmarshalJSON is a custom unmarshaller because json package in Go unmarshal numbers to float64 by default.
func (c *customUnmarshaller) UnmarshalJSON(data []byte) error {
var rawData map[string]interface{}

err := json.Unmarshal(data, &rawData)
if err != nil {
return err
}

dataMap := make(map[string]any, len(rawData))

for key, val := range rawData {
if valFloat, ok := val.(float64); ok {
valInt := int(valFloat)
if valFloat == float64(valInt) {
val = valInt
}
}

dataMap[key] = val
}

*c = customUnmarshaller{dataMap}

return nil
}

func parseStringToMap(data string) (map[string]interface{}, error) {
var c customUnmarshaller
err := json.Unmarshal([]byte(data), &c)

return c.dataMap, err
}

func setStringField(field reflect.Value, data interface{}) error {
if val, ok := data.(string); ok {
field.SetString(val)
return nil
}

return fmt.Errorf("%w: expected string but got %T", errUnsupportedFieldType, data)
}

func setIntField(field reflect.Value, data interface{}) error {
if val, ok := data.(int); ok {
field.SetInt(int64(val))
return nil
}

return fmt.Errorf("%w: expected int but got %T", errUnsupportedFieldType, data)
}

func setFloatField(field reflect.Value, data interface{}) error {
if val, ok := data.(float64); ok {
field.SetFloat(val)
return nil
}

return fmt.Errorf("%w: expected float64 but got %T", errUnsupportedFieldType, data)
}

func setBoolField(field reflect.Value, data interface{}) error {
if val, ok := data.(bool); ok {
field.SetBool(val)
return nil
}

return fmt.Errorf("%w: expected bool but got %T", errUnsupportedFieldType, data)
}
42 changes: 37 additions & 5 deletions pkg/gofr/http/multipart_file_bind.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package http

import (
"errors"
"io"
"mime/multipart"
"reflect"
Expand All @@ -9,6 +10,17 @@ import (
"gofr.dev/pkg/gofr/file"
)

var (
errUnsupportedInterfaceType = errors.New("unsupported interface value type")
errDataLengthExceeded = errors.New("data length exceeds array capacity")
errUnsupportedKind = errors.New("unsupported kind")
errSettingValueFailure = errors.New("error setting value at index")
errNotAStruct = errors.New("provided value is not a struct")
errUnexportedField = errors.New("cannot set field; it might be unexported")
errUnsupportedFieldType = errors.New("unsupported type for field")
errFieldsNotSet = errors.New("no fields were set")
)

type formData struct {
fields map[string][]string
files map[string][]*multipart.FileHeader
Expand Down Expand Up @@ -134,6 +146,8 @@ func (*formData) setFile(value reflect.Value, header []*multipart.FileHeader) (b
}

func (uf *formData) setFieldValue(value reflect.Value, data string) (bool, error) {
value = dereferencePointerType(value)

kind := value.Kind()
switch kind {
case reflect.String:
Expand All @@ -146,13 +160,31 @@ func (uf *formData) setFieldValue(value reflect.Value, data string) (bool, error
return uf.setFloatValue(value, data)
case reflect.Bool:
return uf.setBoolValue(value, data)
case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Array, reflect.Chan, reflect.Func, reflect.Interface,
reflect.Map, reflect.Pointer, reflect.Slice, reflect.Struct, reflect.UnsafePointer:
// These types are not supported for setting via form data
return false, nil
default:
case reflect.Slice, reflect.Array:
return uf.setSliceOrArrayValue(value, data)
case reflect.Interface:
return uf.setInterfaceValue(value, data)
case reflect.Struct:
return uf.setStructValue(value, data)
case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func,
reflect.Map, reflect.Pointer, reflect.UnsafePointer:
return false, nil
}

return false, nil
}

func dereferencePointerType(value reflect.Value) reflect.Value {
if value.Kind() == reflect.Ptr {
if value.IsNil() {
// Initialize the pointer to a new value if it's nil
value.Set(reflect.New(value.Type().Elem()))
}

value = value.Elem() // Dereference the pointer
}

return value
}

func (*formData) setStringValue(value reflect.Value, data string) (bool, error) {
Expand Down
Loading

0 comments on commit d8dd55a

Please sign in to comment.