Kevin Wan 1 жил өмнө
parent
commit
cb3ffc76a3

+ 18 - 5
core/mapping/unmarshaler.go

@@ -49,6 +49,7 @@ type (
 	unmarshalOptions struct {
 		fillDefault  bool
 		fromString   bool
+		opaqueKeys   bool
 		canonicalKey func(key string) string
 	}
 )
@@ -494,7 +495,7 @@ func (u *Unmarshaler) processAnonymousStructFieldOptional(fieldType reflect.Type
 			return err
 		}
 
-		_, hasValue := getValue(m, fieldKey)
+		_, hasValue := getValue(m, fieldKey, u.opts.opaqueKeys)
 		if hasValue {
 			if !filled {
 				filled = true
@@ -737,7 +738,7 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
 	}
 
 	valuer := createValuer(m, opts)
-	mapValue, hasValue := getValue(valuer, canonicalKey)
+	mapValue, hasValue := getValue(valuer, canonicalKey, u.opts.opaqueKeys)
 
 	// When fillDefault is used, m is a null value, hasValue must be false, all priority judgments fillDefault.
 	if u.opts.fillDefault {
@@ -928,6 +929,14 @@ func WithDefault() UnmarshalOption {
 	}
 }
 
+// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
+// Opaque keys are keys that are not processed by the unmarshaler.
+func WithOpaqueKeys() UnmarshalOption {
+	return func(opt *unmarshalOptions) {
+		opt.opaqueKeys = true
+	}
+}
+
 func createValuer(v valuerWithParent, opts *fieldOptionsWithContext) valuerWithParent {
 	if opts.inherit() {
 		return recursiveValuer{
@@ -1005,8 +1014,8 @@ func fillWithSameType(fieldType reflect.Type, value reflect.Value, mapValue any,
 }
 
 // getValue gets the value for the specific key, the key can be in the format of parentKey.childKey
-func getValue(m valuerWithParent, key string) (any, bool) {
-	keys := readKeys(key)
+func getValue(m valuerWithParent, key string, opaque bool) (any, bool) {
+	keys := readKeys(key, opaque)
 	return getValueWithChainedKeys(m, keys)
 }
 
@@ -1065,7 +1074,11 @@ func newTypeMismatchErrorWithHint(name, expectType, actualType string) error {
 		name, expectType, actualType)
 }
 
-func readKeys(key string) []string {
+func readKeys(key string, opaque bool) []string {
+	if opaque {
+		return []string{key}
+	}
+
 	cacheKeysLock.Lock()
 	keys, ok := cacheKeys[key]
 	cacheKeysLock.Unlock()

+ 15 - 0
core/mapping/unmarshaler_test.go

@@ -5092,6 +5092,21 @@ func TestUnmarshalFromStringSliceForTypeMismatch(t *testing.T) {
 	}, &v))
 }
 
+func TestUnmarshalWithOpaqueKeys(t *testing.T) {
+	var v struct {
+		Opaque string `key:"opaque.key"`
+		Value  string `key:"value"`
+	}
+	unmarshaler := NewUnmarshaler("key", WithOpaqueKeys())
+	if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
+		"opaque.key": "foo",
+		"value":      "bar",
+	}, &v)) {
+		assert.Equal(t, "foo", v.Opaque)
+		assert.Equal(t, "bar", v.Value)
+	}
+}
+
 func BenchmarkDefaultValue(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		var a struct {

+ 2 - 2
rest/httpx/requests.go

@@ -23,8 +23,8 @@ const (
 )
 
 var (
-	formUnmarshaler = mapping.NewUnmarshaler(formKey, mapping.WithStringValues())
-	pathUnmarshaler = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues())
+	formUnmarshaler = mapping.NewUnmarshaler(formKey, mapping.WithStringValues(), mapping.WithOpaqueKeys())
+	pathUnmarshaler = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues(), mapping.WithOpaqueKeys())
 	validator       atomic.Value
 )
 

+ 40 - 1
rest/httpx/requests_test.go

@@ -326,6 +326,8 @@ func TestParseHeaders_Error(t *testing.T) {
 
 func TestParseWithValidator(t *testing.T) {
 	SetValidator(mockValidator{})
+	defer SetValidator(mockValidator{nop: true})
+
 	var v struct {
 		Name    string  `form:"name"`
 		Age     int     `form:"age"`
@@ -343,6 +345,8 @@ func TestParseWithValidator(t *testing.T) {
 
 func TestParseWithValidatorWithError(t *testing.T) {
 	SetValidator(mockValidator{})
+	defer SetValidator(mockValidator{nop: true})
+
 	var v struct {
 		Name    string  `form:"name"`
 		Age     int     `form:"age"`
@@ -356,12 +360,41 @@ func TestParseWithValidatorWithError(t *testing.T) {
 
 func TestParseWithValidatorRequest(t *testing.T) {
 	SetValidator(mockValidator{})
+	defer SetValidator(mockValidator{nop: true})
+
 	var v mockRequest
 	r, err := http.NewRequest(http.MethodGet, "/a?&age=18", http.NoBody)
 	assert.Nil(t, err)
 	assert.Error(t, Parse(r, &v))
 }
 
+func TestParseFormWithDot(t *testing.T) {
+	var v struct {
+		Age int `form:"user.age"`
+	}
+	r, err := http.NewRequest(http.MethodGet, "/a?user.age=18", http.NoBody)
+	assert.Nil(t, err)
+	assert.NoError(t, Parse(r, &v))
+	assert.Equal(t, 18, v.Age)
+}
+
+func TestParsePathWithDot(t *testing.T) {
+	var v struct {
+		Name string `path:"name.val"`
+		Age  int    `path:"age.val"`
+	}
+
+	r := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
+	r = pathvar.WithVars(r, map[string]string{
+		"name.val": "foo",
+		"age.val":  "18",
+	})
+	err := Parse(r, &v)
+	assert.Nil(t, err)
+	assert.Equal(t, "foo", v.Name)
+	assert.Equal(t, 18, v.Age)
+}
+
 func BenchmarkParseRaw(b *testing.B) {
 	r, err := http.NewRequest(http.MethodGet, "http://hello.com/a?name=hello&age=18&percent=3.4", http.NoBody)
 	if err != nil {
@@ -406,9 +439,15 @@ func BenchmarkParseAuto(b *testing.B) {
 	}
 }
 
-type mockValidator struct{}
+type mockValidator struct {
+	nop bool
+}
 
 func (m mockValidator) Validate(r *http.Request, data any) error {
+	if m.nop {
+		return nil
+	}
+
 	if r.URL.Path == "/a" {
 		val := reflect.ValueOf(data).Elem().FieldByName("Name").String()
 		if val != "hello" {