From 507ff965466265da1d8d473a20e8e5c315595c02 Mon Sep 17 00:00:00 2001 From: xuerbujia <83055661+xuerbujia@users.noreply.github.com> Date: Sun, 9 Feb 2025 00:34:41 +0800 Subject: [PATCH] feat add tag switch to disable form array of split comma format (#4633) Co-authored-by: wuhongyu --- core/mapping/fieldoptions.go | 15 +++++----- core/mapping/unmarshaler.go | 38 ++++++++++++++----------- core/mapping/utils.go | 27 +++++++++++++++++- rest/httpx/requests_test.go | 55 ++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 25 deletions(-) diff --git a/core/mapping/fieldoptions.go b/core/mapping/fieldoptions.go index 14b3c84b3..7ba0eae4f 100644 --- a/core/mapping/fieldoptions.go +++ b/core/mapping/fieldoptions.go @@ -8,13 +8,14 @@ type ( // use context and OptionalDep option to determine the value of Optional // nothing to do with context.Context fieldOptionsWithContext struct { - Inherit bool - FromString bool - Optional bool - Options []string - Default string - EnvVar string - Range *numberRange + Inherit bool + FromString bool + Optional bool + Options []string + Default string + EnvVar string + Range *numberRange + FormArrayComma bool } fieldOptions struct { diff --git a/core/mapping/unmarshaler.go b/core/mapping/unmarshaler.go index 8d7839869..32dce6ef3 100644 --- a/core/mapping/unmarshaler.go +++ b/core/mapping/unmarshaler.go @@ -131,7 +131,7 @@ func (u *Unmarshaler) fillMapFromString(value reflect.Value, mapValue any) error } func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, - mapValue any, fullName string) error { + mapValue any, fullName string, opts *fieldOptionsWithContext) error { if !value.CanSet() { return errValueNotSettable } @@ -153,7 +153,7 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, } if u.opts.fromArray { - refValue = makeStringSlice(refValue) + refValue = makeStringSlice(refValue, opts) } var valid bool @@ -174,7 +174,7 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, return err } case reflect.Slice: - if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil { + if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName, opts); err != nil { return err } default: @@ -288,7 +288,7 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle defaultCacheLock.Unlock() } - return u.fillSlice(derefedType, value, slice, fullName) + return u.fillSlice(derefedType, value, slice, fullName, nil) } func (u *Unmarshaler) fillStructElement(baseType reflect.Type, target reflect.Value, @@ -359,7 +359,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any, switch dereffedElemKind { case reflect.Slice: target := reflect.New(dereffedElemType) - if err := u.fillSlice(elemType, target.Elem(), keythData, mapFullName); err != nil { + if err := u.fillSlice(elemType, target.Elem(), keythData, mapFullName, nil); err != nil { return emptyValue, err } @@ -604,7 +604,7 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re parent: vp.parent, }, fullName) case typeKind == reflect.Slice && valueKind == reflect.Slice: - return u.fillSlice(fieldType, value, mapValue, fullName) + return u.fillSlice(fieldType, value, mapValue, fullName, opts) case valueKind == reflect.Map && typeKind == reflect.Map: return u.fillMap(fieldType, value, mapValue, fullName) case valueKind == reflect.String && typeKind == reflect.Map: @@ -985,7 +985,7 @@ func (u *Unmarshaler) unmarshal(i, v any, fullName string) error { return errTypeMismatch } - return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName) + return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName, nil) default: return errUnsupportedType } @@ -1189,7 +1189,7 @@ func join(elem ...string) string { return builder.String() } -func makeStringSlice(refValue reflect.Value) reflect.Value { +func makeStringSlice(refValue reflect.Value, opts *fieldOptionsWithContext) reflect.Value { if refValue.Len() != 1 { return refValue } @@ -1203,19 +1203,23 @@ func makeStringSlice(refValue reflect.Value) reflect.Value { 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 + } - splits := strings.Split(val, comma) - if len(splits) <= 1 { + 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 } - 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 } func newInitError(name string) error { diff --git a/core/mapping/utils.go b/core/mapping/utils.go index f84167e3f..f6a1666d7 100644 --- a/core/mapping/utils.go +++ b/core/mapping/utils.go @@ -22,6 +22,7 @@ const ( optionalOption = "optional" optionsOption = "options" rangeOption = "range" + formArrayComma = "arrayComma" optionSeparator = "|" equalToken = "=" escapeChar = '\\' @@ -160,6 +161,10 @@ func doParseKeyAndOptions(field reflect.StructField, value string) (string, *fie } 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 { option := strings.TrimSpace(segment) if err := parseOption(&fieldOpts, field.Name, option); err != nil { @@ -410,6 +415,12 @@ func parseOption(fieldOpts *fieldOptions, fieldName, option string) error { } fieldOpts.Range = nr + case strings.HasPrefix(option, formArrayComma): + val, err := parseEqBoolOption(fieldName, formArrayComma, option) + if err != nil { + return err + } + fieldOpts.FormArrayComma = val } return nil @@ -437,7 +448,21 @@ func parseProperty(field, tag, val string) (string, error) { 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 { var segments []string var escaped, grouped bool diff --git a/rest/httpx/requests_test.go b/rest/httpx/requests_test.go index c81a5707f..0cf4df2f0 100644 --- a/rest/httpx/requests_test.go +++ b/rest/httpx/requests_test.go @@ -268,6 +268,61 @@ func TestParseFormArray(t *testing.T) { assert.ElementsMatch(t, []float64{2}, v.Numbers) } }) + t.Run("slice with one value on disable array of comma split format", func(t *testing.T) { + var v struct { + Codes []string `form:"codes,arrayComma=false"` + } + 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) + } + }) + t.Run("slice with multiple value on disable array of comma split format", func(t *testing.T) { + var v struct { + Codes []string `form:"codes,arrayComma=false"` + } + + 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 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) {