Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
studyzy authored Nov 5, 2024
2 parents d685c8f + 18cb314 commit bfa0586
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 9 deletions.
24 changes: 24 additions & 0 deletions core/mapping/unmarshaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type (

unmarshalOptions struct {
fillDefault bool
fromArray bool
fromString bool
opaqueKeys bool
canonicalKey func(key string) string
Expand Down Expand Up @@ -811,6 +812,19 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName)
}

if u.opts.fromArray {
fieldKind := field.Type.Kind()
if fieldKind != reflect.Slice && fieldKind != reflect.Array {
valueKind := reflect.TypeOf(mapValue).Kind()
if valueKind == reflect.Slice || valueKind == reflect.Array {
val := reflect.ValueOf(mapValue)
if val.Len() > 0 {
mapValue = val.Index(0).Interface()
}
}
}
}

return u.processNamedFieldWithValue(field.Type, value, valueWithParent{
value: mapValue,
parent: valuer,
Expand Down Expand Up @@ -990,6 +1004,16 @@ func WithDefault() UnmarshalOption {
}
}

// WithFromArray customizes an Unmarshaler with converting array values to non-array types.
// For example, if the field type is []string, and the value is [hello],
// the field type can be `string`, instead of `[]string`.
// Typically, this option is used for unmarshaling from form values.
func WithFromArray() UnmarshalOption {
return func(opt *unmarshalOptions) {
opt.fromArray = true
}
}

// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
// Opaque keys are keys that are not processed by the unmarshaler.
func WithOpaqueKeys() UnmarshalOption {
Expand Down
56 changes: 56 additions & 0 deletions core/mapping/unmarshaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5639,6 +5639,62 @@ func TestUnmarshalFromStringSliceForTypeMismatch(t *testing.T) {
}, &v))
}

func TestUnmarshalWithFromArray(t *testing.T) {
t.Run("array", func(t *testing.T) {
var v struct {
Value []string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{"foo", "bar"},
}, &v)) {
assert.ElementsMatch(t, []string{"foo", "bar"}, v.Value)
}
})

t.Run("not array", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{"foo"},
}, &v)) {
assert.Equal(t, "foo", v.Value)
}
})

t.Run("not array and empty", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{""},
}, &v)) {
assert.Empty(t, v.Value)
}
})

t.Run("not array and no value", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
assert.Error(t, unmarshaler.Unmarshal(map[string]any{}, &v))
})

t.Run("not array and no value and optional", func(t *testing.T) {
var v struct {
Value string `key:"value,optional"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{}, &v)) {
assert.Empty(t, v.Value)
}
})
}

func TestUnmarshalWithOpaqueKeys(t *testing.T) {
var v struct {
Opaque string `key:"opaque.key"`
Expand Down
13 changes: 10 additions & 3 deletions rest/httpx/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ const (
)

var (
formUnmarshaler = mapping.NewUnmarshaler(formKey, mapping.WithStringValues(), mapping.WithOpaqueKeys())
pathUnmarshaler = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues(), mapping.WithOpaqueKeys())
validator atomic.Value
formUnmarshaler = mapping.NewUnmarshaler(
formKey,
mapping.WithStringValues(),
mapping.WithOpaqueKeys(),
mapping.WithFromArray())
pathUnmarshaler = mapping.NewUnmarshaler(
pathKey,
mapping.WithStringValues(),
mapping.WithOpaqueKeys())
validator atomic.Value
)

// Validator defines the interface for validating the request.
Expand Down
55 changes: 55 additions & 0 deletions rest/httpx/requests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,61 @@ func TestParseForm(t *testing.T) {
})
}

func TestParseFormArray(t *testing.T) {
t.Run("slice", func(t *testing.T) {
var v struct {
Name []string `form:"name"`
Age []int `form:"age"`
Percent []float64 `form:"percent,optional"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?name=hello&name=world&age=18&age=19&percent=3.4&percent=4.5",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"hello", "world"}, v.Name)
assert.ElementsMatch(t, []int{18, 19}, v.Age)
assert.ElementsMatch(t, []float64{3.4, 4.5}, v.Percent)
}
})

t.Run("slice with single value", func(t *testing.T) {
var v struct {
Name []string `form:"name"`
Age []int `form:"age"`
Percent []float64 `form:"percent,optional"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?name=hello&age=18&percent=3.4",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"hello"}, v.Name)
assert.ElementsMatch(t, []int{18}, v.Age)
assert.ElementsMatch(t, []float64{3.4}, v.Percent)
}
})

t.Run("slice with empty and non-empty", func(t *testing.T) {
var v struct {
Name []string `form:"name"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?name=&name=1",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"1"}, v.Name)
}
})
}

func TestParseForm_Error(t *testing.T) {
var v struct {
Name string `form:"name"`
Expand Down
14 changes: 10 additions & 4 deletions rest/httpx/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ func GetFormValues(r *http.Request) (map[string]any, error) {
}

params := make(map[string]any, len(r.Form))
for name := range r.Form {
formValue := r.Form.Get(name)
if len(formValue) > 0 {
params[name] = formValue
for name, values := range r.Form {
filtered := make([]string, 0, len(values))
for _, v := range values {
if len(v) > 0 {
filtered = append(filtered, v)
}
}

if len(filtered) > 0 {
params[name] = filtered
}
}

Expand Down
4 changes: 2 additions & 2 deletions rest/router/patrouter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func TestPatRouter(t *testing.T) {
}

func TestParseSlice(t *testing.T) {
body := `names=%5B%22first%22%2C%22second%22%5D`
body := `names=first&names=second`
reader := strings.NewReader(body)
r, err := http.NewRequest(http.MethodPost, "http://hello.com/", reader)
assert.Nil(t, err)
Expand Down Expand Up @@ -388,7 +388,7 @@ func TestParseQueryRequired(t *testing.T) {
}

func TestParseOptional(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever&zipcode=", nil)
r, err := http.NewRequest(http.MethodGet, "http://hello.com/kevin/2017?nickname=whatever", nil)
assert.Nil(t, err)

router := NewRouter()
Expand Down

0 comments on commit bfa0586

Please sign in to comment.