From e3ee01d1850b018b24ac215a3bb8de3d79e2604b Mon Sep 17 00:00:00 2001 From: ziheng Date: Mon, 28 Jun 2021 22:05:29 +0800 Subject: [PATCH 1/2] improve sliceValidateError.Error performance using switch and strings.Builder (#2765) fix missing nil pointer check use simpler switch case add missing tests use for-loop instead of range add benchmark test codes --- binding/default_validator.go | 24 +++++++++++++++------ binding/default_validator_benchmark_test.go | 20 +++++++++++++++++ binding/default_validator_test.go | 20 +++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 binding/default_validator_benchmark_test.go diff --git a/binding/default_validator.go b/binding/default_validator.go index ee69329e..87fc4c66 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -20,15 +20,27 @@ type defaultValidator struct { type sliceValidateError []error +// Error concatenates all error elements in sliceValidateError into a single string separated by \n. func (err sliceValidateError) Error() string { - var errMsgs []string - for i, e := range err { - if e == nil { - continue + n := len(err) + switch n { + case 0: + return "" + default: + var b strings.Builder + if err[0] != nil { + fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error()) } - errMsgs = append(errMsgs, fmt.Sprintf("[%d]: %s", i, e.Error())) + if n > 1 { + for i := 1; i < n; i++ { + if err[i] != nil { + b.WriteString("\n") + fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error()) + } + } + } + return b.String() } - return strings.Join(errMsgs, "\n") } var _ StructValidator = &defaultValidator{} diff --git a/binding/default_validator_benchmark_test.go b/binding/default_validator_benchmark_test.go new file mode 100644 index 00000000..839cf710 --- /dev/null +++ b/binding/default_validator_benchmark_test.go @@ -0,0 +1,20 @@ +package binding + +import ( + "errors" + "strconv" + "testing" +) + +func BenchmarkSliceValidateError(b *testing.B) { + const size int = 100 + for i := 0; i < b.N; i++ { + e := make(sliceValidateError, size) + for j := 0; j < size; j++ { + e[j] = errors.New(strconv.Itoa(j)) + } + if len(e.Error()) == 0 { + b.Errorf("error") + } + } +} diff --git a/binding/default_validator_test.go b/binding/default_validator_test.go index e9c6de44..e9debe59 100644 --- a/binding/default_validator_test.go +++ b/binding/default_validator_test.go @@ -16,6 +16,26 @@ func TestSliceValidateError(t *testing.T) { want string }{ {"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"}, + {"has zero elements", sliceValidateError{}, ""}, + {"has one element", sliceValidateError{errors.New("test one error")}, "[0]: test one error"}, + {"has two elements", + sliceValidateError{ + errors.New("first error"), + errors.New("second error"), + }, + "[0]: first error\n[1]: second error", + }, + {"has many elements", + sliceValidateError{ + errors.New("first error"), + errors.New("second error"), + nil, + nil, + nil, + errors.New("last error"), + }, + "[0]: first error\n[1]: second error\n[5]: last error", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 690aa2b1b9de7fe61dc15295210f64583dd3b90e Mon Sep 17 00:00:00 2001 From: voidman Date: Wed, 30 Jun 2021 00:53:56 +0800 Subject: [PATCH 2/2] feat(binding): support custom struct tag (#2720) * feat(binding): support custom struct tag Add function `binding.MapFormWithTag` (#2719) * doc: add 'bind form-data with custom struct tag' Add 'Bind form-data request with custom struct and custom tag' section (#2719) * test(binding): add test for MapFromWithTag --- README.md | 55 ++++++++++++++++++++++++++++++++++++ binding/form_mapping.go | 4 +++ binding/form_mapping_test.go | 9 ++++++ 3 files changed, 68 insertions(+) diff --git a/README.md b/README.md index ef37fd20..ef801179 100644 --- a/README.md +++ b/README.md @@ -2021,6 +2021,61 @@ enough to call binding at once. can be called by `c.ShouldBind()` multiple times without any damage to performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)). +### Bind form-data request with custom struct and custom tag + +```go +const ( + customerTag = "url" + defaultMemory = 32 << 20 +) + +type customerBinding struct {} + +func (customerBinding) Name() string { + return "form" +} + +func (customerBinding) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + if err := req.ParseMultipartForm(defaultMemory); err != nil { + if err != http.ErrNotMultipart { + return err + } + } + if err := binding.MapFormWithTag(obj, req.Form, customerTag); err != nil { + return err + } + return validate(obj) +} + +func validate(obj interface{}) error { + if binding.Validator == nil { + return nil + } + return binding.Validator.ValidateStruct(obj) +} + +// Now we can do this!!! +// FormA is a external type that we can't modify it's tag +type FormA struct { + FieldA string `url:"field_a"` +} + +func ListHandler(s *Service) func(ctx *gin.Context) { + return func(ctx *gin.Context) { + var urlBinding = customerBinding{} + var opt FormA + err := ctx.MustBindWith(&opt, urlBinding) + if err != nil { + ... + } + ... + } +} +``` + ### http2 server push http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information. diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 421c0f71..cb66dd4a 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -34,6 +34,10 @@ func mapForm(ptr interface{}, form map[string][]string) error { return mapFormByTag(ptr, form, "form") } +func MapFormWithTag(ptr interface{}, form map[string][]string, tag string) error { + return mapFormByTag(ptr, form, tag) +} + var emptyField = reflect.StructField{} func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index 2675d46b..608594e2 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -145,6 +145,15 @@ func TestMappingForm(t *testing.T) { assert.Equal(t, int(6), s.F) } +func TestMapFormWithTag(t *testing.T) { + var s struct { + F int `externalTag:"field"` + } + err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag") + assert.NoError(t, err) + assert.Equal(t, int(6), s.F) +} + func TestMappingTime(t *testing.T) { var s struct { Time time.Time