mirror of
https://github.com/zeromicro/go-zero.git
synced 2025-02-02 16:28:39 +08:00
feat: support query array in httpx.Parse (#4440)
This commit is contained in:
parent
f822c9a94f
commit
18cb3141ba
@ -50,6 +50,7 @@ type (
|
|||||||
|
|
||||||
unmarshalOptions struct {
|
unmarshalOptions struct {
|
||||||
fillDefault bool
|
fillDefault bool
|
||||||
|
fromArray bool
|
||||||
fromString bool
|
fromString bool
|
||||||
opaqueKeys bool
|
opaqueKeys bool
|
||||||
canonicalKey func(key string) string
|
canonicalKey func(key string) string
|
||||||
@ -811,6 +812,19 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
|
|||||||
return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName)
|
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{
|
return u.processNamedFieldWithValue(field.Type, value, valueWithParent{
|
||||||
value: mapValue,
|
value: mapValue,
|
||||||
parent: valuer,
|
parent: valuer,
|
||||||
@ -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.
|
// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
|
||||||
// Opaque keys are keys that are not processed by the unmarshaler.
|
// Opaque keys are keys that are not processed by the unmarshaler.
|
||||||
func WithOpaqueKeys() UnmarshalOption {
|
func WithOpaqueKeys() UnmarshalOption {
|
||||||
|
@ -5639,6 +5639,62 @@ func TestUnmarshalFromStringSliceForTypeMismatch(t *testing.T) {
|
|||||||
}, &v))
|
}, &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) {
|
func TestUnmarshalWithOpaqueKeys(t *testing.T) {
|
||||||
var v struct {
|
var v struct {
|
||||||
Opaque string `key:"opaque.key"`
|
Opaque string `key:"opaque.key"`
|
||||||
|
@ -23,9 +23,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
formUnmarshaler = mapping.NewUnmarshaler(formKey, mapping.WithStringValues(), mapping.WithOpaqueKeys())
|
formUnmarshaler = mapping.NewUnmarshaler(
|
||||||
pathUnmarshaler = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues(), mapping.WithOpaqueKeys())
|
formKey,
|
||||||
validator atomic.Value
|
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.
|
// Validator defines the interface for validating the request.
|
||||||
|
@ -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) {
|
func TestParseForm_Error(t *testing.T) {
|
||||||
var v struct {
|
var v struct {
|
||||||
Name string `form:"name"`
|
Name string `form:"name"`
|
||||||
|
@ -20,10 +20,16 @@ func GetFormValues(r *http.Request) (map[string]any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
params := make(map[string]any, len(r.Form))
|
params := make(map[string]any, len(r.Form))
|
||||||
for name := range r.Form {
|
for name, values := range r.Form {
|
||||||
formValue := r.Form.Get(name)
|
filtered := make([]string, 0, len(values))
|
||||||
if len(formValue) > 0 {
|
for _, v := range values {
|
||||||
params[name] = formValue
|
if len(v) > 0 {
|
||||||
|
filtered = append(filtered, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filtered) > 0 {
|
||||||
|
params[name] = filtered
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ func TestPatRouter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseSlice(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)
|
reader := strings.NewReader(body)
|
||||||
r, err := http.NewRequest(http.MethodPost, "http://hello.com/", reader)
|
r, err := http.NewRequest(http.MethodPost, "http://hello.com/", reader)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -388,7 +388,7 @@ func TestParseQueryRequired(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseOptional(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)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
router := NewRouter()
|
router := NewRouter()
|
||||||
|
Loading…
Reference in New Issue
Block a user