diff --git a/core/mapping/unmarshaler.go b/core/mapping/unmarshaler.go index f68748c68172..4c25bd821317 100644 --- a/core/mapping/unmarshaler.go +++ b/core/mapping/unmarshaler.go @@ -50,12 +50,13 @@ type ( // UnmarshalOption defines the method to customize an Unmarshaler. UnmarshalOption func(*unmarshalOptions) - unmarshalOptions struct { + unmarshalOptions struct { fillDefault bool fromArray bool fromString bool opaqueKeys bool canonicalKey func(key string) string + customFieldUnsetErr func(key string) error } ) @@ -72,7 +73,18 @@ func NewUnmarshaler(key string, opts ...UnmarshalOption) *Unmarshaler { return &unmarshaler } -// UnmarshalKey unmarshals m into v with the tag key. +func WithOpts(u *Unmarshaler, opts ...UnmarshalOption) *Unmarshaler { + if u == nil { + return u + } + for _, opt := range opts { + opt(&u.opts) + } + + return u +} + +// UnmarshalKey unmarshals m into v with tag key. func UnmarshalKey(m map[string]any, v any) error { return keyUnmarshaler.Unmarshal(m, v) } @@ -933,6 +945,9 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu } if required { + if u.opts.customFieldUnsetErr != nil { + return u.opts.customFieldUnsetErr(fullName) + } return fmt.Errorf("%q is not set", fullName) } @@ -942,6 +957,9 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu } default: if !opts.optional() { + if u.opts.customFieldUnsetErr != nil { + return u.opts.customFieldUnsetErr(fullName) + } return newInitError(fullName) } } @@ -1023,6 +1041,13 @@ func WithCanonicalKeyFunc(f func(string) string) UnmarshalOption { } } +// WithCustomFieldUnsetErr customizes an Unmarshaler with custom field unset error. +func WithCustomFieldUnsetErr(f func(fullName string) error) UnmarshalOption { + return func(opt *unmarshalOptions) { + opt.customFieldUnsetErr = f + } +} + // WithDefault customizes an Unmarshaler with fill default values. func WithDefault() UnmarshalOption { return func(opt *unmarshalOptions) { diff --git a/core/mapping/unmarshaler_test.go b/core/mapping/unmarshaler_test.go index ae2aba0edc86..5824fe26e659 100644 --- a/core/mapping/unmarshaler_test.go +++ b/core/mapping/unmarshaler_test.go @@ -5434,6 +5434,43 @@ func TestUnmarshalJsonBytesWithAnonymousFieldNotInOptions(t *testing.T) { assert.Error(t, UnmarshalJsonBytes(input, &c)) } +func TestUnmarshalJsonBytesWithCustomFieldUnsetErr(t *testing.T) { + type ( + InnerConf struct { + Name string `json:"name"` + } + Conf struct { + Name string `json:"name"` + Inner InnerConf `json:"inner"` + } + ) + + t.Run("inner name unset", func(t *testing.T) { + var ( + input = []byte(`{"inner": {}, "name": "world"}`) + c Conf + ) + + e := UnmarshalJsonBytes(input, &c, WithCustomFieldUnsetErr(func(field string) error { + // Here you can customize exceptions and internationalization processing + return fmt.Errorf("the key %s is unset", field) + })) + assert.Error(t, e) + }) + + t.Run("name unset", func(t *testing.T) { + var ( + input = []byte(`{"inner": {"name":"hello"}`) + c Conf + ) + e := UnmarshalJsonBytes(input, &c, WithCustomFieldUnsetErr(func(field string) error { + // Here you can customize exceptions and internationalization processing + return fmt.Errorf("the key %s is unset", field) + })) + assert.Error(t, e) + }) +} + func TestUnmarshalNestedPtr(t *testing.T) { type inner struct { Int **int `key:"int"` diff --git a/rest/httpx/requests.go b/rest/httpx/requests.go index 87c576a9fe47..ee5e575ecb9e 100644 --- a/rest/httpx/requests.go +++ b/rest/httpx/requests.go @@ -1,6 +1,7 @@ package httpx import ( + "context" "io" "net/http" "reflect" @@ -34,6 +35,7 @@ var ( mapping.WithStringValues(), mapping.WithOpaqueKeys()) validator atomic.Value + customFieldUnsetErr func(ctx context.Context, key string) error ) // Validator defines the interface for validating the request. @@ -83,8 +85,8 @@ func ParseForm(r *http.Request, v any) error { if err != nil { return err } - - return formUnmarshaler.Unmarshal(params, v) + unmarshaler := mapping.WithOpts(formUnmarshaler, getUnmarshalOptions(r)...) + return unmarshaler.Unmarshal(params, v) } // ParseHeader parses the request header and returns a map. @@ -111,12 +113,13 @@ func ParseHeader(headerValue string) map[string]string { // ParseJsonBody parses the post request which contains json in body. func ParseJsonBody(r *http.Request, v any) error { + opts := getUnmarshalOptions(r) if withJsonBody(r) { reader := io.LimitReader(r.Body, maxBodyLen) - return mapping.UnmarshalJsonReader(reader, v) + return mapping.UnmarshalJsonReader(reader, v, opts...) } - return mapping.UnmarshalJsonMap(nil, v) + return mapping.UnmarshalJsonMap(nil, v, opts...) } // ParsePath parses the symbols reside in url path. @@ -127,8 +130,8 @@ func ParsePath(r *http.Request, v any) error { for k, v := range vars { m[k] = v } - - return pathUnmarshaler.Unmarshal(m, v) + unmarshaler := mapping.WithOpts(pathUnmarshaler, getUnmarshalOptions(r)...) + return unmarshaler.Unmarshal(m, v) } // SetValidator sets the validator. @@ -141,3 +144,20 @@ func SetValidator(val Validator) { func withJsonBody(r *http.Request) bool { return r.ContentLength > 0 && strings.Contains(r.Header.Get(header.ContentType), header.ApplicationJson) } + +func getUnmarshalOptions(r *http.Request) []mapping.UnmarshalOption { + var opts []mapping.UnmarshalOption + if customFieldUnsetErr != nil { + unsetErrFun := func(key string) error { + return customFieldUnsetErr(r.Context(), key) + } + opts = append(opts, mapping.WithCustomFieldUnsetErr(unsetErrFun)) + } + return opts +} + +func SetCustomUnsetError(f func(ctx context.Context, fullName string) error) { + customFieldUnsetErr = f + //formUnmarshaler = mapping.NewUnmarshaler(formKey, mapping.WithStringValues(), mapping.WithCustomFieldUnsetErr(f)) + //pathUnmarshaler = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues(), mapping.WithCustomFieldUnsetErr(f)) +} diff --git a/rest/httpx/requests_test.go b/rest/httpx/requests_test.go index fd7fb3a5ac5e..2958db0709b5 100644 --- a/rest/httpx/requests_test.go +++ b/rest/httpx/requests_test.go @@ -1,8 +1,10 @@ package httpx import ( + "context" "bytes" "errors" + "fmt" "net/http" "net/http/httptest" "reflect" @@ -442,6 +444,38 @@ func TestParseJsonBody(t *testing.T) { }) } +func TestParseCustomUnsetErr(t *testing.T) { + startCtx := context.Background() + ctxKey := "method" + + SetCustomUnsetError(func(ctx context.Context, tag string) error { + return fmt.Errorf("%s: custom %s unset error", ctx.Value(ctxKey).(string), tag) + }) + t.Run("request get", func(t *testing.T) { + v := struct { + Name string `form:"name"` + Percent float64 `form:"percent"` + }{} + + gr, err := http.NewRequest(http.MethodGet, "/a?name=hello", http.NoBody) + gr = gr.WithContext(context.WithValue(startCtx, ctxKey, "GET")) + assert.Nil(t, err) + assert.EqualErrorf(t, Parse(gr, &v), "GET: custom percent unset error", "custom unset error") + }) + + t.Run("request post", func(t *testing.T) { + pv := struct { + Name string `json:"name"` + Percent float64 `json:"percent"` + }{} + body := `{"name":"hello"}` + pr := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) + pr.Header.Set(ContentType, header.JsonContentType) + pr = pr.WithContext(context.WithValue(startCtx, ctxKey, "POST")) + assert.EqualErrorf(t, Parse(pr, &pv), "POST: custom percent unset error", "custom unset error") + }) +} + func TestParseRequired(t *testing.T) { v := struct { Name string `form:"name"`