Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add custom unset err when parse request #3460

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions core/mapping/unmarshaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
)

Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 37 additions & 0 deletions core/mapping/unmarshaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
32 changes: 26 additions & 6 deletions rest/httpx/requests.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package httpx

import (
"context"
"io"
"net/http"
"reflect"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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))
}
34 changes: 34 additions & 0 deletions rest/httpx/requests_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package httpx

import (
"context"
"bytes"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
Expand Down Expand Up @@ -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"`
Expand Down
Loading