commit 4a0a5220ce3f0e5400c5527ee04e5b916eceef8b Author: upstream Date: Fri Sep 4 08:15:15 2026 +0200 Upstream go-playground/form state of the touched files before 8baf0c3554 diff --git a/decoder.go b/decoder.go new file mode 100644 index 0000000..d746ea0 --- /dev/null +++ b/decoder.go @@ -0,0 +1,758 @@ +package form + +import ( + "fmt" + "log" + "net/url" + "reflect" + "strconv" + "time" +) + +const ( + errArraySize = "Array size of '%d' is larger than the maximum currently set on the decoder of '%d'. To increase this limit please see, SetMaxArraySize(size uint)" + errMissingStartBracket = "Invalid formatting for key '%s' missing '[' bracket" + errMissingEndBracket = "Invalid formatting for key '%s' missing ']' bracket" +) + +type decoder struct { + d *Decoder + errs DecodeErrors + dm dataMap + aliasMap map[string]*recursiveData + values url.Values + maxKeyLen int + namespace []byte +} + +func (d *decoder) setError(namespace []byte, err error) { + if d.errs == nil { + d.errs = make(DecodeErrors) + } + d.errs[string(namespace)] = err +} + +func (d *decoder) findAlias(ns string) *recursiveData { + if d.aliasMap != nil { + return d.aliasMap[ns] + } + return nil +} + +func (d *decoder) parseMapData() { + // already parsed + if len(d.dm) > 0 { + return + } + + d.maxKeyLen = 0 + d.dm = d.dm[0:0] + + if d.aliasMap == nil { + d.aliasMap = make(map[string]*recursiveData) + } else { + clear(d.aliasMap) + } + + var i int + var idx int + var l int + var insideBracket bool + var rd *recursiveData + var isNum bool + + for k := range d.values { + + if len(k) > d.maxKeyLen { + d.maxKeyLen = len(k) + } + + for i = 0; i < len(k); i++ { + + switch k[i] { + case '[': + idx = i + insideBracket = true + isNum = true + case ']': + + if !insideBracket { + log.Panicf(errMissingStartBracket, k) + } + + if rd = d.findAlias(k[:idx]); rd == nil { + + l = len(d.dm) + 1 + + if l > cap(d.dm) { + dm := make(dataMap, l) + copy(dm, d.dm) + rd = new(recursiveData) + dm[len(d.dm)] = rd + d.dm = dm + } else { + l = len(d.dm) + d.dm = d.dm[:l+1] + rd = d.dm[l] + rd.sliceLen = 0 + rd.keys = rd.keys[0:0] + } + + rd.alias = k[:idx] + d.aliasMap[rd.alias] = rd + } + + // is map + key + ke := key{ + ivalue: -1, + value: k[idx+1 : i], + searchValue: k[idx : i+1], + } + + // is key is number, most likely array key, keep track of just in case an array/slice. + if isNum { + + // no need to check for error, it will always pass + // as we have done the checking to ensure + // the value is a number ahead of time. + var err error + ke.ivalue, err = strconv.Atoi(ke.value) + if err != nil { + ke.ivalue = -1 + } + + if ke.ivalue > rd.sliceLen { + rd.sliceLen = ke.ivalue + + } + } + + rd.keys = append(rd.keys, ke) + + insideBracket = false + default: + // checking if not a number, 0-9 is 48-57 in byte, see for yourself fmt.Println('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') + if insideBracket && (k[i] > 57 || k[i] < 48) { + isNum = false + } + } + } + + // if still inside bracket, that means no ending bracket was ever specified + if insideBracket { + log.Panicf(errMissingEndBracket, k) + } + } +} + +func (d *decoder) traverseStruct(v reflect.Value, typ reflect.Type, namespace []byte) (set bool) { + + l := len(namespace) + first := l == 0 + + // anonymous structs will still work for caching as the whole definition is stored + // including tags + s, ok := d.d.structCache.Get(typ) + if !ok { + s = d.d.structCache.parseStruct(d.d.mode, v, typ, d.d.tagName) + } + + for _, f := range s.fields { + namespace = namespace[:l] + + if f.isAnonymous { + if d.setFieldByType(v.Field(f.idx), namespace, 0) { + set = true + } + } + + if first { + namespace = append(namespace, f.name...) + } else { + namespace = append(namespace, d.d.namespacePrefix...) + namespace = append(namespace, f.name...) + namespace = append(namespace, d.d.namespaceSuffix...) + } + + if d.setFieldByType(v.Field(f.idx), namespace, 0) { + set = true + } + } + + return +} + +func (d *decoder) setFieldByType(current reflect.Value, namespace []byte, idx int) (set bool) { + + var err error + v, kind := ExtractType(current) + + arr, ok := d.values[string(namespace)] + + if d.d.customTypeFuncs != nil { + + if ok { + if cf, ok := d.d.customTypeFuncs[v.Type()]; ok { + val, err := cf(arr[idx:]) + if err != nil { + d.setError(namespace, err) + return + } + + v.Set(reflect.ValueOf(val)) + set = true + return + } + } + } + switch kind { + case reflect.Interface: + if !ok || idx == len(arr) { + return + } + v.Set(reflect.ValueOf(arr[idx])) + set = true + + case reflect.Ptr: + newVal := reflect.New(v.Type().Elem()) + if set = d.setFieldByType(newVal.Elem(), namespace, idx); set { + v.Set(newVal) + } + + case reflect.String: + if !ok || idx == len(arr) { + return + } + v.SetString(arr[idx]) + set = true + + case reflect.Uint, reflect.Uint64: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var u64 uint64 + if u64, err = strconv.ParseUint(arr[idx], 10, 64); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetUint(u64) + set = true + + case reflect.Uint8: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var u64 uint64 + if u64, err = strconv.ParseUint(arr[idx], 10, 8); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetUint(u64) + set = true + + case reflect.Uint16: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var u64 uint64 + if u64, err = strconv.ParseUint(arr[idx], 10, 16); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetUint(u64) + set = true + + case reflect.Uint32: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var u64 uint64 + if u64, err = strconv.ParseUint(arr[idx], 10, 32); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetUint(u64) + set = true + + case reflect.Int, reflect.Int64: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var i64 int64 + if i64, err = strconv.ParseInt(arr[idx], 10, 64); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetInt(i64) + set = true + + case reflect.Int8: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var i64 int64 + if i64, err = strconv.ParseInt(arr[idx], 10, 8); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetInt(i64) + set = true + + case reflect.Int16: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var i64 int64 + if i64, err = strconv.ParseInt(arr[idx], 10, 16); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetInt(i64) + set = true + + case reflect.Int32: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var i64 int64 + if i64, err = strconv.ParseInt(arr[idx], 10, 32); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetInt(i64) + set = true + + case reflect.Float32: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var f float64 + if f, err = strconv.ParseFloat(arr[idx], 32); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Float Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetFloat(f) + set = true + + case reflect.Float64: + if !ok || idx == len(arr) || len(arr[idx]) == 0 { + return + } + var f float64 + if f, err = strconv.ParseFloat(arr[idx], 64); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Float Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetFloat(f) + set = true + + case reflect.Bool: + if !ok || idx == len(arr) { + return + } + var b bool + if b, err = parseBool(arr[idx]); err != nil { + d.setError(namespace, fmt.Errorf("Invalid Boolean Value '%s' Type '%v' Namespace '%s'", arr[idx], v.Type(), string(namespace))) + return + } + v.SetBool(b) + set = true + + case reflect.Slice: + d.parseMapData() + // slice elements could be mixed eg. number and non-numbers Value[0]=[]string{"10"} and Value=[]string{"10","20"} + + if ok && len(arr) > 0 { + var varr reflect.Value + + var ol int + l := len(arr) + + if v.IsNil() { + varr = reflect.MakeSlice(v.Type(), len(arr), len(arr)) + } else { + + ol = v.Len() + l += ol + + if v.Cap() <= l { + varr = reflect.MakeSlice(v.Type(), l, l) + } else { + // preserve predefined capacity, possibly for reuse after decoding + varr = reflect.MakeSlice(v.Type(), l, v.Cap()) + } + reflect.Copy(varr, v) + } + + for i := ol; i < l; i++ { + newVal := reflect.New(v.Type().Elem()).Elem() + + if d.setFieldByType(newVal, namespace, i-ol) { + set = true + varr.Index(i).Set(newVal) + } + } + + v.Set(varr) + } + + // maybe it's an numbered array i.e. Phone[0].Number + if rd := d.findAlias(string(namespace)); rd != nil { + + var varr reflect.Value + var kv key + + sl := rd.sliceLen + 1 + + // checking below for maxArraySize, but if array exists and already + // has sufficient capacity allocated then we do not check as the code + // obviously allows a capacity greater than the maxArraySize. + + if v.IsNil() { + + if sl > d.d.maxArraySize { + d.setError(namespace, fmt.Errorf(errArraySize, sl, d.d.maxArraySize)) + return + } + + varr = reflect.MakeSlice(v.Type(), sl, sl) + + } else if v.Len() < sl { + + if v.Cap() <= sl { + + if sl > d.d.maxArraySize { + d.setError(namespace, fmt.Errorf(errArraySize, sl, d.d.maxArraySize)) + return + } + + varr = reflect.MakeSlice(v.Type(), sl, sl) + } else { + varr = reflect.MakeSlice(v.Type(), sl, v.Cap()) + } + + reflect.Copy(varr, v) + + } else { + varr = v + } + + for i := 0; i < len(rd.keys); i++ { + + kv = rd.keys[i] + newVal := reflect.New(varr.Type().Elem()).Elem() + + if kv.ivalue == -1 { + d.setError(namespace, fmt.Errorf("invalid slice index '%s'", kv.value)) + continue + } + + if d.setFieldByType(newVal, append(namespace, kv.searchValue...), 0) { + set = true + varr.Index(kv.ivalue).Set(newVal) + } + } + + if !set { + return + } + + v.Set(varr) + } + + case reflect.Array: + d.parseMapData() + + // array elements could be mixed eg. number and non-numbers Value[0]=[]string{"10"} and Value=[]string{"10","20"} + + if ok && len(arr) > 0 { + var varr reflect.Value + l := len(arr) + overCapacity := v.Len() < l + if overCapacity { + // more values than array capacity, ignore values over capacity as it's possible some would just want + // to grab the first x number of elements; in the future strict mode logic should return an error + fmt.Println("warning number of post form array values is larger than array capacity, ignoring overflow values") + } + varr = reflect.Indirect(reflect.New(reflect.ArrayOf(v.Len(), v.Type().Elem()))) + reflect.Copy(varr, v) + + if v.Len() < len(arr) { + l = v.Len() + } + for i := 0; i < l; i++ { + newVal := reflect.New(v.Type().Elem()).Elem() + + if d.setFieldByType(newVal, namespace, i) { + set = true + varr.Index(i).Set(newVal) + } + } + v.Set(varr) + } + + // maybe it's an numbered array i.e. Phone[0].Number + if rd := d.findAlias(string(namespace)); rd != nil { + var varr reflect.Value + var kv key + + overCapacity := rd.sliceLen >= v.Len() + if overCapacity { + // more values than array capacity, ignore values over capacity as it's possible some would just want + // to grab the first x number of elements; in the future strict mode logic should return an error + fmt.Println("warning number of post form array values is larger than array capacity, ignoring overflow values") + } + varr = reflect.Indirect(reflect.New(reflect.ArrayOf(v.Len(), v.Type().Elem()))) + reflect.Copy(varr, v) + + for i := 0; i < len(rd.keys); i++ { + kv = rd.keys[i] + if kv.ivalue >= v.Len() { + continue + } + newVal := reflect.New(varr.Type().Elem()).Elem() + + if kv.ivalue == -1 { + d.setError(namespace, fmt.Errorf("invalid array index '%s'", kv.value)) + continue + } + + if d.setFieldByType(newVal, append(namespace, kv.searchValue...), 0) { + set = true + varr.Index(kv.ivalue).Set(newVal) + } + } + + if !set { + return + } + v.Set(varr) + } + + case reflect.Map: + var rd *recursiveData + + d.parseMapData() + + // no natural map support so skip directly to dm lookup + if rd = d.findAlias(string(namespace)); rd == nil { + return + } + + var existing bool + var kv key + var mp reflect.Value + var mk reflect.Value + + typ := v.Type() + + if v.IsNil() { + mp = reflect.MakeMap(typ) + } else { + existing = true + mp = v + } + + for i := 0; i < len(rd.keys); i++ { + newVal := reflect.New(typ.Elem()).Elem() + mk = reflect.New(typ.Key()).Elem() + kv = rd.keys[i] + + if err := d.getMapKey(kv.value, mk, namespace); err != nil { + d.setError(namespace, err) + continue + } + + if d.setFieldByType(newVal, append(namespace, kv.searchValue...), 0) { + set = true + mp.SetMapIndex(mk, newVal) + } + } + + if !set || existing { + return + } + + v.Set(mp) + + case reflect.Struct: + typ := v.Type() + + // if we get here then no custom time function declared so use RFC3339 by default + if typ == timeType { + + if !ok || len(arr[idx]) == 0 { + return + } + + t, err := time.Parse(time.RFC3339, arr[idx]) + if err != nil { + d.setError(namespace, err) + } + + v.Set(reflect.ValueOf(t)) + set = true + return + } + + d.parseMapData() + + // we must be recursing infinitly...but that's ok we caught it on the very first overun. + if len(namespace) > d.maxKeyLen { + return + } + + set = d.traverseStruct(v, typ, namespace) + } + return +} + +func (d *decoder) getMapKey(key string, current reflect.Value, namespace []byte) (err error) { + + v, kind := ExtractType(current) + + if d.d.customTypeFuncs != nil { + if cf, ok := d.d.customTypeFuncs[v.Type()]; ok { + + val, er := cf([]string{key}) + if er != nil { + err = er + return + } + + v.Set(reflect.ValueOf(val)) + return + } + } + + switch kind { + case reflect.Interface: + // If interface would have been set on the struct before decoding, + // say to a struct value we would not get here but kind would be struct. + v.Set(reflect.ValueOf(key)) + return + case reflect.Ptr: + newVal := reflect.New(v.Type().Elem()) + if err = d.getMapKey(key, newVal.Elem(), namespace); err == nil { + v.Set(newVal) + } + + case reflect.String: + v.SetString(key) + + case reflect.Uint, reflect.Uint64: + + u64, e := strconv.ParseUint(key, 10, 64) + if e != nil { + err = fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetUint(u64) + + case reflect.Uint8: + + u64, e := strconv.ParseUint(key, 10, 8) + if e != nil { + err = fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetUint(u64) + + case reflect.Uint16: + + u64, e := strconv.ParseUint(key, 10, 16) + if e != nil { + err = fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetUint(u64) + + case reflect.Uint32: + + u64, e := strconv.ParseUint(key, 10, 32) + if e != nil { + err = fmt.Errorf("Invalid Unsigned Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetUint(u64) + + case reflect.Int, reflect.Int64: + + i64, e := strconv.ParseInt(key, 10, 64) + if e != nil { + err = fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetInt(i64) + + case reflect.Int8: + + i64, e := strconv.ParseInt(key, 10, 8) + if e != nil { + err = fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetInt(i64) + + case reflect.Int16: + + i64, e := strconv.ParseInt(key, 10, 16) + if e != nil { + err = fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetInt(i64) + + case reflect.Int32: + + i64, e := strconv.ParseInt(key, 10, 32) + if e != nil { + err = fmt.Errorf("Invalid Integer Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetInt(i64) + + case reflect.Float32: + + f, e := strconv.ParseFloat(key, 32) + if e != nil { + err = fmt.Errorf("Invalid Float Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetFloat(f) + + case reflect.Float64: + + f, e := strconv.ParseFloat(key, 64) + if e != nil { + err = fmt.Errorf("Invalid Float Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetFloat(f) + + case reflect.Bool: + + b, e := parseBool(key) + if e != nil { + err = fmt.Errorf("Invalid Boolean Value '%s' Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + return + } + + v.SetBool(b) + + default: + err = fmt.Errorf("Unsupported Map Key '%s', Type '%v' Namespace '%s'", key, v.Type(), string(namespace)) + } + + return +} diff --git a/decoder_test.go b/decoder_test.go new file mode 100644 index 0000000..2b631a0 --- /dev/null +++ b/decoder_test.go @@ -0,0 +1,2074 @@ +package form + +import ( + "errors" + "fmt" + "net/url" + "reflect" + "strings" + "testing" + "time" + + . "github.com/go-playground/assert/v2" +) + +// NOTES: +// - Run "go test" to run tests +// - Run "gocov test | gocov report" to report on test converage by file +// - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called +// +// or +// +// -- may be a good idea to change to output path to somewherelike /tmp +// go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html +// +// +// go test -cpuprofile cpu.out +// ./validator.test -test.bench=. -test.cpuprofile=cpu.prof +// go tool pprof validator.test cpu.prof +// +// +// go test -memprofile mem.out + +func TestDecoderInt(t *testing.T) { + + type TestInt struct { + Int int + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + IntPtr *int + Int8Ptr *int8 + Int16Ptr *int16 + Int32Ptr *int32 + Int64Ptr *int64 + IntArray []int + IntPtrArray []*int + IntArrayArray [][]int + IntPtrArrayArray [][]*int + IntMap map[int]int + IntPtrMap map[*int]*int + NoURLValue int + IntNoValues int + Int8NoValues int8 + Int16NoValues int16 + Int32NoValues int32 + Int64NoValues int64 + } + + values := url.Values{ + "Int": []string{"3"}, + "Int8": []string{"3"}, + "Int16": []string{"3"}, + "Int32": []string{"3"}, + "Int64": []string{"3"}, + "IntPtr": []string{"3"}, + "Int8Ptr": []string{"3"}, + "Int16Ptr": []string{"3"}, + "Int32Ptr": []string{"3"}, + "Int64Ptr": []string{"3"}, + "IntArray": []string{"1", "2", "3"}, + "IntPtrArray[0]": []string{"1"}, + "IntPtrArray[2]": []string{"3"}, + "IntArrayArray[0][0]": []string{"1"}, + "IntArrayArray[0][2]": []string{"3"}, + "IntArrayArray[2][0]": []string{"1"}, + "IntPtrArrayArray[0][0]": []string{"1"}, + "IntPtrArrayArray[0][2]": []string{"3"}, + "IntPtrArrayArray[2][0]": []string{"1"}, + "IntMap[1]": []string{"3"}, + "IntPtrMap[1]": []string{"3"}, + } + + var test TestInt + + test.IntArray = make([]int, 4) + + decoder := NewDecoder() + errs := decoder.Decode(&test, values) + Equal(t, errs, nil) + + Equal(t, test.Int, int(3)) + Equal(t, test.Int8, int8(3)) + Equal(t, test.Int16, int16(3)) + Equal(t, test.Int32, int32(3)) + Equal(t, test.Int64, int64(3)) + + Equal(t, *test.IntPtr, int(3)) + Equal(t, *test.Int8Ptr, int8(3)) + Equal(t, *test.Int16Ptr, int16(3)) + Equal(t, *test.Int32Ptr, int32(3)) + Equal(t, *test.Int64Ptr, int64(3)) + + Equal(t, len(test.IntArray), 7) + Equal(t, test.IntArray[0], int(0)) + Equal(t, test.IntArray[1], int(0)) + Equal(t, test.IntArray[2], int(0)) + Equal(t, test.IntArray[3], int(0)) + Equal(t, test.IntArray[4], int(1)) + Equal(t, test.IntArray[5], int(2)) + Equal(t, test.IntArray[6], int(3)) + + Equal(t, len(test.IntPtrArray), 3) + Equal(t, *test.IntPtrArray[0], int(1)) + Equal(t, test.IntPtrArray[1], nil) + Equal(t, *test.IntPtrArray[2], int(3)) + + Equal(t, len(test.IntArrayArray), 3) + Equal(t, len(test.IntArrayArray[0]), 3) + Equal(t, len(test.IntArrayArray[1]), 0) + Equal(t, len(test.IntArrayArray[2]), 1) + Equal(t, test.IntArrayArray[0][0], int(1)) + Equal(t, test.IntArrayArray[0][1], int(0)) + Equal(t, test.IntArrayArray[0][2], int(3)) + Equal(t, test.IntArrayArray[2][0], int(1)) + + Equal(t, len(test.IntPtrArrayArray), 3) + Equal(t, len(test.IntPtrArrayArray[0]), 3) + Equal(t, len(test.IntPtrArrayArray[1]), 0) + Equal(t, len(test.IntPtrArrayArray[2]), 1) + Equal(t, *test.IntPtrArrayArray[0][0], int(1)) + Equal(t, test.IntPtrArrayArray[0][1], nil) + Equal(t, *test.IntPtrArrayArray[0][2], int(3)) + Equal(t, *test.IntPtrArrayArray[2][0], int(1)) + + Equal(t, len(test.IntMap), 1) + Equal(t, len(test.IntPtrMap), 1) + + v, ok := test.IntMap[1] + Equal(t, ok, true) + Equal(t, v, int(3)) + + Equal(t, test.NoURLValue, int(0)) + + Equal(t, test.IntNoValues, int(0)) + Equal(t, test.Int8NoValues, int8(0)) + Equal(t, test.Int16NoValues, int16(0)) + Equal(t, test.Int32NoValues, int32(0)) + Equal(t, test.Int64NoValues, int64(0)) +} + +func TestDecoderUint(t *testing.T) { + + type TestUint struct { + Uint uint + Uint8 uint8 + Uint16 uint16 + Uint32 uint32 + Uint64 uint64 + UintPtr *uint + Uint8Ptr *uint8 + Uint16Ptr *uint16 + Uint32Ptr *uint32 + Uint64Ptr *uint64 + UintArray []uint + UintPtrArray []*uint + UintArrayArray [][]uint + UintPtrArrayArray [][]*uint + UintMap map[uint]uint + UintPtrMap map[*uint]*uint + NoURLValue uint + UintNoValues uint + Uint8NoValues uint8 + Uint16NoValues uint16 + Uint32NoValues uint32 + Uint64NoValues uint64 + } + + values := url.Values{ + "Uint": []string{"3"}, + "Uint8": []string{"3"}, + "Uint16": []string{"3"}, + "Uint32": []string{"3"}, + "Uint64": []string{"3"}, + "UintPtr": []string{"3"}, + "Uint8Ptr": []string{"3"}, + "Uint16Ptr": []string{"3"}, + "Uint32Ptr": []string{"3"}, + "Uint64Ptr": []string{"3"}, + "UintArray": []string{"1", "2", "3"}, + "UintPtrArray[0]": []string{"1"}, + "UintPtrArray[2]": []string{"3"}, + "UintArrayArray[0][0]": []string{"1"}, + "UintArrayArray[0][2]": []string{"3"}, + "UintArrayArray[2][0]": []string{"1"}, + "UintPtrArrayArray[0][0]": []string{"1"}, + "UintPtrArrayArray[0][2]": []string{"3"}, + "UintPtrArrayArray[2][0]": []string{"1"}, + "UintMap[1]": []string{"3"}, + "UintPtrMap[1]": []string{"3"}, + } + + var test TestUint + + test.UintArray = make([]uint, 4) + + decoder := NewDecoder() + errs := decoder.Decode(&test, values) + Equal(t, errs, nil) + + Equal(t, test.Uint, uint(3)) + Equal(t, test.Uint8, uint8(3)) + Equal(t, test.Uint16, uint16(3)) + Equal(t, test.Uint32, uint32(3)) + Equal(t, test.Uint64, uint64(3)) + + Equal(t, *test.UintPtr, uint(3)) + Equal(t, *test.Uint8Ptr, uint8(3)) + Equal(t, *test.Uint16Ptr, uint16(3)) + Equal(t, *test.Uint32Ptr, uint32(3)) + Equal(t, *test.Uint64Ptr, uint64(3)) + + Equal(t, len(test.UintArray), 7) + Equal(t, test.UintArray[0], uint(0)) + Equal(t, test.UintArray[1], uint(0)) + Equal(t, test.UintArray[2], uint(0)) + Equal(t, test.UintArray[3], uint(0)) + Equal(t, test.UintArray[4], uint(1)) + Equal(t, test.UintArray[5], uint(2)) + Equal(t, test.UintArray[6], uint(3)) + + Equal(t, len(test.UintPtrArray), 3) + Equal(t, *test.UintPtrArray[0], uint(1)) + Equal(t, test.UintPtrArray[1], nil) + Equal(t, *test.UintPtrArray[2], uint(3)) + + Equal(t, len(test.UintArrayArray), 3) + Equal(t, len(test.UintArrayArray[0]), 3) + Equal(t, len(test.UintArrayArray[1]), 0) + Equal(t, len(test.UintArrayArray[2]), 1) + Equal(t, test.UintArrayArray[0][0], uint(1)) + Equal(t, test.UintArrayArray[0][1], uint(0)) + Equal(t, test.UintArrayArray[0][2], uint(3)) + Equal(t, test.UintArrayArray[2][0], uint(1)) + + Equal(t, len(test.UintPtrArrayArray), 3) + Equal(t, len(test.UintPtrArrayArray[0]), 3) + Equal(t, len(test.UintPtrArrayArray[1]), 0) + Equal(t, len(test.UintPtrArrayArray[2]), 1) + Equal(t, *test.UintPtrArrayArray[0][0], uint(1)) + Equal(t, test.UintPtrArrayArray[0][1], nil) + Equal(t, *test.UintPtrArrayArray[0][2], uint(3)) + Equal(t, *test.UintPtrArrayArray[2][0], uint(1)) + + Equal(t, len(test.UintMap), 1) + Equal(t, len(test.UintPtrMap), 1) + + v, ok := test.UintMap[1] + Equal(t, ok, true) + Equal(t, v, uint(3)) + + Equal(t, test.NoURLValue, uint(0)) + + Equal(t, test.UintNoValues, uint(0)) + Equal(t, test.Uint8NoValues, uint8(0)) + Equal(t, test.Uint16NoValues, uint16(0)) + Equal(t, test.Uint32NoValues, uint32(0)) + Equal(t, test.Uint64NoValues, uint64(0)) +} + +func TestDecoderString(t *testing.T) { + + type TestString struct { + String string + StringPtr *string + StringArray []string + StringPtrArray []*string + StringArrayArray [][]string + StringPtrArrayArray [][]*string + StringMap map[string]string + StringPtrMap map[*string]*string + NoURLValue string + } + + values := url.Values{ + "String": []string{"3"}, + "StringPtr": []string{"3"}, + "StringArray": []string{"1", "2", "3"}, + "StringPtrArray[0]": []string{"1"}, + "StringPtrArray[2]": []string{"3"}, + "StringArrayArray[0][0]": []string{"1"}, + "StringArrayArray[0][2]": []string{"3"}, + "StringArrayArray[2][0]": []string{"1"}, + "StringPtrArrayArray[0][0]": []string{"1"}, + "StringPtrArrayArray[0][2]": []string{"3"}, + "StringPtrArrayArray[2][0]": []string{"1"}, + "StringMap[1]": []string{"3"}, + "StringPtrMap[1]": []string{"3"}, + } + + var test TestString + + test.StringArray = make([]string, 4) + + decoder := NewDecoder() + errs := decoder.Decode(&test, values) + Equal(t, errs, nil) + + Equal(t, test.String, "3") + + Equal(t, *test.StringPtr, "3") + + Equal(t, len(test.StringArray), 7) + Equal(t, test.StringArray[0], "") + Equal(t, test.StringArray[1], "") + Equal(t, test.StringArray[2], "") + Equal(t, test.StringArray[3], "") + Equal(t, test.StringArray[4], "1") + Equal(t, test.StringArray[5], "2") + Equal(t, test.StringArray[6], "3") + + Equal(t, len(test.StringPtrArray), 3) + Equal(t, *test.StringPtrArray[0], "1") + Equal(t, test.StringPtrArray[1], nil) + Equal(t, *test.StringPtrArray[2], "3") + + Equal(t, len(test.StringArrayArray), 3) + Equal(t, len(test.StringArrayArray[0]), 3) + Equal(t, len(test.StringArrayArray[1]), 0) + Equal(t, len(test.StringArrayArray[2]), 1) + Equal(t, test.StringArrayArray[0][0], "1") + Equal(t, test.StringArrayArray[0][1], "") + Equal(t, test.StringArrayArray[0][2], "3") + Equal(t, test.StringArrayArray[2][0], "1") + + Equal(t, len(test.StringPtrArrayArray), 3) + Equal(t, len(test.StringPtrArrayArray[0]), 3) + Equal(t, len(test.StringPtrArrayArray[1]), 0) + Equal(t, len(test.StringPtrArrayArray[2]), 1) + Equal(t, *test.StringPtrArrayArray[0][0], "1") + Equal(t, test.StringPtrArrayArray[0][1], nil) + Equal(t, *test.StringPtrArrayArray[0][2], "3") + Equal(t, *test.StringPtrArrayArray[2][0], "1") + + Equal(t, len(test.StringMap), 1) + Equal(t, len(test.StringPtrMap), 1) + + v, ok := test.StringMap["1"] + Equal(t, ok, true) + Equal(t, v, "3") + + Equal(t, test.NoURLValue, "") +} + +func TestDecoderFloat(t *testing.T) { + + type TestFloat struct { + Float32 float32 + Float32Ptr *float32 + Float64 float64 + Float64Ptr *float64 + Float32Array []float32 + Float32PtrArray []*float32 + Float32ArrayArray [][]float32 + Float32PtrArrayArray [][]*float32 + Float32Map map[float32]float32 + Float32PtrMap map[*float32]*float32 + Float32NoValue float32 + Float64NoValue float64 + } + + values := url.Values{ + "Float32": []string{"3.3"}, + "Float32Ptr": []string{"3.3"}, + "Float64": []string{"3.3"}, + "Float64Ptr": []string{"3.3"}, + "Float32Array": []string{"1.1", "2.2", "3.3"}, + "Float32PtrArray[0]": []string{"1.1"}, + "Float32PtrArray[2]": []string{"3.3"}, + "Float32ArrayArray[0][0]": []string{"1.1"}, + "Float32ArrayArray[0][2]": []string{"3.3"}, + "Float32ArrayArray[2][0]": []string{"1.1"}, + "Float32PtrArrayArray[0][0]": []string{"1.1"}, + "Float32PtrArrayArray[0][2]": []string{"3.3"}, + "Float32PtrArrayArray[2][0]": []string{"1.1"}, + "Float32Map[1.1]": []string{"3.3"}, + "Float32PtrMap[1.1]": []string{"3.3"}, + } + + var test TestFloat + + test.Float32Array = make([]float32, 4) + + decoder := NewDecoder() + errs := decoder.Decode(&test, values) + Equal(t, errs, nil) + + Equal(t, test.Float32, float32(3.3)) + Equal(t, test.Float64, float64(3.3)) + + Equal(t, *test.Float32Ptr, float32(3.3)) + Equal(t, *test.Float64Ptr, float64(3.3)) + + Equal(t, len(test.Float32Array), 7) + Equal(t, test.Float32Array[0], float32(0.0)) + Equal(t, test.Float32Array[1], float32(0.0)) + Equal(t, test.Float32Array[2], float32(0.0)) + Equal(t, test.Float32Array[3], float32(0.0)) + Equal(t, test.Float32Array[4], float32(1.1)) + Equal(t, test.Float32Array[5], float32(2.2)) + Equal(t, test.Float32Array[6], float32(3.3)) + + Equal(t, len(test.Float32PtrArray), 3) + Equal(t, *test.Float32PtrArray[0], float32(1.1)) + Equal(t, test.Float32PtrArray[1], nil) + Equal(t, *test.Float32PtrArray[2], float32(3.3)) + + Equal(t, len(test.Float32ArrayArray), 3) + Equal(t, len(test.Float32ArrayArray[0]), 3) + Equal(t, len(test.Float32ArrayArray[1]), 0) + Equal(t, len(test.Float32ArrayArray[2]), 1) + Equal(t, test.Float32ArrayArray[0][0], float32(1.1)) + Equal(t, test.Float32ArrayArray[0][1], float32(0.0)) + Equal(t, test.Float32ArrayArray[0][2], float32(3.3)) + Equal(t, test.Float32ArrayArray[2][0], float32(1.1)) + + Equal(t, len(test.Float32PtrArrayArray), 3) + Equal(t, len(test.Float32PtrArrayArray[0]), 3) + Equal(t, len(test.Float32PtrArrayArray[1]), 0) + Equal(t, len(test.Float32PtrArrayArray[2]), 1) + Equal(t, *test.Float32PtrArrayArray[0][0], float32(1.1)) + Equal(t, test.Float32PtrArrayArray[0][1], nil) + Equal(t, *test.Float32PtrArrayArray[0][2], float32(3.3)) + Equal(t, *test.Float32PtrArrayArray[2][0], float32(1.1)) + + Equal(t, len(test.Float32Map), 1) + Equal(t, len(test.Float32PtrMap), 1) + + v, ok := test.Float32Map[float32(1.1)] + Equal(t, ok, true) + Equal(t, v, float32(3.3)) + + Equal(t, test.Float32NoValue, float32(0.0)) + Equal(t, test.Float64NoValue, float64(0.0)) +} + +func TestDecoderBool(t *testing.T) { + + type TestBool struct { + Bool bool + BoolPtr *bool + BoolPtrNil *bool + BoolPtrEmpty *bool + BoolArray []bool + BoolPtrArray []*bool + BoolArrayArray [][]bool + BoolPtrArrayArray [][]*bool + BoolMap map[bool]bool + BoolPtrMap map[*bool]*bool + NoURLValue bool + } + + values := url.Values{ + "Bool": []string{"true"}, + "BoolPtr": []string{"true"}, + "BoolPtrEmpty": []string{""}, + "BoolArray": []string{"off", "t", "on"}, + "BoolPtrArray[0]": []string{"true"}, + "BoolPtrArray[2]": []string{"T"}, + "BoolArrayArray[0][0]": []string{"TRUE"}, + "BoolArrayArray[0][2]": []string{"True"}, + "BoolArrayArray[2][0]": []string{"true"}, + "BoolPtrArrayArray[0][0]": []string{"true"}, + "BoolPtrArrayArray[0][2]": []string{"t"}, + "BoolPtrArrayArray[2][0]": []string{"1"}, + "BoolMap[true]": []string{"true"}, + "BoolPtrMap[t]": []string{"true"}, + } + + var test TestBool + + test.BoolArray = make([]bool, 4) + + decoder := NewDecoder() + errs := decoder.Decode(&test, values) + Equal(t, errs, nil) + + Equal(t, test.Bool, true) + + Equal(t, *test.BoolPtr, true) + Equal(t, test.BoolPtrNil, nil) + NotEqual(t, test.BoolPtrEmpty, nil) + Equal(t, *test.BoolPtrEmpty, false) + + Equal(t, len(test.BoolArray), 7) + Equal(t, test.BoolArray[0], false) + Equal(t, test.BoolArray[1], false) + Equal(t, test.BoolArray[2], false) + Equal(t, test.BoolArray[3], false) + Equal(t, test.BoolArray[4], false) + Equal(t, test.BoolArray[5], true) + Equal(t, test.BoolArray[6], true) + + Equal(t, len(test.BoolPtrArray), 3) + Equal(t, *test.BoolPtrArray[0], true) + Equal(t, test.BoolPtrArray[1], nil) + Equal(t, *test.BoolPtrArray[2], true) + + Equal(t, len(test.BoolArrayArray), 3) + Equal(t, len(test.BoolArrayArray[0]), 3) + Equal(t, len(test.BoolArrayArray[1]), 0) + Equal(t, len(test.BoolArrayArray[2]), 1) + Equal(t, test.BoolArrayArray[0][0], true) + Equal(t, test.BoolArrayArray[0][1], false) + Equal(t, test.BoolArrayArray[0][2], true) + Equal(t, test.BoolArrayArray[2][0], true) + + Equal(t, len(test.BoolPtrArrayArray), 3) + Equal(t, len(test.BoolPtrArrayArray[0]), 3) + Equal(t, len(test.BoolPtrArrayArray[1]), 0) + Equal(t, len(test.BoolPtrArrayArray[2]), 1) + Equal(t, *test.BoolPtrArrayArray[0][0], true) + Equal(t, test.BoolPtrArrayArray[0][1], nil) + Equal(t, *test.BoolPtrArrayArray[0][2], true) + Equal(t, *test.BoolPtrArrayArray[2][0], true) + + Equal(t, len(test.BoolMap), 1) + Equal(t, len(test.BoolPtrMap), 1) + + v, ok := test.BoolMap[true] + Equal(t, ok, true) + Equal(t, v, true) + + Equal(t, test.NoURLValue, false) +} + +func TestDecoderEqualStructMapValue(t *testing.T) { + type PhoneStruct struct { + Number string + } + + type PhoneMap map[string]string + + type TestStruct struct { + PhoneStruct PhoneStruct `form:"Phone"` + PhoneMap PhoneMap `form:"Phone"` + } + + testCases := []struct { + NamespacePrefix string + NamespaceSuffix string + Values url.Values + }{{ + NamespacePrefix: ".", + Values: url.Values{ + "Phone.Number": []string{"111"}, + "Phone[Number]": []string{"222"}, + }, + }, { + NamespacePrefix: "[", + NamespaceSuffix: "]", + Values: url.Values{ + "Phone[Number]": []string{"111"}, + }, + }} + + for _, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("Namespace_%s%s", tc.NamespacePrefix, tc.NamespaceSuffix), func(t *testing.T) { + decoder := NewDecoder() + decoder.SetNamespacePrefix(tc.NamespacePrefix) + decoder.SetNamespaceSuffix(tc.NamespaceSuffix) + + var test TestStruct + + err := decoder.Decode(&test, tc.Values) + Equal(t, err, nil) + + Equal(t, test.PhoneStruct.Number, "111") + + if tc.NamespacePrefix == "." { + Equal(t, test.PhoneMap["Number"], "222") + } else { + Equal(t, test.PhoneMap["Number"], "111") + } + }) + } +} + +func TestDecoderStruct(t *testing.T) { + + type Phone struct { + Number string + } + + type TestMapKeys struct { + MapIfaceKey map[interface{}]string + MapFloat32Key map[float32]float32 + MapFloat64Key map[float64]float64 + MapNestedInt map[int]map[int]int + MapInt8 map[int8]int8 + MapInt16 map[int16]int16 + MapInt32 map[int32]int32 + MapUint8 map[uint8]uint8 + MapUint16 map[uint16]uint16 + MapUint32 map[uint32]uint32 + } + + type TestStruct struct { + Name string `form:"name"` + Phone []Phone + PhonePtr []*Phone + NonNilPtr *Phone + Ignore string `form:"-"` + Anonymous struct { + Value string + Ignore string `form:"-"` + unexposed string + } + Time time.Time + TimePtr *time.Time + Invalid interface{} + ExistingMap map[string]string `form:"mp"` + MapNoValue map[int]int + TestMapKeys TestMapKeys + NilArray []string + TooSmallArray []string + TooSmallCapOKArray []string + ZeroLengthArray []string + TooSmallNumberedArray []string + TooSmallCapOKNumberedArray []string + BigEnoughNumberedArray []string + IfaceNonNil interface{} + IfaceInvalid interface{} + TimeMapKey map[time.Time]string + ExistingArray []string + ExistingArrayIndex []string + } + + defaultValues := url.Values{ + "name": []string{"joeybloggs"}, + "Ignore": []string{"ignore"}, + "Time": []string{"2016-01-02"}, + "TimePtr": []string{"2016-01-02"}, + "mp[key]": []string{"value"}, + "NilArray": []string{"1", "2"}, + "TooSmallArray": []string{"1", "2"}, + "TooSmallCapOKArray": []string{"1", "2"}, + "ZeroLengthArray": []string{}, + "TooSmallNumberedArray[2]": []string{"2"}, + "TooSmallCapOKNumberedArray[2]": []string{"2"}, + "BigEnoughNumberedArray[2]": []string{"1"}, + "TimeMapKey[2016-01-02]": []string{"time"}, + "ExistingArray": []string{"arr2"}, + "ExistingArrayIndex[1]": []string{"arr2"}, + } + + testCases := []struct { + NamespacePrefix string + NamespaceSuffix string + Values url.Values + }{{ + NamespacePrefix: ".", + Values: url.Values{ + "Phone[0].Number": []string{"1(111)111-1111"}, + "Phone[1].Number": []string{"9(999)999-9999"}, + "PhonePtr[0].Number": []string{"1(111)111-1111"}, + "PhonePtr[1].Number": []string{"9(999)999-9999"}, + "NonNilPtr.Number": []string{"9(999)999-9999"}, + "Anonymous.Value": []string{"Anon"}, + "TestMapKeys.MapIfaceKey[key]": []string{"3"}, + "TestMapKeys.MapFloat32Key[0.0]": []string{"3.3"}, + "TestMapKeys.MapFloat64Key[0.0]": []string{"3.3"}, + "TestMapKeys.MapNestedInt[1][2]": []string{"3"}, + "TestMapKeys.MapInt8[0]": []string{"3"}, + "TestMapKeys.MapInt16[0]": []string{"3"}, + "TestMapKeys.MapInt32[0]": []string{"3"}, + "TestMapKeys.MapUint8[0]": []string{"3"}, + "TestMapKeys.MapUint16[0]": []string{"3"}, + "TestMapKeys.MapUint32[0]": []string{"3"}, + }, + }, { + NamespacePrefix: "[", + NamespaceSuffix: "]", + Values: url.Values{ + "Phone[0][Number]": []string{"1(111)111-1111"}, + "Phone[1][Number]": []string{"9(999)999-9999"}, + "PhonePtr[0][Number]": []string{"1(111)111-1111"}, + "PhonePtr[1][Number]": []string{"9(999)999-9999"}, + "NonNilPtr[Number]": []string{"9(999)999-9999"}, + "Anonymous[Value]": []string{"Anon"}, + "TestMapKeys[MapIfaceKey][key]": []string{"3"}, + "TestMapKeys[MapFloat32Key][0.0]": []string{"3.3"}, + "TestMapKeys[MapFloat64Key][0.0]": []string{"3.3"}, + "TestMapKeys[MapNestedInt][1][2]": []string{"3"}, + "TestMapKeys[MapInt8][0]": []string{"3"}, + "TestMapKeys[MapInt16][0]": []string{"3"}, + "TestMapKeys[MapInt32][0]": []string{"3"}, + "TestMapKeys[MapUint8][0]": []string{"3"}, + "TestMapKeys[MapUint16][0]": []string{"3"}, + "TestMapKeys[MapUint32][0]": []string{"3"}, + }, + }} + + for _, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("Namespace_%s%s", tc.NamespacePrefix, tc.NamespaceSuffix), func(t *testing.T) { + decoder := NewDecoder() + decoder.SetNamespacePrefix(tc.NamespacePrefix) + decoder.SetNamespaceSuffix(tc.NamespaceSuffix) + + values := url.Values{} + + for key, vals := range defaultValues { + values[key] = vals + } + for key, vals := range tc.Values { + values[key] = vals + } + + decoder.SetTagName("form") + decoder.RegisterCustomTypeFunc(func(vals []string) (interface{}, error) { + return time.Parse("2006-01-02", vals[0]) + }, time.Time{}) + + var test TestStruct + test.ExistingMap = map[string]string{"existingkey": "existingvalue"} + test.NonNilPtr = new(Phone) + test.IfaceNonNil = new(Phone) + test.IfaceInvalid = nil + test.TooSmallArray = []string{"0"} + test.TooSmallCapOKArray = make([]string, 0, 10) + test.TooSmallNumberedArray = []string{"0"} + test.TooSmallCapOKNumberedArray = make([]string, 0, 10) + test.BigEnoughNumberedArray = make([]string, 3, 10) + test.ExistingArray = []string{"arr1"} + test.ExistingArrayIndex = []string{"arr1"} + + errs := decoder.Decode(&test, values) + Equal(t, errs, nil) + + Equal(t, test.Name, "joeybloggs") + Equal(t, test.Ignore, "") + Equal(t, len(test.Phone), 2) + Equal(t, test.Phone[0].Number, "1(111)111-1111") + Equal(t, test.Phone[1].Number, "9(999)999-9999") + Equal(t, len(test.PhonePtr), 2) + Equal(t, test.PhonePtr[0].Number, "1(111)111-1111") + Equal(t, test.PhonePtr[1].Number, "9(999)999-9999") + Equal(t, test.NonNilPtr.Number, "9(999)999-9999") + Equal(t, test.Anonymous.Value, "Anon") + Equal(t, len(test.ExistingMap), 2) + Equal(t, test.ExistingMap["existingkey"], "existingvalue") + Equal(t, test.ExistingMap["key"], "value") + Equal(t, len(test.NilArray), 2) + Equal(t, test.NilArray[0], "1") + Equal(t, test.NilArray[1], "2") + Equal(t, len(test.TooSmallArray), 3) + Equal(t, test.TooSmallArray[0], "0") + Equal(t, test.TooSmallArray[1], "1") + Equal(t, test.TooSmallArray[2], "2") + Equal(t, len(test.ZeroLengthArray), 0) + Equal(t, len(test.TooSmallNumberedArray), 3) + Equal(t, test.TooSmallNumberedArray[0], "0") + Equal(t, test.TooSmallNumberedArray[1], "") + Equal(t, test.TooSmallNumberedArray[2], "2") + Equal(t, len(test.BigEnoughNumberedArray), 3) + Equal(t, cap(test.BigEnoughNumberedArray), 10) + Equal(t, test.BigEnoughNumberedArray[0], "") + Equal(t, test.BigEnoughNumberedArray[1], "") + Equal(t, test.BigEnoughNumberedArray[2], "1") + Equal(t, len(test.TooSmallCapOKArray), 2) + Equal(t, cap(test.TooSmallCapOKArray), 10) + Equal(t, test.TooSmallCapOKArray[0], "1") + Equal(t, test.TooSmallCapOKArray[1], "2") + Equal(t, len(test.TooSmallCapOKNumberedArray), 3) + Equal(t, cap(test.TooSmallCapOKNumberedArray), 10) + Equal(t, test.TooSmallCapOKNumberedArray[0], "") + Equal(t, test.TooSmallCapOKNumberedArray[1], "") + Equal(t, test.TooSmallCapOKNumberedArray[2], "2") + + Equal(t, len(test.ExistingArray), 2) + Equal(t, test.ExistingArray[0], "arr1") + Equal(t, test.ExistingArray[1], "arr2") + + Equal(t, len(test.ExistingArrayIndex), 2) + Equal(t, test.ExistingArrayIndex[0], "arr1") + Equal(t, test.ExistingArrayIndex[1], "arr2") + + tm, _ := time.Parse("2006-01-02", "2016-01-02") + Equal(t, test.Time.Equal(tm), true) + Equal(t, test.TimePtr.Equal(tm), true) + + NotEqual(t, test.TimeMapKey, nil) + Equal(t, len(test.TimeMapKey), 1) + + _, ok := test.TimeMapKey[tm] + Equal(t, ok, true) + + s := struct { + Value string + Ignore string `form:"-"` + unexposed string + }{} + + errs = decoder.Decode(&s, defaultValues) + Equal(t, errs, nil) + Equal(t, s.Value, "") + Equal(t, s.Ignore, "") + Equal(t, s.unexposed, "") + + Equal(t, test.TestMapKeys.MapIfaceKey["key"], "3") + Equal(t, test.TestMapKeys.MapFloat32Key[float32(0.0)], float32(3.3)) + Equal(t, test.TestMapKeys.MapFloat64Key[float64(0.0)], float64(3.3)) + + Equal(t, test.TestMapKeys.MapInt8[int8(0)], int8(3)) + Equal(t, test.TestMapKeys.MapInt16[int16(0)], int16(3)) + Equal(t, test.TestMapKeys.MapInt32[int32(0)], int32(3)) + + Equal(t, test.TestMapKeys.MapUint8[uint8(0)], uint8(3)) + Equal(t, test.TestMapKeys.MapUint16[uint16(0)], uint16(3)) + Equal(t, test.TestMapKeys.MapUint32[uint32(0)], uint32(3)) + + Equal(t, len(test.TestMapKeys.MapNestedInt), 1) + Equal(t, len(test.TestMapKeys.MapNestedInt[1]), 1) + Equal(t, test.TestMapKeys.MapNestedInt[1][2], 3) + }) + } +} + +func TestDecoderNativeTime(t *testing.T) { + + type TestError struct { + Time time.Time + TimeNoValue time.Time + TimePtr *time.Time + } + + values := url.Values{ + "Time": []string{"2006-01-02T15:04:05Z"}, + "TimeNoValue": []string{""}, + "TimePtr": []string{"2006-01-02T15:04:05Z"}, + } + + var test TestError + + decoder := NewDecoder() + + errs := decoder.Decode(&test, values) + Equal(t, errs, nil) + + tm, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + Equal(t, test.Time.Equal(tm), true) + Equal(t, test.TimeNoValue.Equal(tm), false) + + NotEqual(t, test.TimePtr, nil) + Equal(t, (*test.TimePtr).Equal(tm), true) +} + +func TestDecoderErrors(t *testing.T) { + + type TestError struct { + Bool bool `form:"bool"` + Int int + Int8 int8 + Int16 int16 + Int32 int32 + Uint uint + Uint8 uint8 + Uint16 uint16 + Uint32 uint32 + Float32 float32 + Float64 float64 + String string + Time time.Time + MapBadIntKey map[int]int + MapBadInt8Key map[int8]int8 + MapBadInt16Key map[int16]int16 + MapBadInt32Key map[int32]int32 + MapBadUintKey map[uint]uint + MapBadUint8Key map[uint8]uint8 + MapBadUint16Key map[uint16]uint16 + MapBadUint32Key map[uint32]uint32 + MapBadFloat32Key map[float32]float32 + MapBadFloat64Key map[float64]float64 + MapBadBoolKey map[bool]bool + MapBadKeyType map[complex64]int + BadArrayValue []int + BadMapKey map[time.Time]string + OverflowNilArray []int + OverFlowExistingArray []int + BadArrayIndex []int + } + + values := url.Values{ + "bool": []string{"uh-huh"}, + "Int": []string{"bad"}, + "Int8": []string{"bad"}, + "Int16": []string{"bad"}, + "Int32": []string{"bad"}, + "Uint": []string{"bad"}, + "Uint8": []string{"bad"}, + "Uint16": []string{"bad"}, + "Uint32": []string{"bad"}, + "Float32": []string{"bad"}, + "Float64": []string{"bad"}, + "String": []string{"str bad return val"}, + "Time": []string{"bad"}, + "MapBadIntKey[key]": []string{"1"}, + "MapBadInt8Key[key]": []string{"1"}, + "MapBadInt16Key[key]": []string{"1"}, + "MapBadInt32Key[key]": []string{"1"}, + "MapBadUintKey[key]": []string{"1"}, + "MapBadUint8Key[key]": []string{"1"}, + "MapBadUint16Key[key]": []string{"1"}, + "MapBadUint32Key[key]": []string{"1"}, + "MapBadFloat32Key[key]": []string{"1.1"}, + "MapBadFloat64Key[key]": []string{"1.1"}, + "MapBadBoolKey[uh-huh]": []string{"true"}, + "MapBadKeyType[1.4]": []string{"5"}, + "BadArrayValue[0]": []string{"badintval"}, + "BadMapKey[badtime]": []string{"badtime"}, + "OverflowNilArray[999]": []string{"idx 1000"}, + "OverFlowExistingArray[999]": []string{"idx 1000"}, + "BadArrayIndex[bad index]": []string{"bad idx"}, + } + + testCases := []struct { + NamespacePrefix string + NamespaceSuffix string + }{{ + NamespacePrefix: ".", + }, { + NamespacePrefix: "[", + NamespaceSuffix: "]", + }} + + for _, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("Namespace_%s%s", tc.NamespacePrefix, tc.NamespaceSuffix), func(t *testing.T) { + decoder := NewDecoder() + decoder.SetNamespacePrefix(tc.NamespacePrefix) + decoder.SetNamespaceSuffix(tc.NamespaceSuffix) + + decoder.SetMaxArraySize(4) + decoder.RegisterCustomTypeFunc(func(vals []string) (interface{}, error) { + return nil, errors.New("Bad Type Conversion") + }, "") + + test := TestError{ + OverFlowExistingArray: make([]int, 2), + } + + errs := decoder.Decode(&test, values) + NotEqual(t, errs, nil) + + e := errs.Error() + NotEqual(t, e, "") + + err := errs.(DecodeErrors) + Equal(t, len(err), 30) + + k := err["bool"] + Equal(t, k.Error(), "Invalid Boolean Value 'uh-huh' Type 'bool' Namespace 'bool'") + + k = err["Int"] + Equal(t, k.Error(), "Invalid Integer Value 'bad' Type 'int' Namespace 'Int'") + + k = err["Int8"] + Equal(t, k.Error(), "Invalid Integer Value 'bad' Type 'int8' Namespace 'Int8'") + + k = err["Int16"] + Equal(t, k.Error(), "Invalid Integer Value 'bad' Type 'int16' Namespace 'Int16'") + + k = err["Int32"] + Equal(t, k.Error(), "Invalid Integer Value 'bad' Type 'int32' Namespace 'Int32'") + + k = err["Uint"] + Equal(t, k.Error(), "Invalid Unsigned Integer Value 'bad' Type 'uint' Namespace 'Uint'") + + k = err["Uint8"] + Equal(t, k.Error(), "Invalid Unsigned Integer Value 'bad' Type 'uint8' Namespace 'Uint8'") + + k = err["Uint16"] + Equal(t, k.Error(), "Invalid Unsigned Integer Value 'bad' Type 'uint16' Namespace 'Uint16'") + + k = err["Uint32"] + Equal(t, k.Error(), "Invalid Unsigned Integer Value 'bad' Type 'uint32' Namespace 'Uint32'") + + k = err["Float32"] + Equal(t, k.Error(), "Invalid Float Value 'bad' Type 'float32' Namespace 'Float32'") + + k = err["Float64"] + Equal(t, k.Error(), "Invalid Float Value 'bad' Type 'float64' Namespace 'Float64'") + + k = err["String"] + Equal(t, k.Error(), "Bad Type Conversion") + + k = err["Time"] + Equal(t, k.Error(), "parsing time \"bad\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"bad\" as \"2006\"") + + k = err["MapBadIntKey"] + Equal(t, k.Error(), "Invalid Integer Value 'key' Type 'int' Namespace 'MapBadIntKey'") + + k = err["MapBadInt8Key"] + Equal(t, k.Error(), "Invalid Integer Value 'key' Type 'int8' Namespace 'MapBadInt8Key'") + + k = err["MapBadInt16Key"] + Equal(t, k.Error(), "Invalid Integer Value 'key' Type 'int16' Namespace 'MapBadInt16Key'") + + k = err["MapBadInt32Key"] + Equal(t, k.Error(), "Invalid Integer Value 'key' Type 'int32' Namespace 'MapBadInt32Key'") + + k = err["MapBadUintKey"] + Equal(t, k.Error(), "Invalid Unsigned Integer Value 'key' Type 'uint' Namespace 'MapBadUintKey'") + + k = err["MapBadUint8Key"] + Equal(t, k.Error(), "Invalid Unsigned Integer Value 'key' Type 'uint8' Namespace 'MapBadUint8Key'") + + k = err["MapBadUint16Key"] + Equal(t, k.Error(), "Invalid Unsigned Integer Value 'key' Type 'uint16' Namespace 'MapBadUint16Key'") + + k = err["MapBadUint32Key"] + Equal(t, k.Error(), "Invalid Unsigned Integer Value 'key' Type 'uint32' Namespace 'MapBadUint32Key'") + + k = err["MapBadFloat32Key"] + Equal(t, k.Error(), "Invalid Float Value 'key' Type 'float32' Namespace 'MapBadFloat32Key'") + + k = err["MapBadFloat64Key"] + Equal(t, k.Error(), "Invalid Float Value 'key' Type 'float64' Namespace 'MapBadFloat64Key'") + + k = err["MapBadBoolKey"] + Equal(t, k.Error(), "Invalid Boolean Value 'uh-huh' Type 'bool' Namespace 'MapBadBoolKey'") + + k = err["MapBadKeyType"] + Equal(t, k.Error(), "Unsupported Map Key '1.4', Type 'complex64' Namespace 'MapBadKeyType'") + + k = err["BadArrayValue[0]"] + Equal(t, k.Error(), "Invalid Integer Value 'badintval' Type 'int' Namespace 'BadArrayValue[0]'") + + k = err["OverflowNilArray"] + Equal(t, k.Error(), "Array size of '1000' is larger than the maximum currently set on the decoder of '4'. To increase this limit please see, SetMaxArraySize(size uint)") + + k = err["OverFlowExistingArray"] + Equal(t, k.Error(), "Array size of '1000' is larger than the maximum currently set on the decoder of '4'. To increase this limit please see, SetMaxArraySize(size uint)") + + k = err["BadArrayIndex"] + Equal(t, k.Error(), "invalid slice index 'bad index'") + + type TestError2 struct { + BadMapKey map[time.Time]string + } + + values2 := url.Values{ + "BadMapKey[badtime]": []string{"badtime"}, + } + + var test2 TestError2 + decoder2 := NewDecoder() + decoder2.RegisterCustomTypeFunc(func(vals []string) (interface{}, error) { + return time.Parse("2006-01-02", vals[0]) + }, time.Time{}) + + errs = decoder2.Decode(&test2, values2) + NotEqual(t, errs, nil) + + e = errs.Error() + NotEqual(t, e, "") + + k = err["BadMapKey"] + Equal(t, k.Error(), "Unsupported Map Key 'badtime', Type 'time.Time' Namespace 'BadMapKey'") + }) + } +} + +func TestDecodeAllTypes(t *testing.T) { + + values := url.Values{ + "": []string{"3"}, + } + + decoder := NewDecoder() + + var i int + + errs := decoder.Decode(&i, values) + Equal(t, errs, nil) + Equal(t, i, 3) + + var i8 int + + errs = decoder.Decode(&i8, values) + Equal(t, errs, nil) + Equal(t, i8, 3) + + var i16 int + + errs = decoder.Decode(&i16, values) + Equal(t, errs, nil) + Equal(t, i16, 3) + + var i32 int + + errs = decoder.Decode(&i32, values) + Equal(t, errs, nil) + Equal(t, i32, 3) + + var i64 int + + errs = decoder.Decode(&i64, values) + Equal(t, errs, nil) + Equal(t, i64, 3) + + var ui int + + errs = decoder.Decode(&ui, values) + Equal(t, errs, nil) + Equal(t, ui, 3) + + var ui8 int + + errs = decoder.Decode(&ui8, values) + Equal(t, errs, nil) + Equal(t, ui8, 3) + + var ui16 int + + errs = decoder.Decode(&ui16, values) + Equal(t, errs, nil) + Equal(t, ui16, 3) + + var ui32 int + + errs = decoder.Decode(&ui32, values) + Equal(t, errs, nil) + Equal(t, ui32, 3) + + var ui64 int + + errs = decoder.Decode(&ui64, values) + Equal(t, errs, nil) + Equal(t, ui64, 3) + + values = url.Values{ + "": []string{"3.4"}, + } + + var f32 float32 + + errs = decoder.Decode(&f32, values) + Equal(t, errs, nil) + Equal(t, f32, float32(3.4)) + + var f64 float64 + + errs = decoder.Decode(&f64, values) + Equal(t, errs, nil) + Equal(t, f64, float64(3.4)) + + values = url.Values{ + "": []string{"true"}, + } + + var b bool + + errs = decoder.Decode(&b, values) + Equal(t, errs, nil) + Equal(t, b, true) + + tm, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + + values = url.Values{ + "": []string{"2006-01-02T15:04:05Z"}, + } + + var dt time.Time + + errs = decoder.Decode(&dt, values) + + Equal(t, errs, nil) + Equal(t, dt, tm) + + values = url.Values{ + "": []string{"arr1", "arr2"}, + } + + // basic array + var arr []string + + errs = decoder.Decode(&arr, values) + Equal(t, errs, nil) + Equal(t, len(arr), 2) + Equal(t, arr[0], "arr1") + Equal(t, arr[1], "arr2") + + // pre-populated array + + // fmt.Println("Decoding...") + errs = decoder.Decode(&arr, values) + Equal(t, errs, nil) + Equal(t, len(arr), 4) + Equal(t, arr[0], "arr1") + Equal(t, arr[1], "arr2") + Equal(t, arr[2], "arr1") + Equal(t, arr[3], "arr2") + + // basic array Ptr + var arrPtr []*string + + errs = decoder.Decode(&arrPtr, values) + Equal(t, errs, nil) + Equal(t, len(arrPtr), 2) + Equal(t, *arrPtr[0], "arr1") + Equal(t, *arrPtr[1], "arr2") + + // pre-populated array Ptr + + // fmt.Println("Decoding...") + errs = decoder.Decode(&arrPtr, values) + Equal(t, errs, nil) + Equal(t, len(arr), 4) + Equal(t, *arrPtr[0], "arr1") + Equal(t, *arrPtr[1], "arr2") + Equal(t, *arrPtr[2], "arr1") + Equal(t, *arrPtr[3], "arr2") + + // indexed array + + values = url.Values{ + "[0]": []string{"newVal1"}, + "[1]": []string{"newVal2"}, + } + + errs = decoder.Decode(&arr, values) + Equal(t, errs, nil) + Equal(t, len(arr), 4) + Equal(t, arr[0], "newVal1") + Equal(t, arr[1], "newVal2") + Equal(t, arr[2], "arr1") + Equal(t, arr[3], "arr2") + + values = url.Values{ + "[key1]": []string{"val1"}, + "[key2]": []string{"val2"}, + } + + // basic map + var m map[string]string + + errs = decoder.Decode(&m, values) + Equal(t, errs, nil) + Equal(t, len(m), 2) + Equal(t, m["key1"], "val1") + Equal(t, m["key2"], "val2") + + // existing map + + errs = decoder.Decode(&m, values) + Equal(t, errs, nil) + Equal(t, len(m), 2) + Equal(t, m["key1"], "val1") + Equal(t, m["key2"], "val2") + + // basic map, adding more keys + + values = url.Values{ + "[key3]": []string{"val3"}, + } + errs = decoder.Decode(&m, values) + Equal(t, errs, nil) + Equal(t, len(m), 3) + Equal(t, m["key3"], "val3") + + // array of struct + + type Phone struct { + Number string + Label string + } + + values = url.Values{ + "[0].Number": []string{"999"}, + "[1].Label": []string{"label2"}, + "[1].Number": []string{"111"}, + "[0].Label": []string{"label1"}, + } + + var phones []Phone + + errs = decoder.Decode(&phones, values) + Equal(t, errs, nil) + Equal(t, len(phones), 2) + Equal(t, phones[0].Number, "999") + Equal(t, phones[0].Label, "label1") + Equal(t, phones[1].Number, "111") + Equal(t, phones[1].Label, "label2") +} + +func TestDecoderPanicsAndBadValues(t *testing.T) { + + type Phone struct { + Number string + } + + type TestError struct { + Phone []Phone + Phone2 []Phone + Phone3 []Phone + } + + values := url.Values{ + "Phone[0.Number": []string{"1(111)111-1111"}, + } + + var test TestError + + decoder := NewDecoder() + + PanicMatches(t, func() { _ = decoder.Decode(&test, values) }, "Invalid formatting for key 'Phone[0.Number' missing ']' bracket") + + i := 1 + err := decoder.Decode(i, values) + NotEqual(t, err, nil) + + _, ok := err.(*InvalidDecoderError) + Equal(t, ok, true) + Equal(t, err.Error(), "form: Decode(non-pointer int)") + + err = decoder.Decode(nil, values) + NotEqual(t, err, nil) + + _, ok = err.(*InvalidDecoderError) + Equal(t, ok, true) + Equal(t, err.Error(), "form: Decode(nil)") + + var ts *TestError + + err = decoder.Decode(ts, values) + NotEqual(t, err, nil) + + _, ok = err.(*InvalidDecoderError) + Equal(t, ok, true) + Equal(t, err.Error(), "form: Decode(nil *form.TestError)") + + values = url.Values{ + "Phone0].Number": []string{"1(111)111-1111"}, + } + + PanicMatches(t, func() { _ = decoder.Decode(&test, values) }, "Invalid formatting for key 'Phone0].Number' missing '[' bracket") + + values = url.Values{ + "Phone[[0.Number": []string{"1(111)111-1111"}, + } + + PanicMatches(t, func() { _ = decoder.Decode(&test, values) }, "Invalid formatting for key 'Phone[[0.Number' missing ']' bracket") + + values = url.Values{ + "Phone0]].Number": []string{"1(111)111-1111"}, + } + + PanicMatches(t, func() { _ = decoder.Decode(&test, values) }, "Invalid formatting for key 'Phone0]].Number' missing '[' bracket") +} + +func TestDecoderMapKeys(t *testing.T) { + + type TestMapKeys struct { + MapIfaceKey map[interface{}]string + MapFloat32Key map[float32]float32 + MapFloat64Key map[float64]float64 + MapNestedInt map[int]map[int]int + MapInt8 map[int8]int8 + MapInt16 map[int16]int16 + MapInt32 map[int32]int32 + MapUint8 map[uint8]uint8 + MapUint16 map[uint16]uint16 + MapUint32 map[uint32]uint32 + } + + values := url.Values{ + "MapIfaceKey[key]": []string{"3"}, + "MapFloat32Key[0.0]": []string{"3.3"}, + "MapFloat64Key[0.0]": []string{"3.3"}, + "MapNestedInt[1][2]": []string{"3"}, + "MapInt8[0]": []string{"3"}, + "MapInt16[0]": []string{"3"}, + "MapInt32[0]": []string{"3"}, + "MapUint8[0]": []string{"3"}, + "MapUint16[0]": []string{"3"}, + "MapUint32[0]": []string{"3"}, + } + + var test TestMapKeys + + testCases := []struct { + NamespacePrefix string + NamespaceSuffix string + }{ + { + NamespacePrefix: ".", + }, + { + NamespacePrefix: "[", + NamespaceSuffix: "]", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("Namespace_%s%s", tc.NamespacePrefix, tc.NamespaceSuffix), func(t *testing.T) { + decoder := NewDecoder() + decoder.SetNamespacePrefix(tc.NamespacePrefix) + decoder.SetNamespaceSuffix(tc.NamespaceSuffix) + + errs := decoder.Decode(&test, values) + Equal(t, errs, nil) + + Equal(t, test.MapIfaceKey["key"], "3") + Equal(t, test.MapFloat32Key[float32(0.0)], float32(3.3)) + Equal(t, test.MapFloat64Key[float64(0.0)], float64(3.3)) + + Equal(t, test.MapInt8[int8(0)], int8(3)) + Equal(t, test.MapInt16[int16(0)], int16(3)) + Equal(t, test.MapInt32[int32(0)], int32(3)) + + Equal(t, test.MapUint8[uint8(0)], uint8(3)) + Equal(t, test.MapUint16[uint16(0)], uint16(3)) + Equal(t, test.MapUint32[uint32(0)], uint32(3)) + + Equal(t, len(test.MapNestedInt), 1) + Equal(t, len(test.MapNestedInt[1]), 1) + Equal(t, test.MapNestedInt[1][2], 3) + }) + } +} + +func TestDecoderStructRecursion(t *testing.T) { + + type Nested struct { + Value string + Nested *Nested + } + + type TestRecursive struct { + Nested Nested + NestedPtr *Nested + NestedTwo Nested + } + + defaultValues := url.Values{ + "Value": []string{"value"}, + } + + testCases := []struct { + Values url.Values + NamespacePrefix string + NamespaceSuffix string + }{{ + NamespacePrefix: ".", + Values: url.Values{ + "Nested.Value": []string{"value"}, + "NestedPtr.Value": []string{"value"}, + "NestedTwo.Nested.Value": []string{"value"}, + }, + }, { + NamespacePrefix: "[", + NamespaceSuffix: "]", + Values: url.Values{ + "Nested[Value]": []string{"value"}, + "NestedPtr[Value]": []string{"value"}, + "NestedTwo[Nested][Value]": []string{"value"}, + }, + }} + + for _, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("Namespace_%s%s", tc.NamespacePrefix, tc.NamespaceSuffix), func(t *testing.T) { + values := url.Values{} + + for key, vals := range defaultValues { + values[key] = vals + } + for key, vals := range tc.Values { + values[key] = vals + } + + decoder := NewDecoder() + decoder.SetNamespacePrefix(tc.NamespacePrefix) + decoder.SetNamespaceSuffix(tc.NamespaceSuffix) + + var test TestRecursive + + errs := decoder.Decode(&test, values) + Equal(t, errs, nil) + + Equal(t, test.Nested.Value, "value") + Equal(t, test.NestedPtr.Value, "value") + Equal(t, test.Nested.Nested, nil) + Equal(t, test.NestedTwo.Nested.Value, "value") + }) + } + +} + +func TestDecoderFormDecode(t *testing.T) { + + type Struct2 struct { + Foo string + Bar string + } + + type Struct2Wrapper struct { + InnerSlice []Struct2 + } + + sliceValues := map[string][]string{ + "InnerSlice[0].Foo": {"foo-is-set"}, + } + + singleValues := map[string][]string{ + "Foo": {"foo-is-set"}, + } + + fd := NewDecoder() + + dst := Struct2Wrapper{} + err := fd.Decode(&dst, sliceValues) + Equal(t, err, nil) + NotEqual(t, dst.InnerSlice, nil) + Equal(t, dst.InnerSlice[0].Foo, "foo-is-set") + + dst2 := Struct2{} + err = fd.Decode(&dst2, singleValues) + Equal(t, err, nil) + Equal(t, dst2.Foo, "foo-is-set") +} + +func TestDecoderArrayKeysSort(t *testing.T) { + + type Struct struct { + Array []int + } + + values := map[string][]string{ + + "Array[2]": {"2"}, + "Array[10]": {"10"}, + } + + var test Struct + + d := NewDecoder() + + err := d.Decode(&test, values) + Equal(t, err, nil) + + Equal(t, len(test.Array), 11) + Equal(t, test.Array[2], int(2)) + Equal(t, test.Array[10], int(10)) +} + +func TestDecoderIncreasingKeys(t *testing.T) { + + type Struct struct { + Array []int + } + + values := map[string][]string{ + "Array[2]": {"2"}, + } + + var test Struct + + d := NewDecoder() + + err := d.Decode(&test, values) + Equal(t, err, nil) + + Equal(t, len(test.Array), 3) + Equal(t, test.Array[2], int(2)) + + values["Array[10]"] = []string{"10"} + + var test2 Struct + + err = d.Decode(&test2, values) + Equal(t, err, nil) + + Equal(t, len(test2.Array), 11) + Equal(t, test2.Array[2], int(2)) + Equal(t, test2.Array[10], int(10)) +} + +func TestDecoderInterface(t *testing.T) { + + var iface interface{} + + d := NewDecoder() + + values := map[string][]string{ + "": {"2"}, + } + + var i int + + iface = &i + + err := d.Decode(iface, values) + Equal(t, err, nil) + Equal(t, i, 2) + + iface = i + + err = d.Decode(iface, values) + NotEqual(t, err, nil) + + _, ok := err.(*InvalidDecoderError) + Equal(t, ok, true) + Equal(t, err.Error(), "form: Decode(non-pointer int)") + + values = map[string][]string{ + "Value": {"testVal"}, + } + + type test struct { + Value string + } + + var tst test + + iface = &tst + + err = d.Decode(iface, values) + Equal(t, err, nil) + Equal(t, tst.Value, "testVal") + + iface = tst + + err = d.Decode(iface, values) + NotEqual(t, err, nil) + + _, ok = err.(*InvalidDecoderError) + Equal(t, ok, true) + Equal(t, err.Error(), "form: Decode(non-pointer form.test)") +} + +func TestDecoderPointerToPointer(t *testing.T) { + + values := map[string][]string{ + "Value": {"testVal"}, + } + + type Test struct { + Value string + } + + var tst *Test + + d := NewDecoder() + err := d.Decode(&tst, values) + Equal(t, err, nil) + Equal(t, tst.Value, "testVal") +} + +func TestDecoderExplicit(t *testing.T) { + + type Test struct { + Name string `form:"Name"` + Age int + } + + values := map[string][]string{ + "Name": {"Joeybloggs"}, + "Age": {"3"}, + } + + var test Test + + d := NewDecoder() + d.SetMode(ModeExplicit) + + err := d.Decode(&test, values) + Equal(t, err, nil) + Equal(t, test.Name, "Joeybloggs") + Equal(t, test.Age, 0) +} + +func TestDecoderStructWithJSONTag(t *testing.T) { + type Test struct { + Name string `json:"name,omitempty"` + Age int `json:",omitempty"` + } + + values := map[string][]string{ + "name": {"Joeybloggs"}, + "Age": {"3"}, + } + + var test Test + + d := NewDecoder() + d.SetTagName("json") + + err := d.Decode(&test, values) + Equal(t, err, nil) + Equal(t, test.Name, "Joeybloggs") + Equal(t, test.Age, int(3)) +} + +func TestDecoderRegisterTagNameFunc(t *testing.T) { + + type Test struct { + Value string `json:"val,omitempty"` + Ignore string `json:"-"` + } + + values := url.Values{ + "val": []string{"joeybloggs"}, + "Ignore": []string{"ignore"}, + } + + var test Test + + decoder := NewDecoder() + decoder.RegisterTagNameFunc(func(fld reflect.StructField) string { + name := fld.Tag.Get("json") + + if commaIndex := strings.Index(name, ","); commaIndex != -1 { + name = name[:commaIndex] + } + + return name + }) + + err := decoder.Decode(&test, values) + Equal(t, err, nil) + Equal(t, test.Value, "joeybloggs") + Equal(t, test.Ignore, "") +} + +func TestDecoderEmbedModes(t *testing.T) { + + type A struct { + Field string + } + + type B struct { + A + Field string + } + + var b B + + decoder := NewDecoder() + + values := url.Values{ + "Field": []string{"Value"}, + } + + err := decoder.Decode(&b, values) + Equal(t, err, nil) + Equal(t, b.Field, "Value") + Equal(t, b.A.Field, "Value") + + values = url.Values{ + "Field": []string{"B Val"}, + "A.Field": []string{"A Val"}, + } + + err = decoder.Decode(&b, values) + Equal(t, err, nil) + Equal(t, b.Field, "B Val") + Equal(t, b.A.Field, "A Val") +} + +func TestInterfaceDecoding(t *testing.T) { + + type Test struct { + Iface interface{} + } + + var b Test + + values := url.Values{ + "Iface": []string{"1"}, + } + + decoder := NewDecoder() + err := decoder.Decode(&b, values) + Equal(t, err, nil) + Equal(t, b.Iface, "1") +} + +func TestDecodeArrayBug(t *testing.T) { + var data struct { + A [2]string + B [2]string + C [2]string + D [3]string + E [3]string + F [3]string + G [3]string + } + decoder := NewDecoder() + err := decoder.Decode(&data, url.Values{ + // Mixed types + "A": {"10"}, + "A[1]": {"20"}, + // overflow + "B": {"10", "20", "30"}, + "B[1]": {"31", "10", "20"}, + "B[2]": {"40"}, + // invalid array index + "C[q]": {""}, + // index and mix tests + "D": {"10"}, + "E": {"10", "20"}, + "F": {"10", "", "20"}, + "G": {"10"}, + "G[2]": {"20"}, + }) + NotEqual(t, err, nil) + Equal(t, err.Error(), "Field Namespace:C ERROR:invalid array index 'q'") + Equal(t, data.A[0], "10") + Equal(t, data.A[1], "20") + Equal(t, data.B[0], "10") + Equal(t, data.B[1], "31") + Equal(t, data.C[0], "") + Equal(t, data.C[1], "") + Equal(t, data.D[0], "10") + Equal(t, data.D[1], "") + Equal(t, data.D[2], "") + Equal(t, data.E[0], "10") + Equal(t, data.E[1], "20") + Equal(t, data.E[2], "") + Equal(t, data.F[0], "10") + Equal(t, data.F[1], "") + Equal(t, data.F[2], "20") + Equal(t, data.G[0], "10") + Equal(t, data.G[1], "") + Equal(t, data.G[2], "20") +} + +func TestDecoder_RegisterCustomTypeFuncOnSlice(t *testing.T) { + type customString string + + type TestStruct struct { + Slice []customString `form:"slice"` + } + + d := NewDecoder() + d.RegisterCustomTypeFunc(func(vals []string) (i interface{}, e error) { + custom := make([]customString, 0, len(vals)) + for i := 0; i < len(vals); i++ { + custom = append(custom, customString("custom"+vals[i])) + } + return custom, nil + }, []customString{}) + + var v TestStruct + err := d.Decode(&v, url.Values{"slice": []string{"v1", "v2"}}) + Equal(t, err, nil) + Equal(t, v.Slice, []customString{"customv1", "customv2"}) +} + +func TestDecoder_RegisterCustomTypeFunc(t *testing.T) { + type customString string + + type TestStruct struct { + Slice []customString `form:"slice"` + } + + d := NewDecoder() + d.RegisterCustomTypeFunc(func(vals []string) (i interface{}, e error) { + return customString("custom" + vals[0]), nil + }, customString("")) + + var v TestStruct + err := d.Decode(&v, url.Values{"slice": []string{"v1", "v2"}}) + Equal(t, err, nil) + + Equal(t, v.Slice, []customString{"customv1", "customv2"}) +} + +func TestDecoder_EmptyArrayString(t *testing.T) { + type T1 struct { + F1 string `form:"F1"` + } + in := url.Values{ + "F1": []string{}, + } + + v := new(T1) + + d := NewDecoder() + err := d.Decode(v, in) + Equal(t, err, nil) +} + +func TestDecoder_EmptyArrayBool(t *testing.T) { + type T1 struct { + F1 bool `form:"F1"` + } + in := url.Values{ + "F1": []string{}, + } + + v := new(T1) + d := NewDecoder() + err := d.Decode(v, in) + Equal(t, err, nil) +} + +func TestDecoder_InvalidSliceIndex(t *testing.T) { + type PostsRequest struct { + PostIds []string + } + in := url.Values{ + "PostIds[]": []string{"1", "2"}, + } + + v := new(PostsRequest) + d := NewDecoder() + err := d.Decode(v, in) + NotEqual(t, err, nil) + Equal(t, err.Error(), "Field Namespace:PostIds ERROR:invalid slice index ''") + + // No error with proper name + type PostsRequest2 struct { + PostIds []string `form:"PostIds[]"` + } + + v2 := new(PostsRequest2) + err = d.Decode(v2, in) + Equal(t, err, nil) + Equal(t, len(v2.PostIds), 2) + Equal(t, v2.PostIds[0], "1") + Equal(t, v2.PostIds[1], "2") +} + +// TestNestedArrayPerformance verifies decoding performance for nested structures. +// - Uses more lenient thresholds when race detector is enabled, since it can slow down execution. +func TestNestedArrayPerformance(t *testing.T) { + type NestedBar struct { + Bazs []string `form:"bazs"` + Lookup map[string]string `form:"lookup"` + } + + type NestedFoo struct { + Bars []*NestedBar `form:"bars"` + } + + type FormRequest struct { + Foos []*NestedFoo `form:"foos"` + } + + decoder := NewDecoder() + + var thresholdTests []struct { + numValues int + maxTime time.Duration + } + + if raceEnabled { + thresholdTests = []struct { + numValues int + maxTime time.Duration + }{ + {10, 50 * time.Millisecond}, + {50, 500 * time.Millisecond}, + {200, 5 * time.Second}, + } + } else { + thresholdTests = []struct { + numValues int + maxTime time.Duration + }{ + {10, 10 * time.Millisecond}, + {50, 50 * time.Millisecond}, + {200, 500 * time.Millisecond}, + } + } + + for _, tt := range thresholdTests { + urlValues := make(url.Values) + + for i := 0; i < tt.numValues; i++ { + urlValues.Add(fmt.Sprintf("foos[0].bars[%d].bazs", i), fmt.Sprintf("value%d", i)) + urlValues.Add(fmt.Sprintf("foos[0].bars[%d].lookup[A]", i), fmt.Sprintf("lookupA%d", i)) + } + + var req FormRequest + start := time.Now() + err := decoder.Decode(&req, urlValues) + elapsed := time.Since(start) + + if err != nil { + t.Errorf("Decode error for %d values: %v", tt.numValues, err) + } + + if len(req.Foos) != 1 { + t.Errorf("Expected 1 Foo, got %d", len(req.Foos)) + } + if len(req.Foos[0].Bars) != tt.numValues { + t.Errorf("Expected %d Bars, got %d", tt.numValues, len(req.Foos[0].Bars)) + } + + if elapsed > tt.maxTime { + t.Errorf("[race=%t] Decoding %d values took %v, expected less than %v", + raceEnabled, tt.numValues, elapsed, tt.maxTime) + } + } +} + +func BenchmarkNestedArrayDecode100(b *testing.B) { + type NestedBar struct { + Bazs []string `form:"bazs"` + Lookup map[string]string `form:"lookup"` + } + + type NestedFoo struct { + Bars []*NestedBar `form:"bars"` + } + + type FormRequest struct { + Foos []*NestedFoo `form:"foos"` + } + + decoder := NewDecoder() + urlValues := make(url.Values) + + for i := 0; i < 100; i++ { + urlValues.Add(fmt.Sprintf("foos[0].bars[%d].bazs", i), fmt.Sprintf("value%d", i)) + urlValues.Add(fmt.Sprintf("foos[0].bars[%d].lookup[A]", i), fmt.Sprintf("lookupA%d", i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + var req FormRequest + if err := decoder.Decode(&req, urlValues); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkNestedArrayDecode1000(b *testing.B) { + type NestedBar struct { + Bazs []string `form:"bazs"` + Lookup map[string]string `form:"lookup"` + } + + type NestedFoo struct { + Bars []*NestedBar `form:"bars"` + } + + type FormRequest struct { + Foos []*NestedFoo `form:"foos"` + } + + decoder := NewDecoder() + urlValues := make(url.Values) + + for i := 0; i < 1000; i++ { + urlValues.Add(fmt.Sprintf("foos[0].bars[%d].bazs", i), fmt.Sprintf("value%d", i)) + urlValues.Add(fmt.Sprintf("foos[0].bars[%d].lookup[A]", i), fmt.Sprintf("lookupA%d", i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + var req FormRequest + if err := decoder.Decode(&req, urlValues); err != nil { + b.Fatal(err) + } + } +}