chore: simplify http query array parsing (#4637)

This commit is contained in:
Kevin Wan
2025-02-09 01:00:52 +08:00
committed by GitHub
parent 507ff96546
commit f747585518
5 changed files with 38 additions and 116 deletions

View File

@@ -8,14 +8,13 @@ type (
// use context and OptionalDep option to determine the value of Optional // use context and OptionalDep option to determine the value of Optional
// nothing to do with context.Context // nothing to do with context.Context
fieldOptionsWithContext struct { fieldOptionsWithContext struct {
Inherit bool Inherit bool
FromString bool FromString bool
Optional bool Optional bool
Options []string Options []string
Default string Default string
EnvVar string EnvVar string
Range *numberRange Range *numberRange
FormArrayComma bool
} }
fieldOptions struct { fieldOptions struct {

View File

@@ -131,7 +131,7 @@ func (u *Unmarshaler) fillMapFromString(value reflect.Value, mapValue any) error
} }
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
mapValue any, fullName string, opts *fieldOptionsWithContext) error { mapValue any, fullName string) error {
if !value.CanSet() { if !value.CanSet() {
return errValueNotSettable return errValueNotSettable
} }
@@ -152,10 +152,6 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
return nil return nil
} }
if u.opts.fromArray {
refValue = makeStringSlice(refValue, opts)
}
var valid bool var valid bool
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap()) conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
@@ -174,7 +170,7 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
return err return err
} }
case reflect.Slice: case reflect.Slice:
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName, opts); err != nil { if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
return err return err
} }
default: default:
@@ -288,7 +284,7 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
defaultCacheLock.Unlock() defaultCacheLock.Unlock()
} }
return u.fillSlice(derefedType, value, slice, fullName, nil) return u.fillSlice(derefedType, value, slice, fullName)
} }
func (u *Unmarshaler) fillStructElement(baseType reflect.Type, target reflect.Value, func (u *Unmarshaler) fillStructElement(baseType reflect.Type, target reflect.Value,
@@ -359,7 +355,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any,
switch dereffedElemKind { switch dereffedElemKind {
case reflect.Slice: case reflect.Slice:
target := reflect.New(dereffedElemType) target := reflect.New(dereffedElemType)
if err := u.fillSlice(elemType, target.Elem(), keythData, mapFullName, nil); err != nil { if err := u.fillSlice(elemType, target.Elem(), keythData, mapFullName); err != nil {
return emptyValue, err return emptyValue, err
} }
@@ -604,7 +600,7 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
parent: vp.parent, parent: vp.parent,
}, fullName) }, fullName)
case typeKind == reflect.Slice && valueKind == reflect.Slice: case typeKind == reflect.Slice && valueKind == reflect.Slice:
return u.fillSlice(fieldType, value, mapValue, fullName, opts) return u.fillSlice(fieldType, value, mapValue, fullName)
case valueKind == reflect.Map && typeKind == reflect.Map: case valueKind == reflect.Map && typeKind == reflect.Map:
return u.fillMap(fieldType, value, mapValue, fullName) return u.fillMap(fieldType, value, mapValue, fullName)
case valueKind == reflect.String && typeKind == reflect.Map: case valueKind == reflect.String && typeKind == reflect.Map:
@@ -985,7 +981,7 @@ func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
return errTypeMismatch return errTypeMismatch
} }
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName, nil) return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
default: default:
return errUnsupportedType return errUnsupportedType
} }
@@ -1189,39 +1185,6 @@ func join(elem ...string) string {
return builder.String() return builder.String()
} }
func makeStringSlice(refValue reflect.Value, opts *fieldOptionsWithContext) reflect.Value {
if refValue.Len() != 1 {
return refValue
}
element := refValue.Index(0)
if element.Kind() != reflect.String {
return refValue
}
val, ok := element.Interface().(string)
if !ok {
return refValue
}
// comma mode is on by default or display designations are split by commas
if opts == nil || opts.FormArrayComma {
splits := strings.Split(val, comma)
if len(splits) <= 1 {
return refValue
}
slice := reflect.MakeSlice(stringSliceType, len(splits), len(splits))
for i, split := range splits {
// allow empty strings
slice.Index(i).Set(reflect.ValueOf(split))
}
return slice
} else {
return refValue
}
}
func newInitError(name string) error { func newInitError(name string) error {
return fmt.Errorf("field %q is not set", name) return fmt.Errorf("field %q is not set", name)
} }

View File

@@ -1462,9 +1462,7 @@ func TestUnmarshalIntSlice(t *testing.T) {
ast := assert.New(t) ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray()) unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) { ast.Error(unmarshaler.Unmarshal(m, &v))
ast.ElementsMatch([]int{1, 2}, v.Ages)
}
}) })
} }
@@ -1546,7 +1544,22 @@ func TestUnmarshalStringSliceFromString(t *testing.T) {
ast := assert.New(t) ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray()) unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) { if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]string{"", ""}, v.Names) ast.ElementsMatch([]string{","}, v.Names)
}
})
t.Run("slice from valid strings with comma", func(t *testing.T) {
var v struct {
Names []string `key:"names"`
}
m := map[string]any{
"names": []string{"aa,bb"},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]string{"aa,bb"}, v.Names)
} }
}) })

View File

