Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

En/fix multipart form types #986

Merged
merged 25 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ed5cc91
initial commit
Umang01-hash Sep 4, 2024
3a1fdf8
resolve linters
Umang01-hash Sep 4, 2024
c339cac
add test methods for various types
Umang01-hash Sep 4, 2024
06c50d4
change setInterfaceValue method and update test
Umang01-hash Sep 4, 2024
7852654
resolve review comments 1
Umang01-hash Sep 4, 2024
8f123f3
updated test case
Umang01-hash Sep 4, 2024
cb479f5
replace if in tests to require methods
Umang01-hash Sep 5, 2024
3cba30a
break file multipart_file_bind.go into two files
Umang01-hash Sep 9, 2024
b526bf9
Merge branch 'development' into en/fix_multipart_form_types
Umang01-hash Sep 10, 2024
c369f1b
Merge branch 'development' into en/fix_multipart_form_types
Umang01-hash Sep 11, 2024
5adcce8
Merge branch 'development' of github.com:gofr-dev/gofr into en/fix_mu…
Umang01-hash Sep 11, 2024
560d0b9
Merge branch 'en/fix_multipart_form_types' of github.com:gofr-dev/gof…
Umang01-hash Sep 12, 2024
29497e0
update documentation
Umang01-hash Sep 12, 2024
b5b71f4
refactpr setStructValue method in form binder
Umang01-hash Sep 12, 2024
1e01033
update setStructValue method to set values for all fields in struct
Umang01-hash Sep 13, 2024
4373eec
resolve linter
Umang01-hash Sep 13, 2024
62dd5f0
Merge branch 'development' into en/fix_multipart_form_types
Umang01-hash Sep 13, 2024
5c71e32
Merge branch 'development' of github.com:gofr-dev/gofr into en/fix_mu…
Umang01-hash Sep 13, 2024
fd37828
Merge branch 'en/fix_multipart_form_types' of github.com:gofr-dev/gof…
Umang01-hash Sep 13, 2024
880b451
Merge branch 'development' of github.com:gofr-dev/gofr into en/fix_mu…
Umang01-hash Sep 17, 2024
854e281
refactor setSliceOrArrayValue to use if-else instead of switch
Umang01-hash Sep 17, 2024
1d8a0e3
Merge branch 'development' into en/fix_multipart_form_types
Umang01-hash Sep 17, 2024
7ff8ba3
Merge branch 'development' into en/fix_multipart_form_types
Umang01-hash Sep 17, 2024
a6a1e92
replace 'a' with 'file_uplaod' in FileHeader tag
Umang01-hash Sep 17, 2024
42769fb
fix tests
Umang01-hash Sep 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading