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 6 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
212 changes: 208 additions & 4 deletions pkg/gofr/http/multipart_file_bind.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
package http

import (
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"reflect"
"strconv"
"strings"

"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")
errFieldNotFound = errors.New("field not found in 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 +150,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 +164,199 @@ 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
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) 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) {
elemType := value.Type().Elem()

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

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

switch value.Kind() {
case reflect.Slice:
newSlice = reflect.MakeSlice(value.Type(), len(elements), len(elements))
case reflect.Array:
if len(elements) > value.Len() {
return false, errDataLengthExceeded
}

newSlice = reflect.New(value.Type()).Elem()
case reflect.Invalid, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint,
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64,
reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.String,
reflect.Struct, reflect.UnsafePointer, reflect.Pointer:
return false, fmt.Errorf("%w: %s", errUnsupportedKind, value.Kind())
default:
return false, nil
return false, fmt.Errorf("%w: %s", errUnsupportedKind, value.Kind())
}

// 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
}

for key, val := range dataMap {
field, err := getFieldByName(value, key)
if err != nil {
return false, err
}

if err := setFieldValueFromData(field, val); err != nil {
return false, err
}

// Return true and nil error once a field is set
return true, nil
}

// Return false and an error if no fields were set
return false, errFieldsNotSet
}

// getFieldByName retrieves a field by its name, considering case insensitivity.
func getFieldByName(value reflect.Value, key string) (reflect.Value, error) {
field := value.FieldByName(key)
if !field.IsValid() {
field = findFieldByNameIgnoreCase(value, key)
if !field.IsValid() {
return reflect.Value{}, fmt.Errorf("%w: %s", errFieldNotFound, key)
}
}

if !field.CanSet() {
return reflect.Value{}, fmt.Errorf("%w: %s", errUnexportedField, key)
}

return field, nil
}

// setFieldValueFromData sets the field's value based on the provided data.
func setFieldValueFromData(field reflect.Value, data interface{}) error {
switch val := data.(type) {
case string:
field.SetString(val)
case int:
field.SetInt(int64(val))
case float64:
field.SetFloat(val)
case bool:
field.SetBool(val)
default:
return fmt.Errorf("%w: %s, %T", errUnsupportedFieldType, field.Type().Name(), val)
}

return nil
}

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
}

// Helper function to find a struct field by name, ignoring case.
func findFieldByNameIgnoreCase(value reflect.Value, name string) reflect.Value {
t := value.Type()

for i := 0; i < t.NumField(); i++ {
if strings.EqualFold(t.Field(i).Name, name) {
return value.Field(i)
}
}

return reflect.Value{}
}

func (*formData) setStringValue(value reflect.Value, data string) (bool, error) {
Expand Down
127 changes: 127 additions & 0 deletions pkg/gofr/http/multipart_file_bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package http
import (
"reflect"
"testing"
"unsafe"

"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -54,3 +55,129 @@ func TestGetFieldName(t *testing.T) {
})
}
}

type testValue struct {
kind reflect.Kind
value interface{}
}

func Test_SetFieldValue_Success(t *testing.T) {
testCases := []struct {
desc string
data string
expected bool
valueType testValue
}{
{"String", "test", true, testValue{reflect.String, "string"}},
{"Int", "10", true, testValue{reflect.Int, 0}},
{"Uint", "10", true, testValue{reflect.Uint16, uint16(10)}},
{"Float64", "3.14", true, testValue{reflect.Float64, 0.0}},
{"Bool", "true", true, testValue{reflect.Bool, false}},
{"Slice", "1,2,3,4,5", true, testValue{reflect.Slice, []int{}}},
{"Array", "1,2,3,4,5", true, testValue{reflect.Array, [5]int{}}},
{"Struct", `{"name": "John", "age": 30}`, true, testValue{reflect.Struct, struct {
Name string `json:"name"`
Age int `json:"age"`
}{}}},
{"Interface", "test interface", true, testValue{reflect.Interface, new(any)}},
}

for _, tc := range testCases {
f := &formData{}
val := reflect.New(reflect.TypeOf(tc.valueType.value)).Elem()

set, err := f.setFieldValue(val, tc.data)
if err != nil {
t.Errorf("Unexpected error for value kind %v and data %q: %v", val.Kind(), tc.data, err)
}
Umang01-hash marked this conversation as resolved.
Show resolved Hide resolved

if set != tc.expected {
t.Errorf("Expected set to be %v for value kind %v and data %q, got %v", tc.expected, val.Kind(), tc.data, set)
}
Umang01-hash marked this conversation as resolved.
Show resolved Hide resolved
}
}

func TestSetFieldValue_InvalidKinds(t *testing.T) {
uf := &formData{}

tests := []struct {
kind reflect.Kind
data string
typ reflect.Type
}{
{reflect.Complex64, "foo", reflect.TypeOf(complex64(0))},
{reflect.Complex128, "bar", reflect.TypeOf(complex128(0))},
{reflect.Chan, "baz", reflect.TypeOf(make(chan int))},
{reflect.Func, "qux", reflect.TypeOf(func() {})},
{reflect.Map, "quux", reflect.TypeOf(map[string]int{})},
{reflect.UnsafePointer, "grault", reflect.TypeOf(unsafe.Pointer(nil))},
}

for _, tt := range tests {
value := reflect.New(tt.typ).Elem()
ok, err := uf.setFieldValue(value, tt.data)

assert.False(t, ok, "expected false, got true for kind %v", tt.kind)

assert.NoError(t, err, "expected nil, got %v for kind %v", err, tt.kind)
}
}

func TestSetSliceOrArrayValue(t *testing.T) {
type testStruct struct {
Slice []string
Array [3]string
}

uf := &formData{}

// Test with a slice
value := reflect.ValueOf(&testStruct{Slice: nil}).Elem().FieldByName("Slice")

data := "a,b,c"

ok, err := uf.setSliceOrArrayValue(value, data)

assert.True(t, ok, "setSliceOrArrayValue failed")

if err != nil {
t.Errorf("setSliceOrArrayValue failed: %v", err)
}
Umang01-hash marked this conversation as resolved.
Show resolved Hide resolved

if len(value.Interface().([]string)) != 3 {
t.Fatal("slice not set correctly")
}

// Test with an array
value = reflect.ValueOf(&testStruct{Array: [3]string{}}).Elem().FieldByName("Array")

data = "a,b,c"

ok, err = uf.setSliceOrArrayValue(value, data)
if !ok || err != nil {
t.Errorf("setSliceOrArrayValue failed: %v", err)
}
}

func TestSetStructValue(t *testing.T) {
type testStruct struct {
Field1 string
Field2 int
}

uf := &formData{}

// Test with a valid input string
value := reflect.ValueOf(&testStruct{}).Elem()

data := `{"Field1":"value1","Field2":123}`

ok, err := uf.setStructValue(value, data)
if !ok || err != nil {
t.Errorf("setStructValue failed: %v", err)
}

if value.FieldByName("Field1").String() != "value1" {
t.Error("struct fields not set correctly")
}
}
Loading