refactor: remove net.URL and net.CIDR types, improved unmarshal handling

This commit is contained in:
yusing
2025-04-13 07:06:21 +08:00
parent 1eac48e899
commit fce96ff3be
37 changed files with 236 additions and 292 deletions

View File

@@ -1,8 +1,11 @@
package utils
import (
"bytes"
"encoding/json"
"errors"
"net"
"net/url"
"os"
"reflect"
"strconv"
@@ -42,7 +45,14 @@ var (
tagAliases = "aliases" // declare aliases for fields
)
var mapUnmarshalerType = reflect.TypeFor[MapUnmarshaller]()
var (
typeDuration = reflect.TypeFor[time.Duration]()
typeURL = reflect.TypeFor[url.URL]()
typeCIDR = reflect.TypeFor[*net.IPNet]()
typeMapMarshaller = reflect.TypeFor[MapMarshaller]()
typeMapUnmarshaler = reflect.TypeFor[MapUnmarshaller]()
)
var defaultValues = functional.NewMapOf[reflect.Type, func() any]()
@@ -196,7 +206,7 @@ func MapUnmarshalValidate(src SerializedObject, dst any) (err gperr.Error) {
return gperr.Errorf("unmarshal: src is %w and dst is not settable", ErrNilValue)
}
if dstT.Implements(mapUnmarshalerType) {
if dstT.Implements(typeMapUnmarshaler) {
dstV, _, err = dive(dstV)
if err != nil {
return err
@@ -370,7 +380,7 @@ func Convert(src reflect.Value, dst reflect.Value) gperr.Error {
}
obj, ok := src.Interface().(SerializedObject)
if !ok {
return ErrUnsupportedConversion.Subject(dstT.String() + " to " + srcT.String())
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT)
}
return MapUnmarshalValidate(obj, dst.Addr().Interface())
case srcKind == reflect.Slice:
@@ -378,7 +388,7 @@ func Convert(src reflect.Value, dst reflect.Value) gperr.Error {
return nil
}
if dstT.Kind() != reflect.Slice {
return ErrUnsupportedConversion.Subject(dstT.String() + " to " + srcT.String())
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT)
}
sliceErrs := gperr.NewBuilder("slice conversion errors")
newSlice := reflect.MakeSlice(dstT, src.Len(), src.Len())
@@ -402,6 +412,10 @@ func Convert(src reflect.Value, dst reflect.Value) gperr.Error {
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT)
}
func nilPointer[T any]() reflect.Value {
return reflect.ValueOf((*T)(nil))
}
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gperr.Error) {
convertible = true
dstT := dst.Type()
@@ -417,10 +431,10 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
return
}
switch dstT {
case reflect.TypeFor[time.Duration]():
case typeDuration:
if src == "" {
dst.Set(reflect.Zero(dstT))
return
return false, nil
}
d, err := time.ParseDuration(src)
if err != nil {
@@ -431,7 +445,31 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
}
dst.Set(reflect.ValueOf(d))
return
default:
case typeURL:
if src == "" {
dst.Addr().Set(nilPointer[*url.URL]())
return
}
u, err := url.Parse(src)
if err != nil {
return true, gperr.Wrap(err)
}
dst.Set(reflect.ValueOf(u).Elem())
return
case typeCIDR:
if src == "" {
dst.Addr().Set(nilPointer[*net.IPNet]())
return
}
if !strings.Contains(src, "/") {
src += "/32" // single IP
}
_, ipnet, err := net.ParseCIDR(src)
if err != nil {
return true, gperr.Wrap(err)
}
dst.Set(reflect.ValueOf(ipnet).Elem())
return
}
if dstKind := dst.Kind(); isIntFloat(dstKind) {
var i any

View File

@@ -1,10 +1,13 @@
package utils
import (
"net/url"
"reflect"
"strconv"
"testing"
"time"
"github.com/yusing/go-proxy/internal/utils/strutils"
. "github.com/yusing/go-proxy/internal/utils/testing"
"gopkg.in/yaml.v3"
)
@@ -42,7 +45,7 @@ func TestDeserialize(t *testing.T) {
var s2 S
err := MapUnmarshalValidate(testStructSerialized, &s2)
ExpectNoError(t, err)
ExpectEqual(t, s2, testStruct)
ExpectEqualValues(t, s2, testStruct)
})
}
@@ -62,15 +65,15 @@ func TestDeserializeAnonymousField(t *testing.T) {
// t.Fatalf("anon %v, all %v", anon, all)
err := MapUnmarshalValidate(map[string]any{"a": 1, "b": 2, "c": 3}, &s)
ExpectNoError(t, err)
ExpectEqual(t, s.A, 1)
ExpectEqual(t, s.B, 2)
ExpectEqual(t, s.C, 3)
ExpectEqualValues(t, s.A, 1)
ExpectEqualValues(t, s.B, 2)
ExpectEqualValues(t, s.C, 3)
err = MapUnmarshalValidate(map[string]any{"a": 1, "b": 2, "c": 3}, &s2)
ExpectNoError(t, err)
ExpectEqual(t, s2.A, 1)
ExpectEqual(t, s2.B, 2)
ExpectEqual(t, s2.C, 3)
ExpectEqualValues(t, s2.A, 1)
ExpectEqualValues(t, s2.B, 2)
ExpectEqualValues(t, s2.C, 3)
}
func TestStringIntConvert(t *testing.T) {
@@ -91,42 +94,42 @@ func TestStringIntConvert(t *testing.T) {
ExpectTrue(t, ok)
ExpectNoError(t, err)
ExpectEqual(t, test.i8, int8(127))
ExpectEqualValues(t, test.i8, int8(127))
ok, err = ConvertString(s, reflect.ValueOf(&test.i16))
ExpectTrue(t, ok)
ExpectNoError(t, err)
ExpectEqual(t, test.i16, int16(127))
ExpectEqualValues(t, test.i16, int16(127))
ok, err = ConvertString(s, reflect.ValueOf(&test.i32))
ExpectTrue(t, ok)
ExpectNoError(t, err)
ExpectEqual(t, test.i32, int32(127))
ExpectEqualValues(t, test.i32, int32(127))
ok, err = ConvertString(s, reflect.ValueOf(&test.i64))
ExpectTrue(t, ok)
ExpectNoError(t, err)
ExpectEqual(t, test.i64, int64(127))
ExpectEqualValues(t, test.i64, int64(127))
ok, err = ConvertString(s, reflect.ValueOf(&test.u8))
ExpectTrue(t, ok)
ExpectNoError(t, err)
ExpectEqual(t, test.u8, uint8(127))
ExpectEqualValues(t, test.u8, uint8(127))
ok, err = ConvertString(s, reflect.ValueOf(&test.u16))
ExpectTrue(t, ok)
ExpectNoError(t, err)
ExpectEqual(t, test.u16, uint16(127))
ExpectEqualValues(t, test.u16, uint16(127))
ok, err = ConvertString(s, reflect.ValueOf(&test.u32))
ExpectTrue(t, ok)
ExpectNoError(t, err)
ExpectEqual(t, test.u32, uint32(127))
ExpectEqualValues(t, test.u32, uint32(127))
ok, err = ConvertString(s, reflect.ValueOf(&test.u64))
ExpectTrue(t, ok)
ExpectNoError(t, err)
ExpectEqual(t, test.u64, uint64(127))
ExpectEqualValues(t, test.u64, uint64(127))
}
type testModel struct {
@@ -150,19 +153,19 @@ func TestConvertor(t *testing.T) {
m := new(testModel)
ExpectNoError(t, MapUnmarshalValidate(map[string]any{"Test": "123"}, m))
ExpectEqual(t, m.Test.foo, 123)
ExpectEqual(t, m.Test.bar, "123")
ExpectEqualValues(t, m.Test.foo, 123)
ExpectEqualValues(t, m.Test.bar, "123")
})
t.Run("int_to_string", func(t *testing.T) {
m := new(testModel)
ExpectNoError(t, MapUnmarshalValidate(map[string]any{"Test": "123"}, m))
ExpectEqual(t, m.Test.foo, 123)
ExpectEqual(t, m.Test.bar, "123")
ExpectEqualValues(t, m.Test.foo, 123)
ExpectEqualValues(t, m.Test.bar, "123")
ExpectNoError(t, MapUnmarshalValidate(map[string]any{"Baz": 123}, m))
ExpectEqual(t, m.Baz, "123")
ExpectEqualValues(t, m.Baz, "123")
})
t.Run("invalid", func(t *testing.T) {
@@ -177,21 +180,21 @@ func TestStringToSlice(t *testing.T) {
convertible, err := ConvertString("a,b,c", reflect.ValueOf(&dst))
ExpectTrue(t, convertible)
ExpectNoError(t, err)
ExpectEqual(t, dst, []string{"a", "b", "c"})
ExpectEqualValues(t, dst, []string{"a", "b", "c"})
})
t.Run("yaml-like", func(t *testing.T) {
dst := make([]string, 0)
convertible, err := ConvertString("- a\n- b\n- c", reflect.ValueOf(&dst))
ExpectTrue(t, convertible)
ExpectNoError(t, err)
ExpectEqual(t, dst, []string{"a", "b", "c"})
ExpectEqualValues(t, dst, []string{"a", "b", "c"})
})
t.Run("single-line-yaml-like", func(t *testing.T) {
dst := make([]string, 0)
convertible, err := ConvertString("- a", reflect.ValueOf(&dst))
ExpectTrue(t, convertible)
ExpectNoError(t, err)
ExpectEqual(t, dst, []string{"a"})
ExpectEqualValues(t, dst, []string{"a"})
})
}
@@ -215,7 +218,7 @@ func TestStringToMap(t *testing.T) {
convertible, err := ConvertString(" a: b\n c: d", reflect.ValueOf(&dst))
ExpectTrue(t, convertible)
ExpectNoError(t, err)
ExpectEqual(t, dst, map[string]string{"a": "b", "c": "d"})
ExpectEqualValues(t, dst, map[string]string{"a": "b", "c": "d"})
})
}
@@ -242,7 +245,7 @@ func TestStringToStruct(t *testing.T) {
convertible, err := ConvertString(" A: a\n B: 123", reflect.ValueOf(&dst))
ExpectTrue(t, convertible)
ExpectNoError(t, err)
ExpectEqual(t, dst, struct {
ExpectEqualValues(t, dst, struct {
A string
B int
}{"a", 123})

View File

@@ -21,50 +21,55 @@ func Must[Result any](r Result, err error) Result {
return r
}
func ExpectNoError(t *testing.T, err error) {
func ExpectNoError(t *testing.T, err error, msgAndArgs ...any) {
t.Helper()
require.NoError(t, err)
require.NoError(t, err, msgAndArgs...)
}
func ExpectHasError(t *testing.T, err error) {
func ExpectHasError(t *testing.T, err error, msgAndArgs ...any) {
t.Helper()
require.Error(t, err)
require.Error(t, err, msgAndArgs...)
}
func ExpectError(t *testing.T, expected error, err error) {
func ExpectError(t *testing.T, expected error, err error, msgAndArgs ...any) {
t.Helper()
require.ErrorIs(t, err, expected)
require.ErrorIs(t, err, expected, msgAndArgs...)
}
func ExpectErrorT[T error](t *testing.T, err error) {
func ExpectErrorT[T error](t *testing.T, err error, msgAndArgs ...any) {
t.Helper()
var errAs T
require.ErrorAs(t, err, &errAs)
require.ErrorAs(t, err, &errAs, msgAndArgs...)
}
func ExpectEqual[T any](t *testing.T, got T, want T) {
func ExpectEqual[T any](t *testing.T, got T, want T, msgAndArgs ...any) {
t.Helper()
require.EqualValues(t, got, want)
require.Equal(t, want, got, msgAndArgs...)
}
func ExpectContains[T any](t *testing.T, got T, wants []T) {
func ExpectEqualValues(t *testing.T, got any, want any, msgAndArgs ...any) {
t.Helper()
require.Contains(t, wants, got)
require.EqualValues(t, want, got, msgAndArgs...)
}
func ExpectTrue(t *testing.T, got bool) {
func ExpectContains[T any](t *testing.T, got T, wants []T, msgAndArgs ...any) {
t.Helper()
require.True(t, got)
require.Contains(t, wants, got, msgAndArgs...)
}
func ExpectFalse(t *testing.T, got bool) {
func ExpectTrue(t *testing.T, got bool, msgAndArgs ...any) {
t.Helper()
require.False(t, got)
require.True(t, got, msgAndArgs...)
}
func ExpectType[T any](t *testing.T, got any) (_ T) {
func ExpectFalse(t *testing.T, got bool, msgAndArgs ...any) {
t.Helper()
require.False(t, got, msgAndArgs...)
}
func ExpectType[T any](t *testing.T, got any, msgAndArgs ...any) (_ T) {
t.Helper()
_, ok := got.(T)
require.True(t, ok)
require.True(t, ok, msgAndArgs...)
return got.(T)
}