diff --git a/core/mapping/unmarshaler.go b/core/mapping/unmarshaler.go index b6281e42..4a1129b6 100644 --- a/core/mapping/unmarshaler.go +++ b/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() diff --git a/core/mapping/unmarshaler_test.go b/core/mapping/unmarshaler_test.go index 6d12457c..f89ffb52 100644 --- a/core/mapping/unmarshaler_test.go +++ b/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 { diff --git a/rest/httpx/requests.go b/rest/httpx/requests.go index 02d29eca..cac81a58 100644 --- a/rest/httpx/requests.go +++ b/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 ) diff --git a/rest/httpx/requests_test.go b/rest/httpx/requests_test.go index aa462176..04b6f21c 100644 --- a/rest/httpx/requests_test.go +++ b/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" {