@@ -22,7 +22,6 @@ const (
optionalOption = "optional" optionalOption = "optional"
optionsOption = "options" optionsOption = "options"
rangeOption = "range" rangeOption = "range"
formArrayComma = "arrayComma"
optionSeparator = "|" optionSeparator = "|"
equalToken = "=" equalToken = "="
escapeChar = '\\' escapeChar = '\\'
@@ -161,10 +160,6 @@ func doParseKeyAndOptions(field reflect.StructField, value string) (string, *fie
} }
var fieldOpts fieldOptions var fieldOpts fieldOptions
// The comma split form array mode was enabled in 1.7.5
// so the default value is true in order not to introduce destructiveness
fieldOpts.FormArrayComma = true
for _, segment := range options { for _, segment := range options {
option := strings.TrimSpace(segment) option := strings.TrimSpace(segment)
if err := parseOption(&fieldOpts, field.Name, option); err != nil { if err := parseOption(&fieldOpts, field.Name, option); err != nil {
@@ -415,12 +410,6 @@ func parseOption(fieldOpts *fieldOptions, fieldName, option string) error {
} }
fieldOpts.Range = nr fieldOpts.Range = nr
case strings.HasPrefix(option, formArrayComma):
val, err := parseEqBoolOption(fieldName, formArrayComma, option)
if err != nil {
return err
}
fieldOpts.FormArrayComma = val
} }
return nil return nil
@@ -448,21 +437,7 @@ func parseProperty(field, tag, val string) (string, error) {
return strings.TrimSpace(segs[1]), nil return strings.TrimSpace(segs[1]), nil
} }
func parseEqBoolOption(field, tag, val string) (bool, error) {
segs := strings.Split(val, equalToken)
switch len(segs) {
case 1:
return true, nil
case 2:
parseBool, err := strconv.ParseBool(segs[1])
if err != nil {
return false, fmt.Errorf("field %q has wrong %s", field, tag)
}
return parseBool, nil
default:
return false, fmt.Errorf("field %q has wrong %s", field, tag)
}
}
func parseSegments(val string) []string { func parseSegments(val string) []string {
var segments []string var segments []string
var escaped, grouped bool var escaped, grouped bool

View File

@@ -160,7 +160,7 @@ func TestParseFormArray(t *testing.T) {
http.NoBody) http.NoBody)
assert.NoError(t, err) assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) { if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"1", "2", "3"}, v.Names) assert.ElementsMatch(t, []string{"1,2,3"}, v.Names)
} }
}) })
@@ -189,9 +189,7 @@ func TestParseFormArray(t *testing.T) {
"/a?numbers=1,2,3", "/a?numbers=1,2,3",
http.NoBody) http.NoBody)
assert.NoError(t, err) assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) { assert.Error(t, Parse(r, &v))
assert.ElementsMatch(t, []int{1, 2, 3}, v.Numbers)
}
}) })
t.Run("slice with one value on array format brackets", func(t *testing.T) { t.Run("slice with one value on array format brackets", func(t *testing.T) {
@@ -268,9 +266,10 @@ func TestParseFormArray(t *testing.T) {
assert.ElementsMatch(t, []float64{2}, v.Numbers) assert.ElementsMatch(t, []float64{2}, v.Numbers)
} }
}) })
t.Run("slice with one value on disable array of comma split format", func(t *testing.T) {
t.Run("slice with one value", func(t *testing.T) {
var v struct { var v struct {
Codes []string `form:"codes,arrayComma=false"` Codes []string `form:"codes"`
} }
r, err := http.NewRequest( r, err := http.NewRequest(
http.MethodGet, http.MethodGet,
@@ -281,7 +280,8 @@ func TestParseFormArray(t *testing.T) {
assert.ElementsMatch(t, []string{"aaa,bbb,ccc"}, v.Codes) assert.ElementsMatch(t, []string{"aaa,bbb,ccc"}, v.Codes)
} }
}) })
t.Run("slice with multiple value on disable array of comma split format", func(t *testing.T) {
t.Run("slice with multiple values", func(t *testing.T) {
var v struct { var v struct {
Codes []string `form:"codes,arrayComma=false"` Codes []string `form:"codes,arrayComma=false"`
} }
@@ -295,34 +295,6 @@ func TestParseFormArray(t *testing.T) {
assert.ElementsMatch(t, []string{"aaa,bbb,ccc", "ccc,ddd,eee"}, v.Codes) assert.ElementsMatch(t, []string{"aaa,bbb,ccc", "ccc,ddd,eee"}, v.Codes)
} }
}) })
t.Run("slice with multiple value on enable array of comma split format", func(t *testing.T) {
var v struct {
Codes []string `form:"codes,arrayComma=true"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a?codes=aaa,bbb,ccc&codes=ccc,ddd,eee",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"aaa,bbb,ccc", "ccc,ddd,eee"}, v.Codes)
}
})
t.Run("slice with one value on enable array of comma split format", func(t *testing.T) {
var v struct {
Codes []string `form:"codes,arrayComma=true"`
}
r, err := http.NewRequest(
http.MethodGet,
"/a?codes=aaa,bbb,ccc",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"aaa", "bbb", "ccc"}, v.Codes)
}
})
} }
func TestParseForm_Error(t *testing.T) { func TestParseForm_Error(t *testing.T) {