diff --git a/form/form_custom_type_decoder.go b/form/form_custom_type_decoder.go new file mode 100644 index 0000000..bf6d0c2 --- /dev/null +++ b/form/form_custom_type_decoder.go @@ -0,0 +1,129 @@ +package form + +import ( + "strconv" + "strings" + + "github.com/go-playground/form/v4" + "golang.org/x/exp/constraints" +) + +func RegisterBuiltinSliceTypeDecoderComma(dec *form.Decoder) { + dec.RegisterCustomTypeFunc(DecodeCustomIntSlice[int], []int{}) + dec.RegisterCustomTypeFunc(DecodeCustomInt8Slice[int8], []int8{}) + dec.RegisterCustomTypeFunc(DecodeCustomInt16Slice[int16], []int16{}) + dec.RegisterCustomTypeFunc(DecodeCustomInt32Slice[int32], []int32{}) + dec.RegisterCustomTypeFunc(DecodeCustomInt64Slice[int64], []int64{}) + dec.RegisterCustomTypeFunc(DecodeCustomUintSlice[uint], []uint{}) + dec.RegisterCustomTypeFunc(DecodeCustomUint8Slice[uint8], []uint8{}) + dec.RegisterCustomTypeFunc(DecodeCustomUint16Slice[uint16], []uint16{}) + dec.RegisterCustomTypeFunc(DecodeCustomUint32Slice[uint32], []uint32{}) + dec.RegisterCustomTypeFunc(DecodeCustomUint64Slice[uint64], []uint64{}) + dec.RegisterCustomTypeFunc(DecodeCustomFloat32Slice[float32], []float32{}) + dec.RegisterCustomTypeFunc(DecodeCustomFloat64Slice[float64], []float64{}) + dec.RegisterCustomTypeFunc(DecodeCustomStringSlice[string], []string{}) +} + +//* encoder: custom type number/string slice/array + +func DecodeCustomUintSlice[T ~uint](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (uint64, error) { + return strconv.ParseUint(s, 10, 0) + }) +} + +func DecodeCustomUint8Slice[T ~uint8](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (uint64, error) { + return strconv.ParseUint(s, 10, 8) + }) +} + +func DecodeCustomUint16Slice[T ~uint16](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (uint64, error) { + return strconv.ParseUint(s, 10, 16) + }) +} + +func DecodeCustomUint32Slice[T ~uint32](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (uint64, error) { + return strconv.ParseUint(s, 10, 32) + }) +} + +func DecodeCustomUint64Slice[T ~uint64](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (uint64, error) { + return strconv.ParseUint(s, 10, 64) + }) +} + +func DecodeCustomIntSlice[T ~int](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (int64, error) { + return strconv.ParseInt(s, 10, 0) + }) +} + +func DecodeCustomInt8Slice[T ~int8](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (int64, error) { + return strconv.ParseInt(s, 10, 8) + }) +} + +func DecodeCustomInt16Slice[T ~int16](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (int64, error) { + return strconv.ParseInt(s, 10, 16) + }) +} + +func DecodeCustomInt32Slice[T ~int32](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (int64, error) { + return strconv.ParseInt(s, 10, 32) + }) +} + +func DecodeCustomInt64Slice[T ~int64](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) + }) +} + +func DecodeCustomFloat64Slice[T ~float64](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (float64, error) { + return strconv.ParseFloat(s, 64) + }) +} + +func DecodeCustomFloat32Slice[T ~float32](values []string) (any, error) { + return decodeNumber[T](values, func(s string) (float64, error) { + return strconv.ParseFloat(s, 32) + }) +} + +func DecodeCustomStringSlice[T ~string](values []string) (any, error) { + if len(values) == 0 { + return []T{}, nil + } + ret := make([]T, 0, 32) + for _, s := range values { + for _, v := range strings.Split(s, ",") { + ret = append(ret, T(v)) + } + } + return ret, nil +} + +func decodeNumber[T constraints.Integer | constraints.Float, V ~uint64 | ~int64 | ~float64](values []string, parse func(string) (V, error)) (any, error) { + if len(values) == 0 { + return []T{}, nil + } + ret := make([]T, 0, 32) + for _, s := range values { + for _, v := range strings.Split(s, ",") { + i, err := parse(v) + if err != nil { + return nil, err + } + ret = append(ret, T(i)) + } + } + return ret, nil +} diff --git a/form/form_custom_type_decoder_test.go b/form/form_custom_type_decoder_test.go new file mode 100644 index 0000000..875d38b --- /dev/null +++ b/form/form_custom_type_decoder_test.go @@ -0,0 +1,103 @@ +package form + +import ( + "net/url" + "testing" + + "github.com/go-playground/form/v4" + "github.com/stretchr/testify/require" +) + +type CustomInt int +type CustomInt8 int8 +type CustomInt16 int16 +type CustomInt32 int32 +type CustomInt64 int64 +type CustomUint uint +type CustomUint8 uint8 +type CustomUint16 uint16 +type CustomUint32 uint32 +type CustomUint64 uint64 +type CustomFloat32 float32 +type CustomFloat64 float64 +type CustomString string + +type customDecode struct { + I []int `json:"i"` + I8 []int8 `json:"i8"` + I16 []int16 `json:"i16"` + I32 []int32 `json:"i32"` + I64 []int64 `json:"i64"` + U []int `json:"u"` + U8 []int8 `json:"u8"` + U16 []int16 `json:"u16"` + U32 []int32 `json:"u32"` + U64 []int64 `json:"u64"` + F32 []float32 `json:"f32"` + F64 []float64 `json:"f64"` + S []string `json:"s"` + Ci []CustomInt `json:"ci"` + Ci8 []CustomInt8 `json:"ci8"` + Ci16 []CustomInt16 `json:"ci16"` + Ci32 []CustomInt32 `json:"ci32"` + Ci64 []CustomInt64 `json:"ci64"` + Cu []CustomUint `json:"cu"` + Cu8 []CustomUint8 `json:"cu8"` + Cu16 []CustomUint16 `json:"cu16"` + Cu32 []CustomUint32 `json:"cu32"` + Cu64 []CustomUint64 `json:"cu64"` + Cf32 []CustomFloat32 `json:"cf32"` + Cf64 []CustomFloat64 `json:"cf64"` + Cs []CustomString `json:"cs"` +} + +func Test_CustomTypeDecoder(t *testing.T) { + dec := form.NewDecoder() + dec.SetTagName("json") + + RegisterBuiltinSliceTypeDecoderComma(dec) + dec.RegisterCustomTypeFunc(DecodeCustomIntSlice[CustomInt], []CustomInt{}) + dec.RegisterCustomTypeFunc(DecodeCustomInt8Slice[CustomInt8], []CustomInt8{}) + dec.RegisterCustomTypeFunc(DecodeCustomInt16Slice[CustomInt16], []CustomInt16{}) + dec.RegisterCustomTypeFunc(DecodeCustomInt32Slice[CustomInt32], []CustomInt32{}) + dec.RegisterCustomTypeFunc(DecodeCustomInt64Slice[CustomInt64], []CustomInt64{}) + dec.RegisterCustomTypeFunc(DecodeCustomUintSlice[CustomUint], []CustomUint{}) + dec.RegisterCustomTypeFunc(DecodeCustomUint8Slice[CustomUint8], []CustomUint8{}) + dec.RegisterCustomTypeFunc(DecodeCustomUint16Slice[CustomUint16], []CustomUint16{}) + dec.RegisterCustomTypeFunc(DecodeCustomUint32Slice[CustomUint32], []CustomUint32{}) + dec.RegisterCustomTypeFunc(DecodeCustomUint64Slice[CustomUint64], []CustomUint64{}) + dec.RegisterCustomTypeFunc(DecodeCustomFloat32Slice[CustomFloat32], []CustomFloat32{}) + dec.RegisterCustomTypeFunc(DecodeCustomFloat64Slice[CustomFloat64], []CustomFloat64{}) + dec.RegisterCustomTypeFunc(DecodeCustomStringSlice[CustomString], []CustomString{}) + got := customDecode{} + err := dec.Decode(&got, url.Values{ + "i": []string{"1,2,3", "4,5,6"}, + "i8": []string{"81,82,83"}, + "i16": []string{"161,162,163"}, + "i32": []string{"321,322,323"}, + "i64": []string{"641,642,643"}, + "u": []string{"111111,222222,333333"}, + "u8": []string{"11,22,33"}, + "u16": []string{"1611,1622,1633"}, + "u32": []string{"3211,3222,3233"}, + "u64": []string{"6411,6422,6433"}, + "f32": []string{"1.1,1.2,1.3"}, + "f64": []string{"2.1,2.2,2.3"}, + "s": []string{"a,b,c", "d,e,f"}, + "ci": []string{"1,2,3"}, + "ci8": []string{"81,82,83"}, + "ci16": []string{"161,162,163"}, + "ci32": []string{"321,322,323"}, + "ci64": []string{"641,642,643"}, + "cu": []string{"111111,222222,333333"}, + "cu8": []string{"11,22,33"}, + "cu16": []string{"1611,1622,1633"}, + "cu32": []string{"3211,3222,3233"}, + "cu64": []string{"6411,6422,6433"}, + "cf32": []string{"1.1,1.2,1.3"}, + "cf64": []string{"2.1,2.2,2.3"}, + "cs": []string{"a,b,c"}, + }) + require.NoError(t, err) + t.Logf("%#v", got) +} diff --git a/form/form_custom_type_encoder.go b/form/form_custom_type_encoder.go new file mode 100644 index 0000000..92aa28b --- /dev/null +++ b/form/form_custom_type_encoder.go @@ -0,0 +1,76 @@ +package form + +import ( + "fmt" + "strconv" + "strings" + + "golang.org/x/exp/constraints" +) + +//* encoder: custom type number/string slice/array + +func EncodeCustomUnsignedIntegerSlice[T constraints.Unsigned](x any) ([]string, error) { + return encodeCustomNumberSlice[T](x, func(u uint64) string { + return strconv.FormatUint(u, 10) + }) +} + +func EncodeCustomSignedIntegerSlice[T constraints.Unsigned](x any) ([]string, error) { + return encodeCustomNumberSlice[T](x, func(u int64) string { + return strconv.FormatInt(u, 10) + }) +} + +func EncodeCustomFloat32Slice[T ~float32](x any) ([]string, error) { + return encodeCustomNumberSlice[T](x, func(u float64) string { + return strconv.FormatFloat(u, 'f', -1, 32) + }) +} + +func EncodeCustomFloat64Slice[T ~float64](x any) ([]string, error) { + return encodeCustomNumberSlice[T](x, func(u float64) string { + return strconv.FormatFloat(u, 'f', -1, 64) + }) +} + +func EncodeCustomStringSlice[T ~string](x any) ([]string, error) { + vs, ok := x.([]T) + if !ok { + return nil, fmt.Errorf("") + } + has := false + b := strings.Builder{} + for _, vv := range vs { + if vv == "" { + continue + } + if has { + b.WriteString(",") + } + has = true + b.WriteString(string(vv)) + } + return []string{b.String()}, nil +} + +func encodeCustomNumberSlice[T constraints.Integer | constraints.Float, V uint64 | int64 | float64](x any, format func(V) string) ([]string, error) { + vs, ok := x.([]T) + if !ok { + return nil, fmt.Errorf("") + } + has := false + b := strings.Builder{} + for _, v := range vs { + vv := format(V(v)) + if vv == "" { + continue + } + if has { + b.WriteString(",") + } + has = true + b.WriteString(vv) + } + return []string{b.String()}, nil +} diff --git a/form/form_custom_type_encoder_test.go b/form/form_custom_type_encoder_test.go new file mode 100644 index 0000000..b50f6a4 --- /dev/null +++ b/form/form_custom_type_encoder_test.go @@ -0,0 +1 @@ +package form diff --git a/go.mod b/go.mod index 8ab51e1..1070134 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/spf13/cast v1.6.0 github.com/stretchr/testify v1.9.0 github.com/ugorji/go/codec v1.2.12 + golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be google.golang.org/protobuf v1.34.1 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 9ca39c0..d82f4df 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=